Skip to content

Commit 834d8bb

Browse files
devex
1 parent d982da9 commit 834d8bb

3 files changed

Lines changed: 136 additions & 8 deletions

File tree

docs/design/capab1.png

80.9 KB
Loading
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# Using **ClientCapabilities** (`cp1`) with **Managed Identity** and **Downstream APIs**
2+
3+
## Why ClientCapabilities (cp1)?
4+
5+
Adding the `cp1` client‑capability tells Azure AD your service can handle Continuous Access Evaluation (CAE) claims challenges. Tokens will include an extra xms_cc claim, allowing near‑realtime revocation.
6+
7+
## Purpose
8+
9+
This guide shows how to declare the cp1 client capability (Continuous Access Evaluation)
10+
in a .NET service that authenticates with Managed Identity and calls
11+
resources through Microsoft.Identity.Web’s Downstream API helpers. You’ll learn
12+
how to configure appsettings.json, acquire a CAE‑ready token (xms_cc=cp1),
13+
and optionally invoke Azure Key Vault — all without secrets or certificates.
14+
15+
## Prerequisites
16+
17+
| Requirement | Notes |
18+
|-------------------------------------------|-----------------------------------------------------------|
19+
| .NET SDK | **8.0** or newer |
20+
| Microsoft.Identity.Web | **latest** |
21+
| Microsoft.Identity.Abstractions | **latest** |
22+
| Azure resource with Managed Identity | VM, App Service, Functions, Container Apps, etc. |
23+
| Key Vault permission | MI needs **Get Secret** on the vault |
24+
25+
> **Tip** — For **user-assigned** MI, note the **Client ID** (GUID). Leave it blank for **system-assigned** MI.
26+
27+
## Specification: Configuration Layers
28+
29+
| Layer | JSON path / property | Examples | Lifespan | Rationale |
30+
|------------------|-----------------------------------------------------------|-----------------------------------------------------------------------------------------------------------|---------------------------|-------------------------------------------------------------------------------------------------------------|
31+
| **App-level** | `AzureAd` root → `ClientCapabilities` | `ClientCapabilities: [ "cp1" ]`<br>`Instance`, `TenantId`, credentials | **Process-wide** — set once at startup | MSAL builds the client object **once**; everything here is stamped onto **every** token request. |
32+
| **Request-level**| `AcquireTokenOptions` (inside each *DownstreamApi* entry, or passed programmatically) |`ManagedIdentity.UserAssignedClientId`<br>• `Claims` (CAE challenges)<br>• `ForceRefresh`<br>• `UseMtlsPoP` | **Per token call** | These knobs can differ by resource or retry and therefore belong in the per-call options object. |
33+
34+
### Why `cp1` stays at the App-level
35+
* **Identity of the client** – signals a *capability of the app*, not an individual request.
36+
37+
## Configuration
38+
39+
```
40+
{
41+
"AzureAd": {
42+
"ClientCapabilities": [ "cp1" ]
43+
},
44+
45+
"AzureKeyVault": {
46+
"BaseUrl": "https://<your‑vault>.vault.azure.net/",
47+
"RelativePath": "secrets/<secret-name>?api-version=7.4",
48+
"RequestAppToken": true,
49+
"Scopes": [ "https://vault.azure.net/.default" ],
50+
51+
"AcquireTokenOptions": {
52+
"ManagedIdentity": {
53+
"UserAssignedClientId": "<user-assigned-mi-client-id>"
54+
}
55+
}
56+
}
57+
}
58+
```
59+
60+
## Code
61+
62+
```cs
63+
// Copyright (c) Microsoft Corporation. All rights reserved.
64+
// Licensed under the MIT License.
65+
66+
using Microsoft.Extensions.DependencyInjection;
67+
using Microsoft.Extensions.Logging;
68+
using Microsoft.Identity.Abstractions;
69+
using Microsoft.Identity.Web;
70+
using System.Net;
71+
using System.Text.Json;
72+
73+
// ── 1. bootstrap factory (reads appsettings.json automatically) ─────────
74+
var factory = TokenAcquirerFactory.GetDefaultInstance();
75+
76+
// optional console logging
77+
factory.Services.AddLogging(b => b.AddConsole().SetMinimumLevel(LogLevel.Warning));
78+
79+
// ── 2. register the downstream API using the "AzureKeyVault" section ────
80+
factory.Services.AddDownstreamApi("AzureKeyVault",
81+
factory.Configuration.GetSection("AzureKeyVault"));
82+
IServiceProvider sp = factory.Build();
83+
IDownstreamApi api = sp.GetRequiredService<IDownstreamApi>();
84+
85+
// ── 3. call the vault (app-token path) ──────────────────────────────────
86+
HttpResponseMessage response = await api.CallApiForAppAsync("AzureKeyVault");
87+
88+
if (response.StatusCode != HttpStatusCode.OK)
89+
{
90+
Console.WriteLine($"Vault returned {(int)response.StatusCode} {response.ReasonPhrase}");
91+
return;
92+
}
93+
94+
//Get the secret value from the response
95+
using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
96+
97+
// Check if the "value" property exists and is a string
98+
if (doc.RootElement.TryGetProperty("value", out var valueElement) && valueElement.ValueKind == JsonValueKind.String)
99+
{
100+
// Retrieve the secret value but do not print it
101+
string secret = valueElement.GetString()!;
102+
103+
// Optionally, you can check if the secret is not null or empty
104+
if (!string.IsNullOrEmpty(secret))
105+
{
106+
Console.WriteLine("Secret retrieved successfully (non-null).");
107+
}
108+
else
109+
{
110+
Console.WriteLine("Secret value was empty.");
111+
}
112+
}
113+
```
114+
115+
## Options as seen in MSAL
116+
117+
![alt text](capab1.png)
Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,29 @@
11
{
2+
// authentication settings (apply to the whole app)
3+
"AzureAd": {
4+
// Continuous Access Evaluation capability at app-level
5+
"ClientCapabilities": [ "cp1" ]
6+
},
7+
8+
// downstream API settings (per-resource)
29
"AzureKeyVault": {
3-
"BaseUrl": "https://revoguardkeyvault.vault.azure.net/",
4-
"RelativePath": "secrets/RevoGuardSecret?api-version=7.4",
10+
"BaseUrl": "https://msidlabs.vault.azure.net/",
11+
"RelativePath": "secrets/msidlab4?api-version=7.4",
512
"RequestAppToken": true,
613
"Scopes": [ "https://vault.azure.net/.default" ],
7-
// capabilities for the client are not being passed
8-
// need to fix this in the next commit
9-
"ClientCapabilities": [ "cp1" ],
10-
14+
// per request settings
1115
"AcquireTokenOptions": {
1216
"ManagedIdentity": {
13-
// ► leave empty for system-assigned MI
14-
"UserAssignedClientId": "04ca4d6a-c720-4ba1-aa06-f6634b73fe7a"
17+
// user-assigned MI; omit for system-assigned
18+
"UserAssignedClientId": "4b7a4b0b-ecb2-409e-879a-1e21a15ddaf6"
1519
}
1620
}
21+
},
22+
23+
"Logging": {
24+
"LogLevel": {
25+
"Default": "Warning",
26+
"Microsoft": "Information"
27+
}
1728
}
1829
}

0 commit comments

Comments
 (0)