Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Feb 7, 2026

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 conventions

  • MSI limitations (no WithClientAssertion() API - cannot perform FIC Leg 2)
  • Vanilla vs FIC two-leg flow definitions
  • All 3 UAMI identifier types with real IDs from PR Add IMDSv2 E2E tests for mTLS PoP with Credential Guard attestation #5726
  • FIC valid combinations matrix (4 scenarios: MSI/ConfApp × Bearer/PoP)
  • Version requirements (MSAL.NET 4.82.1+)
  • Required namespaces (AppConfig, KeyAttestation)

.github/skills/msal-mtls-pop-vanilla/ - Direct token acquisition (single-step)

  • SAMI (System-Assigned Managed Identity)
  • UAMI (User-Assigned) via ClientId/ResourceId/ObjectId
  • Confidential Client with certificate (SNI)
  • Self-contained Quick Start examples with complete inline HTTP call code
  • Null-safe certificate handling with proper checks before adding to ClientCertificates
  • mTLS-specific endpoints: Uses https://mtlstb.graph.microsoft.com for Graph API calls
  • Helper classes: VanillaMsiMtlsPop.cs, MtlsPopTokenAcquirer.cs, ResourceCaller.cs

.github/skills/msal-mtls-pop-fic-two-leg/ - Token exchange pattern (two-step)

  • Leg 1: MSI or Confidential Client → api://AzureADTokenExchange
  • Leg 2: Confidential Client ONLY (MSI lacks required API)
  • Bearer or mTLS PoP final tokens
  • Certificate binding requirement: ALL scenarios must pass TokenBindingCertificate from Leg 1
  • Region specification: All Leg 2 Confidential Client apps include .WithAzureRegion() configuration
  • mTLS-specific endpoints: Uses https://mtlstb.graph.microsoft.com for Graph API calls
  • Helper classes: FicLeg1Acquirer.cs, FicAssertionProvider.cs, FicLeg2Exchanger.cs, ResourceCaller.cs

Key Features

  • YAML frontmatter in all SKILL.md for Copilot integration
  • Credential Guard attestation via .WithAttestationSupport() in all flows
  • Self-contained Quick Start examples for easy Copilot code generation:
    • Complete inline code without external dependencies
    • All required using statements (System.Net.Http, System.Net.Http.Headers)
    • Full HttpClient configuration with mTLS binding certificate
    • Null-safe certificate handling: Proper null checks before adding certificates
    • mTLS-specific endpoints: Uses https://mtlstb.graph.microsoft.com for Graph API calls with mTLS PoP
    • Complete request/response handling with error checking
  • Production C# helper classes following MSAL.NET conventions:
    • ConfigureAwait(false) on awaits
    • CancellationToken support with defaults
    • Proper IDisposable implementation
    • ArgumentNullException.ThrowIfNull() / ObjectDisposedException.ThrowIf() validation
    • Correct mTLS PoP authorization header: Uses "mtls_pop" scheme to match token type
    • Service-to-service Graph API endpoints: Uses /applications instead of /me for app-only scenarios
  • FIC two-leg token exchange:
    • ALL scenarios pass TokenBindingCertificate from Leg 1 in ClientSignedAssertion (both Bearer and PoP)
    • All Leg 2 Confidential Client builders include .WithAzureRegion("westus3") configuration
    • Aligns with integration test patterns in ClientCredentialsMtlsPopTests.cs
  • Troubleshooting guides for common errors

Example Usage

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;

// Build SAMI app
var app = ManagedIdentityApplicationBuilder.Create(
    ManagedIdentityId.SystemAssigned)
    .Build();

// Acquire mTLS PoP token with Credential Guard attestation
var result = await app
    .AcquireTokenForManagedIdentity("https://graph.microsoft.com")
    .WithMtlsProofOfPossession()
    .WithAttestationSupport()  // ← Credential Guard attestation
    .ExecuteAsync();

// Configure HttpClient with the binding certificate for mTLS
var handler = new HttpClientHandler();
if (result.BindingCertificate != null)
{
    handler.ClientCertificates.Add(result.BindingCertificate);
}

using var httpClient = new HttpClient(handler);
httpClient.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("mtls_pop", result.AccessToken);

// Call Microsoft Graph using mTLS-specific endpoint
var response = await httpClient.GetAsync("https://mtlstb.graph.microsoft.com/v1.0/applications");
response.EnsureSuccessStatusCode();

string json = await response.Content.ReadAsStringAsync();
Console.WriteLine(json);

Testing

No code changes to MSAL.NET library. Documentation and helper classes reference existing integration tests in ClientCredentialsMtlsPopTests.cs and use real UAMI IDs from PR #5726 E2E tests. All code examples verified for correctness with:

  • Proper authorization header scheme ("mtls_pop")
  • Appropriate service-to-service endpoints (/applications not /me)
  • mTLS-specific resource endpoints (https://mtlstb.graph.microsoft.com for Graph API)
  • Complete self-contained implementations for Copilot code generation
  • Correct certificate binding in FIC two-leg scenarios (ALL scenarios pass TokenBindingCertificate)
  • Null-safe certificate handling to avoid compiler warnings
  • Proper region specification in all Confidential Client configurations
  • Manually validated by user through E2E testing with all feedback incorporated

Performance impact

None. Documentation-only addition.

Documentation

  • All relevant documentation is updated.

✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

…fidential Client coverage

Co-authored-by: gladjohn <90415114+gladjohn@users.noreply.github.com>
Copilot AI changed the title [WIP] Add GitHub Agent Skills for MSAL.NET mTLS PoP Add GitHub Agent Skills for MSAL.NET mTLS PoP with complete MSI & Confidential Client coverage Feb 7, 2026
Copilot AI requested a review from gladjohn February 7, 2026 23:58
@gladjohn
Copy link
Contributor

gladjohn commented Feb 8, 2026

@copilot In Visual Studio, I asked

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.

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 -

  • You correctly commented Console.WriteLine($"Token Type: {result.TokenType}"); // "mtls_pop" but in the resource call you are passing POP, httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("PoP", result.AccessToken); even that needs to be mtls_pop
  • Since this is a service to service call this will not work .GetAsync("https://graph.microsoft.com/v1.0/me") we need to call .GetAsync("https://graph.microsoft.com/v1.0/applications/")

…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>
Copy link
Contributor Author

Copilot AI commented Feb 8, 2026

@copilot In Visual Studio, I asked

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.
...

Fixed both issues in commit 36953ad:

  1. Authorization header scheme: Changed from "PoP" to "mtls_pop" to match the token type
  2. Graph API endpoint: Changed from /me to /applications for service-to-service scenarios

Changes applied to:

  • ResourceCaller.cs in both vanilla and FIC two-leg skills
  • Code examples in SKILL.md files for both skills

The authorization header now correctly uses new AuthenticationHeaderValue("mtls_pop", result.AccessToken) and the Graph API calls use /applications endpoint which works for service-to-service authentication.

@gladjohn
Copy link
Contributor

gladjohn commented Feb 8, 2026

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>
Copy link
Contributor Author

Copilot AI commented Feb 8, 2026

now with the updated code, I see another issue

...

Updated all Quick Start examples in commit 121bcb5 to use complete inline code instead of the ResourceCaller helper class.

Changes:

  • SAMI example: Now includes full HttpClient configuration with mTLS binding
  • All UAMI examples (ClientId, ResourceId, ObjectId): Complete inline HTTP call code
  • Confidential Client example: Full inline implementation
  • Added required using statements: System.Net.Http and System.Net.Http.Headers
  • Shows complete flow: certificate binding → authorization header → HTTP call → success check → response reading

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.

@gladjohn gladjohn marked this pull request as ready for review February 8, 2026 00:49
@gladjohn gladjohn requested a review from a team as a code owner February 8, 2026 00:49
@gladjohn
Copy link
Contributor

gladjohn commented Feb 8, 2026

@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

.WithClientAssertion((AssertionRequestOptions options, CancellationToken ct) =>
                {
                    assertionProviderCalled = true;
                    tokenEndpointSeenByProvider = options.TokenEndpoint;

                    return Task.FromResult(new ClientSignedAssertion
                    {
                        Assertion = assertionJwt,      // forwarded as client_assertion
                        TokenBindingCertificate = cert // binds assertion for mTLS PoP (jwt-pop)
                    });
                })

see here - https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/blob/main/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/ClientCredentialsMtlsPopTests.cs

…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>
Copy link
Contributor Author

Copilot AI commented Feb 8, 2026

@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, a...

Fixed in commit 42a8fec. Now all FIC two-leg scenarios correctly pass TokenBindingCertificate from Leg 1 in the ClientSignedAssertion, regardless of whether the final token is ****** mTLS PoP.

Changes:

  • Scenario 1 (MSI → ConfApp → Bearer): Added TokenBindingCertificate = leg1Result.BindingCertificate
  • Scenario 3 (ConfApp → ConfApp → Bearer): Added TokenBindingCertificate = leg1Result.BindingCertificate
  • Updated Key Points to clarify certificate must always be passed
  • Added comments clarifying that absence of .WithMtlsProofOfPossession() results in ****** token

Aligns with the test code in ClientCredentialsMtlsPopTests.cs line 130 where TokenBindingCertificate = cert is always set in the ClientSignedAssertion.

@gladjohn
Copy link
Contributor

gladjohn commented Feb 9, 2026

@copilot my manual validation is now complete, you did an amazing job with this PR

  1. we have a comment that says "// ← Credential Guard attestation for production" let's update it to "// ← Credential Guard attestation"

  2. For the resource call we do, handler.ClientCertificates.Add(result.BindingCertificate);

And there is a warning - Possible null reference argument for parameter 'value' in 'int X509CertificateCollection.Add(X509Certificate value)'.

  1. rememeber for confidential client we always need to provide region, the sample now does not. we need to add a .WithAzureRegion("westus3") //or region of your azure resource

// ======== Leg 2: Confidential Client exchanges Leg 1 token for Bearer to Graph ========
var leg2App = ConfidentialClientApplicationBuilder
.Create("your-leg2-client-id")
.WithAuthority("https://login.microsoftonline.com/your-tenant-id")
.WithClientAssertion((options, ct) =>
{
return Task.FromResult(new ClientSignedAssertion
{
Assertion = leg1Result.AccessToken, // Leg 1's token as assertion
TokenBindingCertificate = leg1Result.BindingCertificate // Always pass Leg 1's cert
});
})
.Build();

var leg2Result = await leg2App
.AcquireTokenForClient(new[] { "https://graph.microsoft.com/.default" })
.ExecuteAsync(); // No .WithMtlsProofOfPossession() → Bearer token

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>
Copy link
Contributor Author

Copilot AI commented Feb 9, 2026

@copilot my manual validation is now complete, you did an amazing job with this PR

  1. we have a comment that says "// ← Credential Guard attestation for production" let's update it to "// ← Cr...

All three changes completed in commit 6d05bae:

  1. ✅ Updated comment to "Credential Guard attestation" (removed "for production")
  2. ✅ Added null checks: if (result.BindingCertificate != null) before all handler.ClientCertificates.Add() calls in vanilla examples
  3. ✅ Added .WithAzureRegion("westus3") to all four Leg 2 Confidential Client builders in FIC two-leg scenarios

The examples now handle the nullable BindingCertificate properly and all Confidential Client apps specify a region as required.

@gladjohn
Copy link
Contributor

gladjohn commented Feb 9, 2026

while doing an e2e test, I also found that the mTLS resources have a specific endpoints so graph endpoints needs to be updated to "https://mtlstb.graph.microsoft.com/v1.0/applications" @copilot

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>
Copy link
Contributor Author

Copilot AI commented Feb 9, 2026

while doing an e2e test, I also found that the mTLS resources have a specific endpoints so graph endpoints needs to be updated to "https://mtlstb.graph.microsoft.com/v1.0/applications" @copilot

Updated in commit f5d4056. All Graph API HTTP call endpoints now use the mTLS-specific endpoint https://mtlstb.graph.microsoft.com instead of https://graph.microsoft.com.

Changes:

  • SAMI example: Updated to https://mtlstb.graph.microsoft.com/v1.0/applications
  • Confidential Client example: Updated to https://mtlstb.graph.microsoft.com/v1.0/applications
  • Usage Pattern example: Updated to https://mtlstb.graph.microsoft.com/v1.0/applications
  • FIC two-leg usage pattern: Updated to https://mtlstb.graph.microsoft.com/v1.0/applications

Note: Token acquisition still uses https://graph.microsoft.com as the resource/audience identifier, but actual API calls use the mTLS-specific endpoint as required.

Added detailed instructions for GitHub Copilot Agent Skills and mTLS PoP guidance.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants