-
Notifications
You must be signed in to change notification settings - Fork 394
Add GitHub Agent Skills for MSAL.NET mTLS PoP with complete MSI & Confidential Client coverage #5733
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
…fidential Client coverage Co-authored-by: gladjohn <90415114+gladjohn@users.noreply.github.com>
|
@copilot In Visual Studio, I asked
And I got the following code, using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.AppConfig;
using Microsoft.Identity.Client.KeyAttestation;
// 1. Build the SAMI application
var app = ManagedIdentityApplicationBuilder.Create(
ManagedIdentityId.SystemAssigned)
.Build();
// 2. Acquire mTLS PoP token with Credential Guard attestation
var result = await app
.AcquireTokenForManagedIdentity("https://graph.microsoft.com")
.WithMtlsProofOfPossession()
.WithAttestationSupport()
.ExecuteAsync()
.ConfigureAwait(false);
Console.WriteLine($"Token Type: {result.TokenType}"); // "mtls_pop"
Console.WriteLine($"Certificate: {result.BindingCertificate?.Thumbprint}");
// 3. Call Microsoft Graph with the PoP token over mTLS
var handler = new HttpClientHandler();
handler.ClientCertificates.Add(result.BindingCertificate);
using var httpClient = new HttpClient(handler);
httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("PoP", result.AccessToken);
var response = await httpClient
.GetAsync("https://graph.microsoft.com/v1.0/me")
.ConfigureAwait(false);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());This is great, but two changes are needed here -
|
…ervice-to-service calls - Change Authorization header from "PoP" to "mtls_pop" to match token type - Update Graph API endpoint from /me to /applications for service-to-service scenarios - Apply fixes to both vanilla and FIC two-leg ResourceCaller classes and SKILL.md examples Co-authored-by: gladjohn <90415114+gladjohn@users.noreply.github.com>
Fixed both issues in commit 36953ad:
Changes applied to:
The authorization header now correctly uses |
|
now with the updated code, I see another issue using System;
using System.Threading.Tasks;
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.AppConfig;
using Microsoft.Identity.Client.KeyAttestation;
// 1. Build the SAMI app
var app = ManagedIdentityApplicationBuilder.Create(
ManagedIdentityId.SystemAssigned)
.Build();
// 2. Acquire mTLS PoP token for Graph with Credential Guard attestation
var result = await app
.AcquireTokenForManagedIdentity("https://graph.microsoft.com")
.WithMtlsProofOfPossession()
.WithAttestationSupport()
.ExecuteAsync();
// 3. Verify the token
Console.WriteLine($"Token Type: {result.TokenType}"); // "mtls_pop"
Console.WriteLine($"Thumbprint: {result.BindingCertificate?.Thumbprint}");
if (result.TokenType != "mtls_pop" || result.BindingCertificate == null)
throw new InvalidOperationException("Expected mTLS PoP token with binding certificate");
// 4. Call Microsoft Graph with mTLS binding
using var caller = new ResourceCaller(result);
string response = await caller.CallResourceAsync(
"https://graph.microsoft.com/v1.0/applications");The type or namespace name 'ResourceCaller' could not be found (are you missing a using directive or an assembly reference?) I would rather have the code like this // After acquiring the mTLS PoP token...
var result = await app
.AcquireTokenForManagedIdentity("https://graph.microsoft.com")
.WithMtlsProofOfPossession()
.WithAttestationSupport()
.ExecuteAsync();
// Configure HttpClient with the binding certificate for mTLS
var handler = new HttpClientHandler();
handler.ClientCertificates.Add(result.BindingCertificate);
using var httpClient = new HttpClient(handler);
httpClient.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("mtls_pop", result.AccessToken);
// Call Microsoft Graph
var response = await httpClient.GetAsync("https://graph.microsoft.com/v1.0/applications");
response.EnsureSuccessStatusCode();
string json = await response.Content.ReadAsStringAsync();
Console.WriteLine(json);All in one place than some helper methods. The helper methods can stay as is, but my prompt was simply Using the msal-mtls-pop-vanilla skill, show me how to acquire an mTLS PoP token with system-assigned managed identity (SAMI) for Microsoft Graph. What do you think @copilot |
…all code - Add full self-contained examples for SAMI, all UAMI types, and Confidential Client - Include System.Net.Http and System.Net.Http.Headers using statements - Show complete HttpClient configuration with mTLS binding certificate - Add EnsureSuccessStatusCode() and ReadAsStringAsync() for complete flow - Remove dependency on ResourceCaller helper class in Quick Start examples - Makes examples easier to understand and use without external dependencies Co-authored-by: gladjohn <90415114+gladjohn@users.noreply.github.com>
... Updated all Quick Start examples in commit 121bcb5 to use complete inline code instead of the ResourceCaller helper class. Changes:
When Copilot generates code from simple prompts like "show me how to acquire an mTLS PoP token", it will now provide self-contained examples without external dependencies. The helper classes remain available for production use but are no longer required in the Quick Start examples. |
|
@copilot another issue while testing. prompt - Using the msal-mtls-pop-fic-two-leg skill, show me a two-leg token exchange where Leg 1 uses MSI to get a PoP token for api://AzureADTokenExchange, and Leg 2 uses a confidential client app to exchange that token for a Bearer token to Microsoft Graph. code sample using System;
using System.Threading.Tasks;
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.AppConfig;
using Microsoft.Identity.Client.KeyAttestation;
using Microsoft.Identity.Client.Extensibility;
// ═══════════════════════════════════════════════════════════════
// LEG 1: MSI acquires mTLS PoP token for api://AzureADTokenExchange
// ═══════════════════════════════════════════════════════════════
var msiApp = ManagedIdentityApplicationBuilder.Create(
ManagedIdentityId.WithUserAssignedClientId("your-uami-client-id"))
.Build();
var leg1Result = await msiApp
.AcquireTokenForManagedIdentity("api://AzureADTokenExchange")
.WithMtlsProofOfPossession()
.WithAttestationSupport() // Credential Guard attestation
.ExecuteAsync();
Console.WriteLine($"Leg 1 Token Type: {leg1Result.TokenType}"); // "mtls_pop"
Console.WriteLine($"Leg 1 Binding Cert: {leg1Result.BindingCertificate?.Thumbprint}");
// ═══════════════════════════════════════════════════════════════
// LEG 2: Confidential Client exchanges Leg 1 token for Bearer
// token to Microsoft Graph
// ═══════════════════════════════════════════════════════════════
var confApp = ConfidentialClientApplicationBuilder
.Create("your-leg2-client-id")
.WithAuthority("https://login.microsoftonline.com/your-tenant-id")
.WithClientAssertion((options, ct) =>
{
// Pass Leg 1's access token as the assertion.
// No TokenBindingCertificate → result will be a Bearer token.
return Task.FromResult(new ClientSignedAssertion
{
Assertion = leg1Result.AccessToken
});
})
.Build();
var leg2Result = await confApp
.AcquireTokenForClient(new[] { "https://graph.microsoft.com/.default" })
.ExecuteAsync(); // No .WithMtlsProofOfPossession() → Bearer
Console.WriteLine($"Leg 2 Token Type: {leg2Result.TokenType}"); // "Bearer"
// ═══════════════════════════════════════════════════════════════
// Call Microsoft Graph with the Bearer token
// ═══════════════════════════════════════════════════════════════
using var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", leg2Result.AccessToken);
var response = await httpClient.GetAsync("https://graph.microsoft.com/v1.0/applications");
Console.WriteLine(await response.Content.ReadAsStringAsync());We also need to pass the cert because the first leg mtls pop will always return a cert |
…Leg 1 - Add TokenBindingCertificate to Scenario 1 (MSI → Confidential Client → Bearer) - Add TokenBindingCertificate to Scenario 3 (ConfApp → ConfApp → Bearer) - Update Key Points to clarify certificate must be passed for ALL scenarios - Add comments clarifying no .WithMtlsProofOfPossession() means Bearer token - Update guidance skill with same requirement - Aligns with test code in ClientCredentialsMtlsPopTests.cs line 130 Co-authored-by: gladjohn <90415114+gladjohn@users.noreply.github.com>
Fixed in commit 42a8fec. Now all FIC two-leg scenarios correctly pass Changes:
Aligns with the test code in |
|
@copilot my manual validation is now complete, you did an amazing job with this PR
And there is a warning - Possible null reference argument for parameter 'value' in 'int X509CertificateCollection.Add(X509Certificate value)'.
// ======== Leg 2: Confidential Client exchanges Leg 1 token for Bearer to Graph ======== var leg2Result = await leg2App I tested evewrything else and all looks good. can you just make these three changes please |
…xamples
1. Update comment from "Credential Guard support" to "Credential Guard attestation"
2. Add null checks before handler.ClientCertificates.Add(result.BindingCertificate) in all vanilla examples
3. Add .WithAzureRegion("westus3") to all Leg 2 Confidential Client builders in FIC two-leg scenarios
Addresses user feedback from manual validation
Co-authored-by: gladjohn <90415114+gladjohn@users.noreply.github.com>
All three changes completed in commit 6d05bae:
The examples now handle the nullable BindingCertificate properly and all Confidential Client apps specify a region as required. |
|
while doing an e2e test, I also found that the mTLS resources have a specific endpoints so graph endpoints needs to be updated to |
Change all HTTP call endpoints from https://graph.microsoft.com to https://mtlstb.graph.microsoft.com for mTLS PoP scenarios. The token acquisition still uses https://graph.microsoft.com as the resource/audience, but actual API calls must use the mTLS-specific endpoint. Changes: - Updated 3 occurrences in msal-mtls-pop-vanilla/SKILL.md - Updated 1 occurrence in msal-mtls-pop-fic-two-leg/SKILL.md Addresses user E2E test findings Co-authored-by: gladjohn <90415114+gladjohn@users.noreply.github.com>
Updated in commit f5d4056. All Graph API HTTP call endpoints now use the mTLS-specific endpoint Changes:
Note: Token acquisition still uses |
Added detailed instructions for GitHub Copilot Agent Skills and mTLS PoP guidance.
Changes proposed in this request
Adds three GitHub Copilot Agent Skills documenting mTLS Proof-of-Possession flows with production-grade helper classes. Addresses critical gaps in MSI vs Confidential Client capabilities and provides accurate guidance for both vanilla (direct) and FIC two-leg token acquisition patterns.
Skills Added
.github/skills/msal-mtls-pop-guidance/- Shared terminology and conventionsWithClientAssertion()API - cannot perform FIC Leg 2)AppConfig,KeyAttestation).github/skills/msal-mtls-pop-vanilla/- Direct token acquisition (single-step)https://mtlstb.graph.microsoft.comfor Graph API callsVanillaMsiMtlsPop.cs,MtlsPopTokenAcquirer.cs,ResourceCaller.cs.github/skills/msal-mtls-pop-fic-two-leg/- Token exchange pattern (two-step)api://AzureADTokenExchangeTokenBindingCertificatefrom Leg 1.WithAzureRegion()configurationhttps://mtlstb.graph.microsoft.comfor Graph API callsFicLeg1Acquirer.cs,FicAssertionProvider.cs,FicLeg2Exchanger.cs,ResourceCaller.csKey Features
.WithAttestationSupport()in all flowsSystem.Net.Http,System.Net.Http.Headers)https://mtlstb.graph.microsoft.comfor Graph API calls with mTLS PoPConfigureAwait(false)on awaitsCancellationTokensupport with defaultsIDisposableimplementationArgumentNullException.ThrowIfNull()/ObjectDisposedException.ThrowIf()validation"mtls_pop"scheme to match token type/applicationsinstead of/mefor app-only scenariosTokenBindingCertificatefrom Leg 1 inClientSignedAssertion(both Bearer and PoP).WithAzureRegion("westus3")configurationClientCredentialsMtlsPopTests.csExample Usage
Testing
No code changes to MSAL.NET library. Documentation and helper classes reference existing integration tests in
ClientCredentialsMtlsPopTests.csand use real UAMI IDs from PR #5726 E2E tests. All code examples verified for correctness with:"mtls_pop")/applicationsnot/me)https://mtlstb.graph.microsoft.comfor Graph API)TokenBindingCertificate)Performance impact
None. Documentation-only addition.
Documentation
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.