Skip to content

Commit ef579b2

Browse files
init
1 parent e48283a commit ef579b2

22 files changed

+705
-12
lines changed

Microsoft.Identity.Web.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Identity.Web.Oidc
164164
EndProject
165165
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Identity.Web.UI.Test", "tests\Microsoft.Identity.Web.UI.Test\Microsoft.Identity.Web.UI.Test.csproj", "{CF31F33A-E5F5-DB57-4FEF-81BDAFD497C8}"
166166
EndProject
167+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "daemon-app-msi", "tests\DevApps\daemon-app\daemon-app-msi\daemon-app-msi.csproj", "{A8181404-23E0-D38B-454C-D16ECDB18B9F}"
168+
EndProject
167169
Global
168170
GlobalSection(SolutionConfigurationPlatforms) = preSolution
169171
Debug|Any CPU = Debug|Any CPU
@@ -387,6 +389,10 @@ Global
387389
{CF31F33A-E5F5-DB57-4FEF-81BDAFD497C8}.Debug|Any CPU.Build.0 = Debug|Any CPU
388390
{CF31F33A-E5F5-DB57-4FEF-81BDAFD497C8}.Release|Any CPU.ActiveCfg = Release|Any CPU
389391
{CF31F33A-E5F5-DB57-4FEF-81BDAFD497C8}.Release|Any CPU.Build.0 = Release|Any CPU
392+
{A8181404-23E0-D38B-454C-D16ECDB18B9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
393+
{A8181404-23E0-D38B-454C-D16ECDB18B9F}.Debug|Any CPU.Build.0 = Debug|Any CPU
394+
{A8181404-23E0-D38B-454C-D16ECDB18B9F}.Release|Any CPU.ActiveCfg = Release|Any CPU
395+
{A8181404-23E0-D38B-454C-D16ECDB18B9F}.Release|Any CPU.Build.0 = Release|Any CPU
390396
EndGlobalSection
391397
GlobalSection(SolutionProperties) = preSolution
392398
HideSolutionNode = FALSE
@@ -461,6 +467,7 @@ Global
461467
{E927D215-A96C-626C-9A1A-CF99876FE7B4} = {45B20A78-91F8-4DD2-B9AD-F12D3A93536C}
462468
{8DA7A2C6-00D4-4CF1-8145-448D7B7B4E5A} = {1DDE1AAC-5AE6-4725-94B6-A26C58D3423F}
463469
{CF31F33A-E5F5-DB57-4FEF-81BDAFD497C8} = {B4E72F1C-603F-437C-AAA1-153A604CD34A}
470+
{A8181404-23E0-D38B-454C-D16ECDB18B9F} = {E37CDBC1-18F6-4C06-A3EE-532C9106721F}
464471
EndGlobalSection
465472
GlobalSection(ExtensibilityGlobals) = postSolution
466473
SolutionGuid = {104367F1-CE75-4F40-B32F-F14853973187}

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+
# Microsoft.Identity.Web – Continuous Access Evaluation (CAE) for Managed Identity
2+
3+
## Why Continuous Access Evaluation?
4+
5+
Continuous Access Evaluation (CAE) lets Microsoft Entra ID revoke tokens or demand extra claims almost immediately when risk changes (user disabled, password reset, network change, policy update, etc.).
6+
7+
A workload opts-in by sending the client-capability **`cp1`** when acquiring tokens. Entra then includes an **`xms_cc`** claim in the token so downstream Microsoft services know the caller can handle claims challenges.
8+
9+
## What this spec adds to **Microsoft.Identity.Web**
10+
11+
* **Declarative opt-in** – one configuration knob (`ClientCapabilities: [ "cp1" ]`).
12+
* **Transparent 401 recovery** – when a downstream Microsoft API responds with a 401+claims challenge, Id.Web automatically:
13+
1. extracts the claims body;
14+
2. bypasses its token cache;
15+
3. requests a fresh token that satisfies the claims;
16+
4. retries the HTTP call **once**.
17+
18+
The goal is **zero-touch** for most developers.
19+
20+
## Typical Flow (Managed Identity → Downstream API)
21+
22+
```text
23+
1. Id.Web → MSI endpoint : GET /token?resource=...&xms_cc=cp1 ──▶
24+
2. MSI → ESTS : request includes cp1 ──▶
25+
3. ESTS → Id.Web : access_token (xms_cc claim present) ◀──
26+
4. Id.Web → Downstream API : GET /resource ⟶ 200 OK │
27+
5. Policy change occurs │
28+
6. Id.Web → Downstream API : GET /resource ⟶ 401 + claims payload │
29+
7. Id.Web handles challenge (steps 1-4 again, bypassing msal cache) ──▶
30+
```
31+
32+
## Design Goals
33+
34+
| # | Goal | Success Metric |
35+
|-----|--------------------------------------------------------------|----------------------------------------------------------|
36+
| G1 | Transparent CAE retry with cache-bypass on 401 claims challenge. | Downstream API call recovers without developer code. |
37+
| G2 | Declarative client capabilities via configuration. | Single place to add `cp1`; all MI calls include it. |
38+
39+
## Public API Impact
40+
41+
no changes to the public api.
42+
43+
## Configuration Example
44+
45+
```
46+
{
47+
"AzureAd": {
48+
"ClientCapabilities": [ "cp1" ]
49+
},
50+
51+
// Example downstream API definition (Contoso Storage API)
52+
"ContosoStorage": {
53+
"BaseUrl": "https://storage.contoso.com/",
54+
"RelativePath": "data/records?api-version=1.0",
55+
"RequestAppToken": true,
56+
"Scopes": [ "https://storage.contoso.com/.default" ],
57+
"AcquireTokenOptions": {
58+
"ManagedIdentity": {
59+
// optional – omit for system-assigned MI
60+
"UserAssignedClientId": "<client-id>"
61+
}
62+
}
63+
}
64+
}
65+
```
66+
67+
> **Note** : The same configuration block works in *appsettings.json* or can be supplied programmatically.
68+
69+
70+
## Code Snippets
71+
72+
### Registering & Calling a Downstream API
73+
74+
```csharp
75+
// 1 – set up the TokenAcquirerFactory (test-helper shown for brevity)
76+
var factory = TokenAcquirerFactory.GetDefaultInstance();
77+
78+
// 2 – register the downstream API using section "ContosoStorage"
79+
factory.Services.AddDownstreamApi("ContosoStorage",
80+
factory.Configuration.GetSection("ContosoStorage"));
81+
82+
IServiceProvider sp = factory.Build();
83+
IDownstreamApi api = sp.GetRequiredService<IDownstreamApi>();
84+
85+
// 3 – call the API (Id.Web handles CAE automatically)
86+
HttpResponseMessage resp = await api.CallApiForAppAsync("ContosoStorage");
87+
```
88+
89+
### Using **IAuthorizationHeaderProvider** (advanced)
90+
91+
`IAuthorizationHeaderProvider` is fully supported with Managed Identity. Claims challenges propagate the same way:
92+
93+
```csharp
94+
var headerProvider = sp.GetRequiredService<IAuthorizationHeaderProvider>();
95+
string header = await headerProvider.CreateAuthorizationHeaderForAppAsync(
96+
scope: "https://storage.contoso.com/.default",
97+
options: new AuthorizationHeaderProviderOptions
98+
{
99+
AcquireTokenOptions = new AcquireTokenOptions
100+
{
101+
ManagedIdentity = new ManagedIdentityOptions(), // system-assigned MI
102+
Claims = claimsChallengeJson // when retrying after 401
103+
}
104+
});
105+
```
106+
107+
## Telemetry
108+
109+
We rely on server side telemetry for the token revocation features.
110+
111+
Server dashboards add MI success‑rate with/without cp1.
112+
113+
## Options as seen in MSAL
114+
115+
![alt text](capab1.png)
116+
117+
### reference - [How to use Continuous Access Evaluation enabled APIs in your applications](https://learn.microsoft.com/en-us/entra/identity-platform/app-resilience-continuous-access-evaluation?tabs=dotnet)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.Identity.Client;
5+
6+
namespace Microsoft.Identity.Web
7+
{
8+
/// <summary>
9+
/// **TEST-ONLY.** Allows unit tests to supply a custom <see cref="IMsalHttpClientFactory"/>.
10+
/// </summary>
11+
internal interface IManagedIdentityTestHttpClientFactory
12+
{
13+
IMsalHttpClientFactory Create();
14+
}
15+
}
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
const Microsoft.Identity.Web.IDWebErrorMessage.ExceptionAcquiringTokenForConfidentialClient = "IDW10501: Exception acquiring token for a confidential client: " -> string!
1+
const Microsoft.Identity.Web.IDWebErrorMessage.ExceptionAcquiringTokenForConfidentialClient = "IDW10501: Exception acquiring token for a confidential client: " -> string!
2+
Microsoft.Identity.Web.IManagedIdentityTestHttpClientFactory
3+
Microsoft.Identity.Web.IManagedIdentityTestHttpClientFactory.Create() -> Microsoft.Identity.Client.IMsalHttpClientFactory!
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
const Microsoft.Identity.Web.IDWebErrorMessage.ExceptionAcquiringTokenForConfidentialClient = "IDW10501: Exception acquiring token for a confidential client: " -> string!
1+
const Microsoft.Identity.Web.IDWebErrorMessage.ExceptionAcquiringTokenForConfidentialClient = "IDW10501: Exception acquiring token for a confidential client: " -> string!
2+
Microsoft.Identity.Web.IManagedIdentityTestHttpClientFactory
3+
Microsoft.Identity.Web.IManagedIdentityTestHttpClientFactory.Create() -> Microsoft.Identity.Client.IMsalHttpClientFactory!
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
const Microsoft.Identity.Web.IDWebErrorMessage.ExceptionAcquiringTokenForConfidentialClient = "IDW10501: Exception acquiring token for a confidential client: " -> string!
2-
Microsoft.Identity.Web.ClientInfoJsonContext
2+
Microsoft.Identity.Web.ClientInfoJsonContext
3+
Microsoft.Identity.Web.IManagedIdentityTestHttpClientFactory
4+
Microsoft.Identity.Web.IManagedIdentityTestHttpClientFactory.Create() -> Microsoft.Identity.Client.IMsalHttpClientFactory!
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
const Microsoft.Identity.Web.IDWebErrorMessage.ExceptionAcquiringTokenForConfidentialClient = "IDW10501: Exception acquiring token for a confidential client: " -> string!
2-
Microsoft.Identity.Web.ClientInfoJsonContext
2+
Microsoft.Identity.Web.ClientInfoJsonContext
3+
Microsoft.Identity.Web.IManagedIdentityTestHttpClientFactory
4+
Microsoft.Identity.Web.IManagedIdentityTestHttpClientFactory.Create() -> Microsoft.Identity.Client.IMsalHttpClientFactory!
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
const Microsoft.Identity.Web.IDWebErrorMessage.ExceptionAcquiringTokenForConfidentialClient = "IDW10501: Exception acquiring token for a confidential client: " -> string!
2-
Microsoft.Identity.Web.ClientInfoJsonContext
2+
Microsoft.Identity.Web.ClientInfoJsonContext
3+
Microsoft.Identity.Web.IManagedIdentityTestHttpClientFactory
4+
Microsoft.Identity.Web.IManagedIdentityTestHttpClientFactory.Create() -> Microsoft.Identity.Client.IMsalHttpClientFactory!
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
const Microsoft.Identity.Web.IDWebErrorMessage.ExceptionAcquiringTokenForConfidentialClient = "IDW10501: Exception acquiring token for a confidential client: " -> string!
2-
Microsoft.Identity.Web.ClientInfoJsonContext
2+
Microsoft.Identity.Web.ClientInfoJsonContext
3+
Microsoft.Identity.Web.IManagedIdentityTestHttpClientFactory
4+
Microsoft.Identity.Web.IManagedIdentityTestHttpClientFactory.Create() -> Microsoft.Identity.Client.IMsalHttpClientFactory!

0 commit comments

Comments
 (0)