From 57769df9b29998c3008aaa571d48964833cc8e3a Mon Sep 17 00:00:00 2001 From: Gladwin Johnson <90415114+gladjohn@users.noreply.github.com> Date: Tue, 30 Jun 2026 21:14:31 -0700 Subject: [PATCH 1/2] Token Binding Demo Helper Token Binding Demo Helper --- docs/msi_v2/token-binding-demo.html | 885 ++++++++++++++++++++++++++++ 1 file changed, 885 insertions(+) create mode 100644 docs/msi_v2/token-binding-demo.html diff --git a/docs/msi_v2/token-binding-demo.html b/docs/msi_v2/token-binding-demo.html new file mode 100644 index 0000000000..1eb41e30e3 --- /dev/null +++ b/docs/msi_v2/token-binding-demo.html @@ -0,0 +1,885 @@ + + + + + + +S2S Token Binding (mTLS PoP) β€” Interactive Guide + + + + +
+

πŸ” S2S Token Binding (mTLS Proof-of-Possession)

+

An interactive walkthrough of service-to-service token binding β€” what it is, the supported scenarios + (SN+I, Managed Identity, FIC), how to do it in MSAL, Microsoft Identity Web, and the Azure Key Vault SDK, and a short quiz.

+
+ SN+I Β· GA + Managed Identity Β· Public preview + AKV SDK Β· Beta +
+
+
+ +
+ +
+
Concept
+

What is S2S token binding?

+

A standard OAuth access token is a bearer token: whoever holds it can use it. If it leaks β€” + via a log, a proxy, or an SSRF β€” it can be replayed from anywhere until it expires.

+

Token binding issues an access token cryptographically bound to a private key the caller holds, + and the token is presented over mutual-TLS Proof-of-Possession (mTLS PoP). The caller must + prove possession of that key, so a leaked token is useless without it. The resource matches the presented + binding certificate to the token's cnf (confirmation) claim.

+
Status: the SN+I (certificate) path is GA. + The Managed Identity path is in public preview β€” on the MSIT tenant it + works without allow-listing; other tenants may require allow-listing + (tokenbindingfeatureteam@service.microsoft.com). + The target resource must opt in to accept bound tokens.
+

The two tokens, side by side

+
+
Bearer (unbound) +

Steal it β†’ replay anywhere β†’ βœ… works for the attacker. This is the risk.

+
Bound (mTLS PoP) +

Steal it β†’ replay elsewhere β†’ ❌ rejected, no bound key. Sender-constrained.

+
+

Why it matters

+ + + + + +
BearerToken-bound (mTLS PoP)
Replay a leaked tokenWorks for the attackerRejected β€” no bound key
Replay from another machineWorksKey is sender-constrained
Credential typePortable secretSender-constrained
+

Token binding is a core Secure Future Initiative (SFI) pattern: it removes token replay as a + class of attack for high-value S2S calls.

+
+ +
+

Two credential sources (and FIC composes either)

+

The binding works the same way at the wire; what differs is where the binding key lives.

+
+
+

πŸͺͺ SN+I (certificate) GA

+

The caller is an app registration authenticating with a certificate + (Subject Name + Issuer). The token is bound to that certificate β€” no VM or KeyGuard required.

+

Optionally, you can import the certificate and store its private key in KeyGuard (VBS-isolated, non-exportable) for hardware-backed protection of the binding key.

+

SN+I Authentication wiki β†’

+
+
+

βš™οΈ Managed Identity Public preview

+

The caller is the managed identity. The binding key is generated in + VBS / KeyGuard on a Trusted Launch / Confidential VM and attested by + Microsoft Azure Attestation (MAA).

+
+
+

πŸ”— FIC (Federated Identity Credential)

+

An app registration uses a federated credential β€” an SN+I certificate or a + Managed Identity β€” as its client assertion. FIC composes the two sources above.

+
+
+
+ +
+

Supported scenarios

+ + + + + + + + +
ScenarioCredentialHostStatus
SN+I (certificate client credentials)App certificate (Subject Name + Issuer)Any host β€” no VM/KeyGuardGA
Pure Managed IdentityManaged Identity (SAMI/UAMI)Trusted Launch / Confidential VM with VBS/KeyGuardPublic preview
FIC (app registration)Federated credential β€” SN+I or Managed IdentityPer the underlying credentialSN+I: GA Β· MSI: preview
+
SN+I certificate authentication is restricted to allow-listed Microsoft internal / LOB + tenants; for new scenarios Microsoft recommends Managed Identity / FIC.
+
+ +
+

Configure Token Binding on a Trusted Launch / Confidential VM

+
This applies to the Managed Identity flow only. The + SN+I (certificate) flow needs no VM or KeyGuard.
+

Prepare a Windows VM so a Managed Identity can mint bound credentials:

+
    +
  1. Create a Trusted Launch VM + or Confidential VM + (Secure Boot + vTPM on).
  2. +
  3. Assign a Managed Identity (SAMI or UAMI) and grant it RBAC on the target (e.g. Key Vault Secrets User).
  4. +
  5. Enable VBS + KeyGuard inside the guest, then reboot:
  6. +
+
reg add "HKLM\SYSTEM\CurrentControlSet\Control\DeviceGuard" /v "EnableVirtualizationBasedSecurity" /t REG_DWORD /d 1 /f
+reg add "HKLM\SYSTEM\CurrentControlSet\Control\DeviceGuard" /v "RequirePlatformSecurityFeatures" /t REG_DWORD /d 1 /f
+reg add "HKLM\SYSTEM\CurrentControlSet\Control\DeviceGuard\Scenarios\HypervisorEnforcedCodeIntegrity" /v "Enabled" /t REG_DWORD /d 1 /f
+Restart-Computer -Force
+
    +
  1. Install .NET 8, the VC++ x64 redistributable, and the MSAL packages + (Microsoft.Identity.Client + Microsoft.Identity.Client.KeyAttestation).
  2. +
+
Regions (preview): Canary (centraluseuap, eastus2euap) + and the PROD region westcentralus. More PROD regions are lighting up as the rollout continues.
+
+ +
+

SDK Support: MSAL .NET the engine

+

MSAL is the engine for all three credential sources. It acquires the bound token and the binding certificate.

+
<ItemGroup>
+  <PackageReference Include="Microsoft.Identity.Client" Version="4.85.2" />
+  <PackageReference Include="Microsoft.Identity.Client.KeyAttestation" Version="4.85.2" />
+</ItemGroup>
+
Gotcha: WithAttestationSupport() lives in the separate + Microsoft.Identity.Client.KeyAttestation package. Without that reference and the matching + using, you get CS1061: ... does not contain a definition for 'WithAttestationSupport'.
+ +

SN+I (certificate) GA

+
var app = ConfidentialClientApplicationBuilder
+    .Create("<app-client-id>")
+    .WithCertificate(certificate, sendX5C: true)
+    .WithAzureRegion("westus3")
+    .Build();
+
+AuthenticationResult result = await app
+    .AcquireTokenForClient(new[] { "api://<target-app-id>/.default" })
+    .WithMtlsProofOfPossession()
+    .ExecuteAsync();
+// result.TokenType == "mtls_pop"; result.BindingCertificate is set
+
Bearer over mTLS: to authenticate over mTLS but receive a regular + bearer token, use CertificateOptions { SendCertificateOverMtls = true } and omit + WithMtlsProofOfPossession().
+ +

Pure Managed Identity Public preview

+
IManagedIdentityApplication mi = ManagedIdentityApplicationBuilder
+    .Create(ManagedIdentityId.WithUserAssignedClientId("<UAMI-client-id>"))
+    .Build();
+
+AuthenticationResult result = await mi
+    .AcquireTokenForManagedIdentity("https://vault.azure.net")
+    .WithMtlsProofOfPossession()
+    .WithAttestationSupport()
+    .ExecuteAsync();
+// result.TokenType == "mtls_pop"; result.BindingCertificate is set
+ +

FIC (confidential client, MSI as the federated credential)

+
Func<AssertionRequestOptions, CancellationToken, Task<ClientSignedAssertion>> assertionDelegate =
+    async (opts, ct) =>
+    {
+        var miResult = await miApp
+            .AcquireTokenForManagedIdentity("api://AzureADTokenExchange")
+            .WithMtlsProofOfPossession()
+            .WithAttestationSupport()
+            .ExecuteAsync(ct);
+
+        return new ClientSignedAssertion
+        {
+            Assertion = miResult.AccessToken,
+            TokenBindingCertificate = miResult.BindingCertificate
+        };
+    };
+
+var cca = ConfidentialClientApplicationBuilder.Create("<app-client-id>")
+    .WithAuthority(new Uri("https://login.microsoftonline.com/<tenant-id>"), validateAuthority: false)
+    .WithAzureRegion("westus3")
+    .WithClientAssertion(assertionDelegate)
+    .Build();
+
+var result = await cca
+    .AcquireTokenForClient(new[] { "https://vault.azure.net/.default" })
+    .WithMtlsProofOfPossession()
+    .ExecuteAsync();
+// When TokenBindingCertificate is supplied, MSAL uses
+// client_assertion_type: urn:ietf:params:oauth:client-assertion-type:jwt-pop
+

Working SN+I integration test β†’

+
+ +
+

SDK Support: Microsoft Identity Web config-driven

+

Opt in per downstream API with "ProtocolScheme": "MTLS_POP". IDownstreamApi handles + acquisition, the binding certificate, and the mTLS call β€” you write no plumbing.

+ +

SN+I (certificate)

+
{
+  "AzureAd": {
+    "Instance": "https://login.microsoftonline.com/",
+    "TenantId": "<tenant-id>",
+    "ClientId": "<app-client-id>",
+    "AzureRegion": "westus3",
+    "ClientCredentials": [
+      {
+        "SourceType": "StoreWithDistinguishedName",
+        "CertificateStorePath": "LocalMachine/My",
+        "CertificateDistinguishedName": "CN=<your-cert>"
+      }
+    ],
+    "SendX5C": true
+  },
+  "MyWebApi": {
+    "BaseUrl": "https://mtlsauth.myapi.microsoft.com",
+    "RelativePath": "api/TodoList",
+    "RequestAppToken": true,
+    "Scopes": [ "api://<target-app-id>/.default" ],
+    "ProtocolScheme": "MTLS_POP"
+  }
+}
+ +

Pure Managed Identity

+
{
+  "AzureKeyVault": {
+    "BaseUrl": "https://contoso.vault.azure.net/",
+    "RelativePath": "secrets/mysecret?api-version=7.4",
+    "Scopes": [ "https://vault.azure.net/.default" ],
+    "RequestAppToken": true,
+    "ProtocolScheme": "MTLS_POP",
+    "AcquireTokenOptions": {
+      "ManagedIdentity": { "UserAssignedClientId": "<UAMI-client-id>" }
+    }
+  }
+}
+ +

FIC (Managed Identity as a federated credential)

+
{
+  "AzureAd": {
+    "Instance": "https://login.microsoftonline.com/",
+    "TenantId": "<tenant-id>",
+    "ClientId": "<app-client-id>",
+    "ClientCapabilities": [ "cp1" ],
+    "ClientCredentials": [
+      {
+        "SourceType": "SignedAssertionFromManagedIdentity",
+        "ManagedIdentityClientId": "<UAMI-client-id>"
+      }
+    ]
+  },
+  "AzureKeyVault": {
+    "BaseUrl": "https://contoso.vault.azure.net/",
+    "Scopes": [ "https://vault.azure.net/.default" ],
+    "RequestAppToken": true,
+    "ProtocolScheme": "MTLS_POP"
+  }
+}
+
ClientCapabilities: [ "cp1" ] advertises support for + Continuous Access Evaluation (CAE).
+ +

Call it (same for every credential source)

+
var api = serviceProvider.GetRequiredService<IDownstreamApi>();
+HttpResponseMessage resp = await api.CallApiForAppAsync("AzureKeyVault");
+
+ +
+

SDK Support: Azure Key Vault SDK (AKV) Beta

+

The Azure Key Vault SDK clients (SecretClient, CertificateClient, KeyClient) get + token binding transparently when you pass a ManagedIdentityCredential (from Azure.Identity): + on a supported host it acquires a bound token and the AKV client performs the call over mutual TLS. Token binding is + on by default; opt out with DisableMtlsProofOfPossession = true.

+
Beta / preview. Pin the exact versions below from the preview feed + (note Azure.Core is an alpha build); they will change as the feature moves toward GA.
+
<ItemGroup>
+  <!-- BETA / preview packages for the token-binding Key Vault scenario -->
+  <PackageReference Include="Azure.Security.KeyVault.Secrets" Version="4.12.0-beta.1" />
+  <PackageReference Include="Azure.Security.KeyVault.Certificates" Version="4.10.0-beta.2" />
+  <PackageReference Include="Azure.Security.KeyVault.Keys" Version="4.11.0-beta.2" />
+  <PackageReference Include="Azure.Core" Version="1.60.0-alpha.20260622.10" />
+  <PackageReference Include="Azure.Identity.Broker" Version="1.8.0-beta.1" />
+</ItemGroup>
+
using Azure.Identity;
+using Azure.Security.KeyVault.Secrets;
+
+string keyVaultUrl = "https://<your-kv>.vault.azure.net/";
+string clientId    = "<user-assigned-mi-client-id>";
+
+var credential = new ManagedIdentityCredential(
+    new ManagedIdentityCredentialOptions(ManagedIdentityId.FromUserAssignedClientId(clientId))
+    {
+        // Token binding (mTLS PoP) is ON by default. To opt out:
+        // DisableMtlsProofOfPossession = true,
+        Diagnostics = { IsLoggingContentEnabled = true }
+    });
+
+var secretClient = new SecretClient(new Uri(keyVaultUrl), credential);
+KeyVaultSecret secret = await secretClient.GetSecretAsync("<secret-name>");
+

The same credential works with any Azure SDK client (for example BlobServiceClient for Storage).

+
+ +
+

Call a downstream API over mTLS (Azure Key Vault)

+

With raw MSAL you pin result.BindingCertificate to the HttpClient, send the token with the + mtls_pop scheme, and include the Key Vault opt-in header.

+
using var handler = new HttpClientHandler
+{
+    ClientCertificateOptions = ClientCertificateOption.Manual,
+    SslProtocols = SslProtocols.Tls12,
+};
+handler.ClientCertificates.Add(result.BindingCertificate);
+
+using var http = new HttpClient(handler);
+http.DefaultRequestHeaders.Authorization =
+    new AuthenticationHeaderValue(result.TokenType, result.AccessToken); // "mtls_pop <token>"
+http.DefaultRequestHeaders.Add("x-ms-tokenboundauth", "true");          // Key Vault opt-in
+
+var akvUrl = "https://<your-kv>.vault.azure.net/secrets/<name>?api-version=7.4";
+using var resp = await http.GetAsync(akvUrl);
+resp.EnsureSuccessStatusCode();
+
Send the token with the mtls_pop scheme and the binding certificate on the + channel β€” never as a plain Bearer token.
+

Supported resources

+ + + + + +
ResourceScopeStatus
Azure Key Vaulthttps://vault.azure.net/.defaultSupported (requires x-ms-tokenboundauth: true)
Microsoft Graphhttps://graph.microsoft.com/.defaultVerify enforcement for your scenario
Your custom web APIapi://<app-id>/.defaultSupported when validated with MISE
+

A resource accepts bound tokens only when it is on the ESTS MtlsTokenBindingEnabledStateResourceAppList + and declares support in its app manifest (rp_tbreq, rp_tbtype).

+
+ +
+

Token acquisition β€” classic MSI vs a bound token, then the scenarios

+

Then vs now: classic Managed Identity is one hop to the local endpoint and hands back a + plain bearer token. A bound token takes several hops (MSAL drives them) so the + token is cryptographically tied to a key that never leaves the machine. The two cards below show each; the runnable + scenarios A–E follow.

+

Scenarios: A–E are token theft & replay outcomes β€” does a stolen token still work from another machine?

+
+ +
+ + Reference β€” the hops in detail +

The pipeline β€” module β†’ endpoint β†’ result

+ + + + + + + + +
#StageModule β†’ endpointResult
1Platform metadataHttpManager β†’ IMDSv2 GET /getPlatformMetadataclient_id, tenant_id, CUID, MAA endpoint
2Key + CSRWinKeyProvider (KeyGuard KSP)RSA-2048 key + CSR CN={client_id}, DC={tenant_id}
3Attest keyPopKeyAttestor β†’ MAA POST /attest/keyguardattestation_token
4Issue credentialHttpManager β†’ IMDSv2 POST /issuecredentialx509 binding cert (~7-day) + regional token URL
5Acquire tokenHttpManager β†’ ESTS-R POST /oauth2/v2.0/token (mTLS)mtls_pop token (xms_tb claim)
6ReturnManagedIdentityApplication β†’ your appAuthenticationResult = token + BindingCertificate
+ +
Attested vs not: KeyGuard (attested) compute gets an mtls_pop token; unattested compute gets a plain bearer. The binding cert lasts ~7 days and MSAL rotates it ~3 days before expiry. MSI v2 /issuecredential design doc β†’
+ +
401 vs 403: a 403 Forbidden from Key Vault is an RBAC result, not a token-binding failure β€” the mtls_pop token is accepted (auth OK) and only the role (e.g. Key Vault Secrets User) is missing. Token-binding/auth failures surface as 401 (e.g. S2S17001).
+
+ +
+ Scenario A β€” bearer token stolen & replayed βœ… Exfiltrated +
+
// output appears here…
+
+
+ Scenario B β€” bound token stolen & replayed ❌ 401 Blocked +
+
// output appears here…
+
+
+ Scenario C β€” fallback to bearer (PoP disabled) βœ… Exfiltrated +
+
// output appears here…
+
+
+ Scenario D β€” vault: Enforce Token Binding ❌ Still fails +
+
// output appears here…
+
+
+ Scenario E β€” vault: Allow Bearer Auth βœ… Exfiltrated +
+
// output appears here…
+
+
+ +
+

Check your understanding

+
+ +
+ +
+

Glossary

+ + + + + + + + + + +
TermMeaning
mTLS PoPMutual-TLS Proof-of-Possession β€” the caller proves it holds the bound private key during the TLS handshake.
cnfJWT "confirmation" claim β€” holds the thumbprint of the key the token is bound to.
SN+ISubject Name + Issuer β€” certificate-based client credentials; the token binds to the cert.
KeyGuard / VBSVirtualization-Based Security enclave that stores the bound key so it can't be exported.
MAAMicrosoft Azure Attestation β€” attests the KeyGuard-protected key for the Managed Identity flow.
FICFederated Identity Credential β€” an app registration uses an SN+I cert or a Managed Identity as its assertion.
mtls_popThe token type / auth scheme for a bound token (vs Bearer).
Trusted Launch / CVMAzure VM security types (Secure Boot + vTPM / confidential) that KeyGuard requires.
+
+ +
+

Resources

+ +

Interactive guide generated from the S2S Token Binding docset Β· simulated console outputs are illustrative.

+
+ +
+
+ + + + + + From 0ca419b283d04a9bb21f43b535214b77f7a07ccf Mon Sep 17 00:00:00 2001 From: Gladwin Johnson <90415114+gladjohn@users.noreply.github.com> Date: Tue, 30 Jun 2026 21:21:59 -0700 Subject: [PATCH 2/2] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- docs/msi_v2/token-binding-demo.html | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/msi_v2/token-binding-demo.html b/docs/msi_v2/token-binding-demo.html index 1eb41e30e3..4b0948bd7c 100644 --- a/docs/msi_v2/token-binding-demo.html +++ b/docs/msi_v2/token-binding-demo.html @@ -195,7 +195,7 @@

πŸͺͺ SN+I (certificate) GA

The caller is an app registration authenticating with a certificate (Subject Name + Issuer). The token is bound to that certificate β€” no VM or KeyGuard required.

Optionally, you can import the certificate and store its private key in KeyGuard (VBS-isolated, non-exportable) for hardware-backed protection of the binding key.

-

SN+I Authentication wiki β†’

+

SN+I Authentication wiki β†’

βš™οΈ Managed Identity Public preview

@@ -307,7 +307,7 @@

FIC (confidential client, MSI as the federated credential)

}; var cca = ConfidentialClientApplicationBuilder.Create("<app-client-id>") - .WithAuthority(new Uri("https://login.microsoftonline.com/<tenant-id>"), validateAuthority: false) + .WithAuthority(new Uri("https://login.microsoftonline.com/<tenant-id>")) .WithAzureRegion("westus3") .WithClientAssertion(assertionDelegate) .Build(); @@ -421,7 +421,7 @@

SDK Support: Azure Key Vault SDK (AKV) BetaCall a downstream API over mTLS (Azure Key Vault)

ClientCertificateOptions = ClientCertificateOption.Manual, SslProtocols = SslProtocols.Tls12, }; + +if (result.BindingCertificate is null) +{ + throw new InvalidOperationException("mTLS PoP binding certificate was not returned. Ensure the token was acquired with WithMtlsProofOfPossession() on a supported host."); +} + handler.ClientCertificates.Add(result.BindingCertificate); using var http = new HttpClient(handler);