Skip to content

Commit a712055

Browse files
authored
docs(design): devex proposal for Bearer tokens with bound credentials (#3833)
* Refine bearer tokens with bound credentials proposal This update refines the proposal for bearer tokens with bound credentials in Microsoft.Identity.Web, including clarifications on naming conventions and the developer experience for opting into the bound-credential flow. * Update bearer_tokens_with_bound_credentials_devex.md * Update bearer_tokens_with_bound_credentials_devex.md
1 parent 43950d9 commit a712055

1 file changed

Lines changed: 321 additions & 0 deletions

File tree

Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
# Microsoft.Identity.Web – Bearer tokens with Bound credentials
2+
3+
> **Status:** Draft. The final API surface (property name and type) is being
4+
> decided in
5+
> [microsoft-identity-abstractions-for-dotnet #252](https://github.com/AzureAD/microsoft-identity-abstractions-for-dotnet/pull/252).
6+
> The current proposal on that PR is `public bool UseBoundCredential { get; set; }`;
7+
> reviewer feedback has suggested renaming to `PreferBoundCredential` and
8+
> changing the type to `bool?` so the default can evolve over time. This
9+
> IdWeb spec will be updated once the abstractions PR is finalized.
10+
11+
## Why bound credentials for Bearer tokens?
12+
13+
Today, when a confidential client app requests a Bearer access token, the
14+
*credential* presented to Entra is also a Bearer artifact: either a client
15+
assertion JWT signed locally with an X.509 certificate, or a signed assertion
16+
issued by a federation provider (e.g. Managed Identity).
17+
18+
A **bound credential** is a sender-constrained variant:
19+
20+
* For an **X.509 certificate**, MSAL calls the Entra mTLS endpoint and presents
21+
the certificate over mTLS. No client assertion JWT is created.
22+
* For a **Federated Identity Credential (FIC) with Managed Identity**, MSAL
23+
fetches a bound credential bundle (signed assertion + binding certificate)
24+
from MSI and calls Entra over mTLS using the binding certificate.
25+
26+
In **both** cases the access token returned to the app is a regular **Bearer
27+
token**. The downstream API is unaffected.
28+
29+
This spec adds the developer experience for opting credentials into the
30+
bound-credential flow in Microsoft.Identity.Web.
31+
32+
## What this spec adds to **Microsoft.Identity.Web**
33+
34+
* **Per-credential opt-in** – one configuration knob
35+
(`"UseBoundCredential": true` — final name pending the discussion on
36+
[microsoft-identity-abstractions-for-dotnet #252](https://github.com/AzureAD/microsoft-identity-abstractions-for-dotnet/pull/252)).
37+
* **Per-credential scope** – each entry in `AzureAd.ClientCredentials[]`
38+
decides for itself. An app can declare a bound primary credential and a
39+
non-bound fallback in the same array, and IdWeb will honor each entry
40+
independently.
41+
* **Two credential paths** – the opt-in is honored for `Certificate` (and the
42+
certificate-flavored sources) and `SignedAssertionFromManagedIdentity`.
43+
Other source types ignore the flag.
44+
* **No change to downstream APIs** – the access token returned is a regular
45+
Bearer; the `DownstreamApi` section is untouched.
46+
47+
The goal is **zero-touch** for downstream consumers and **one-line** for the
48+
app developer at the credential level.
49+
50+
## Support matrix
51+
52+
| Credential source | Bearer + bound credential after this change |
53+
|----------------------------------------------------------------|---------------------------------------------|
54+
| Certificate (`Certificate`, `KeyVault`, `Path`, `Base64Encoded`, `StoreWith*`, `ManagedCertificate`) ||
55+
| `SignedAssertionFromManagedIdentity` (FIC with MI) ||
56+
| `SignedAssertion` from OIDC IdP (`Microsoft.Identity.Web.OidcFIC`) | Planned — tracked in [#3851](https://github.com/AzureAD/microsoft-identity-web/issues/3851) |
57+
| `ClientSecret` | n/a |
58+
| `SignedAssertion*` (non-MI) | Ignored |
59+
60+
### OIDC FIC as a first-class binding-capable source
61+
62+
`OidcIdpSignedAssertionProvider` (in `Microsoft.Identity.Web.OidcFIC`) currently
63+
produces bearer-only JWT assertions from an external OIDC IdP. The same
64+
`SupportsTokenBinding` / `GetSignedAssertionWithBindingAsync` extension point
65+
that `ManagedIdentityClientAssertion` opts into can be extended to OIDC FIC
66+
once the cert-sourcing model is settled. That work is tracked in
67+
[#3851](https://github.com/AzureAD/microsoft-identity-web/issues/3851); this
68+
spec will be revised alongside that issue.
69+
70+
## How developers wire things up today (non-bound Bearer)
71+
72+
### Certificate
73+
74+
```json
75+
{
76+
"AzureAd": {
77+
"Instance": "https://login.microsoftonline.com/",
78+
"TenantId": "<tenant>",
79+
"ClientId": "<app-registration-client-id>",
80+
"ClientCredentials": [
81+
{
82+
"SourceType": "StoreWithDistinguishedName",
83+
"CertificateStorePath": "CurrentUser/My",
84+
"CertificateDistinguishedName": "CN=MyAppCert"
85+
}
86+
]
87+
}
88+
}
89+
```
90+
91+
### FIC with Managed Identity
92+
93+
```json
94+
{
95+
"AzureAd": {
96+
"Instance": "https://login.microsoftonline.com/",
97+
"TenantId": "<tenant>",
98+
"ClientId": "<app-registration-client-id>",
99+
"ClientCredentials": [
100+
{
101+
"SourceType": "SignedAssertionFromManagedIdentity",
102+
"ManagedIdentityClientId": "<UAMI-client-id>",
103+
"TokenExchangeUrl": "api://AzureADTokenExchange"
104+
}
105+
]
106+
}
107+
}
108+
```
109+
110+
In both cases the call to `/token` carries a client assertion JWT on the
111+
wire.
112+
113+
## Design Goals
114+
115+
| # | Goal | Success Metric |
116+
|-----|---------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------|
117+
| G1 | Honor `UseBoundCredential` for `Certificate`-flavored credentials. | Token call goes to the Entra mTLS endpoint over mTLS; no JWT assertion on the wire. |
118+
| G2 | Honor `UseBoundCredential` for `SignedAssertionFromManagedIdentity`. | Inner MI fetches a bound credential bundle; outer call goes to Entra over mTLS. |
119+
| G3 | Per-credential opt-in. | Two credentials in the same `ClientCredentials[]` can have different settings. |
120+
| G4 | No downstream changes. | Existing `DownstreamApi` sections and `IDownstreamApi` calls work unchanged. |
121+
| G5 | Clear behavior when the platform cannot provide a binding certificate. | Either silent fallback to non-bound or a clear exception, depending on the final property name. |
122+
123+
## Public API Impact
124+
125+
The abstractions surface
126+
([microsoft-identity-abstractions-for-dotnet #252](https://github.com/AzureAD/microsoft-identity-abstractions-for-dotnet/pull/252))
127+
already adds the property:
128+
129+
```csharp
130+
public bool UseBoundCredential { get; set; }
131+
```
132+
(Final name and type subject to review on that PR.)
133+
134+
No new types or extension methods on the IdWeb surface itself. IdWeb reads
135+
the property at credential-load time and forwards it to MSAL.
136+
137+
## Configuration Example
138+
139+
### Certificate + bound credential
140+
141+
```json
142+
{
143+
"AzureAd": {
144+
"Instance": "https://login.microsoftonline.com/",
145+
"TenantId": "<tenant>",
146+
"ClientId": "<app-registration-client-id>",
147+
"ClientCredentials": [
148+
{
149+
"SourceType": "StoreWithDistinguishedName",
150+
"CertificateStorePath": "CurrentUser/My",
151+
"CertificateDistinguishedName": "CN=MyAppCert",
152+
"UseBoundCredential": true
153+
}
154+
]
155+
}
156+
}
157+
```
158+
159+
### FIC with MI + bound credential
160+
161+
```json
162+
{
163+
"AzureAd": {
164+
"Instance": "https://login.microsoftonline.com/",
165+
"TenantId": "<tenant>",
166+
"ClientId": "<app-registration-client-id>",
167+
"ClientCredentials": [
168+
{
169+
"SourceType": "SignedAssertionFromManagedIdentity",
170+
"ManagedIdentityClientId": "<UAMI-client-id>",
171+
"TokenExchangeUrl": "api://AzureADTokenExchange",
172+
"UseBoundCredential": true
173+
}
174+
]
175+
}
176+
}
177+
```
178+
179+
### Mixed (bound primary, non-bound fallback)
180+
181+
```json
182+
{
183+
"AzureAd": {
184+
"ClientCredentials": [
185+
{
186+
"SourceType": "StoreWithDistinguishedName",
187+
"CertificateDistinguishedName": "CN=Primary",
188+
"UseBoundCredential": true
189+
},
190+
{
191+
"SourceType": "StoreWithDistinguishedName",
192+
"CertificateDistinguishedName": "CN=Fallback"
193+
}
194+
]
195+
}
196+
}
197+
```
198+
199+
> **Note** : The same configuration block works in *appsettings.json* or can
200+
> be supplied programmatically via
201+
> `MicrosoftIdentityApplicationOptions.ClientCredentials`.
202+
203+
## Code Snippets
204+
205+
### Acquiring a Bearer token with a bound credential
206+
207+
```csharp
208+
// 1 – set up the TokenAcquirerFactory
209+
var factory = TokenAcquirerFactory.GetDefaultInstance();
210+
211+
// 2 – register the downstream API (unchanged from today)
212+
factory.Services.AddDownstreamApi("Contoso",
213+
factory.Configuration.GetSection("Contoso"));
214+
215+
IServiceProvider sp = factory.Build();
216+
IDownstreamApi api = sp.GetRequiredService<IDownstreamApi>();
217+
218+
// 3 – call the API. Id.Web reads ClientCredentials[].UseBoundCredential
219+
// and wires the bound-credential flow into the MSAL builder.
220+
HttpResponseMessage resp = await api.CallApiForAppAsync("Contoso");
221+
```
222+
223+
The application code is **identical** to the Bearer-only case. The opt-in
224+
lives entirely in `appsettings.json`.
225+
226+
### Using **IAuthorizationHeaderProvider**
227+
228+
`IAuthorizationHeaderProvider` is fully supported. The returned header is a
229+
standard `Bearer <token>` header:
230+
231+
```csharp
232+
var headerProvider = sp.GetRequiredService<IAuthorizationHeaderProvider>();
233+
string header = await headerProvider.CreateAuthorizationHeaderForAppAsync(
234+
scope: "https://contoso.com/.default");
235+
// header => "Bearer eyJ0eXAi..."
236+
```
237+
238+
## How it works
239+
240+
At credential-load time, IdWeb inspects `credential.UseBoundCredential` on
241+
each `CredentialDescription` and forwards it to MSAL:
242+
243+
1. **Certificate-flavored credential, `UseBoundCredential == true`**
244+
`ConfidentialClientApplicationBuilderExtension.WithClientCredentialsAsync`
245+
calls `WithCertificate(cert, new CertificateOptions { SendCertificateOverMtls = true })`.
246+
MSAL routes the `/token` call to the Entra mTLS endpoint over mTLS using
247+
the certificate; no JWT assertion is created.
248+
2. **`SignedAssertionFromManagedIdentity`, `UseBoundCredential == true`**
249+
`ManagedIdentityClientAssertion` calls the V2 MI credential endpoint to
250+
obtain a bound credential bundle (signed assertion + binding certificate),
251+
and IdWeb passes the bundle to MSAL via
252+
`WithClientAssertion(Func<AssertionRequestOptions, CancellationToken, Task<ClientSignedAssertion>>)`.
253+
MSAL uses the binding certificate to talk to Entra over mTLS.
254+
3. **Other source types, or `UseBoundCredential == false`** — current
255+
behavior is preserved. No change.
256+
257+
In all cases the access token returned to the app is a standard Bearer
258+
token. The downstream HTTP call is the existing `IDownstreamApi` /
259+
`IAuthorizationHeaderProvider` flow.
260+
261+
## Samples
262+
263+
Two samples will be added, modeled on the existing `daemon-app-msi`:
264+
265+
| Folder | Demonstrates |
266+
|--------------------------------------------------------------|-------------------------------------------------------------|
267+
| `tests/DevApps/daemon-app/daemon-app-cert-bound` | Certificate credential opted into Bearer-over-mTLS |
268+
| `tests/DevApps/daemon-app/daemon-app-fic-bound` | FIC + MI credential opted into Bearer-over-mTLS |
269+
270+
Each sample is one `Program.cs` (boilerplate `TokenAcquirerFactory` +
271+
`IDownstreamApi.CallApiForAppAsync`) plus one `appsettings.json` matching
272+
the configuration example above.
273+
274+
## Documentation updates
275+
276+
| File | Update |
277+
|------------------------------------------------------------|---------------------------------------------------------------------------------------------------------|
278+
| `docs/authentication/credentials/certificates.md` | Add a "Use as a bound credential" subsection showing the `UseBoundCredential: true` opt-in. |
279+
| `docs/authentication/credentials/certificateless.md` | Add a "Use as a bound credential" subsection for FIC with MI, side-by-side with the existing config. |
280+
281+
## Prerequisites
282+
283+
* `Microsoft.Identity.Web` takes a dependency on the abstractions version
284+
that ships `CredentialDescription.UseBoundCredential` (merge of
285+
[microsoft-identity-abstractions-for-dotnet #252](https://github.com/AzureAD/microsoft-identity-abstractions-for-dotnet/pull/252)).
286+
* MSAL.NET already shipped the underlying capability in
287+
[microsoft-authentication-library-for-dotnet #5849](https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/pull/5849).
288+
* The flow currently covers `client_credentials` and is **Entra-only**
289+
(not enabled in all clouds).
290+
* The application's tenant and client do **not** need to be on any allow-list
291+
for this flow.
292+
* Platform support for the binding certificate today:
293+
* Certificate flow: any platform — uses the app's existing certificate.
294+
* FIC flow: requires the V2 MI credential endpoint, currently Windows
295+
Confidential VMs (Key Guard + attestation).
296+
297+
## Open questions
298+
299+
1. **Naming** – per
300+
[the discussion on #5791](https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/5791),
301+
should the property be `UseBoundCredential` (binary, explicit) or
302+
`PreferBoundCredential` (best-effort, with silent fallback when the
303+
platform cannot provide a binding certificate)? Decision lives in
304+
abstractions PR #252; IdWeb consumes whatever ships.
305+
2. **Default-flip strategy** – if the property is renamed to
306+
`PreferBoundCredential` and the type changes to `bool?`, IdWeb can pass a
307+
"host default" through to MSAL when the value is `null`. Worth deciding
308+
whether IdWeb opts apps in by default in a future release, or stays
309+
off-by-default and requires explicit opt-in.
310+
3. **OIDC FIC cert source** – when the assertion is issued by an external OIDC
311+
IdP, where does the binding certificate come from? Tracked in
312+
[#3851](https://github.com/AzureAD/microsoft-identity-web/issues/3851).
313+
314+
### reference
315+
316+
* [microsoft-authentication-library-for-dotnet issue #5791](https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/5791)
317+
– feature request.
318+
* [microsoft-authentication-library-for-dotnet PR #5849](https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/pull/5849)
319+
– MSAL.NET implementation (shipped).
320+
* [microsoft-identity-abstractions-for-dotnet PR #252](https://github.com/AzureAD/microsoft-identity-abstractions-for-dotnet/pull/252)
321+
`CredentialDescription.UseBoundCredential` (in review).

0 commit comments

Comments
 (0)