Skip to content

Improve GitHub Agent Skills discoverability with comprehensive catalog README#5733

Merged
gladjohn merged 12 commits intomainfrom
copilot/add-github-agent-skills-mtls
Feb 27, 2026
Merged

Improve GitHub Agent Skills discoverability with comprehensive catalog README#5733
gladjohn merged 12 commits intomainfrom
copilot/add-github-agent-skills-mtls

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Feb 7, 2026

Changes proposed in this request

GitHub Agent Skills in .github/skills/ were split across two READMEs with no unified discovery point. Combined them into a two-level structure:

Top-level catalog (.github/skills/README.md):

  • Lists all 5 skills: 3 Confidential Client flows + 2 mTLS PoP skills
  • Quick Start tables mapping scenarios → skills and credential types → setup guides
  • Agent Capabilities section (7 types: code generation, setup, error resolution, best practices, explanation, decision help, validation)
  • Usage examples for Copilot Chat natural language queries
  • DRY architecture documentation showing mTLS PoP skills reference msal-confidential-auth/shared/ patterns

Skill set README (msal-confidential-auth/README.md):

  • Retained for detailed skill set-specific documentation
  • Linked from catalog via "📖 Full documentation"

Skill structure:

.github/skills/
├── README.md                      # Catalog: all skills + Quick Start tables
├── msal-confidential-auth/
│   ├── README.md                  # Skill set details
│   ├── auth-code-flow/SKILL.md
│   ├── obo-flow/SKILL.md
│   ├── client-credentials/SKILL.md
│   └── shared/                    # Referenced by all skills (DRY)
│       ├── credential-setup/
│       └── patterns/
├── msal-mtls-pop-guidance/SKILL.md
├── msal-mtls-pop-vanilla/SKILL.md
└── msal-mtls-pop-fic-two-leg/SKILL.md

Previous commits (refactored mTLS PoP skills to reference shared patterns, fixed mTLS endpoints/headers):

  • Authorization header: "mtls_pop" (not "PoP")
  • Graph API endpoint: https://mtlstb.graph.microsoft.com (not https://graph.microsoft.com)
  • Service-to-service: /applications (not /me)
  • FIC two-leg: TokenBindingCertificate always passed from Leg 1
  • Null checks before handler.ClientCertificates.Add()
  • .WithAzureRegion() on all Leg 2 Confidential Client builders

Testing

Documentation-only. All skills listed in catalog verified:

  • Confidential Client Authentication (auth-code-flow, obo-flow, client-credentials)
  • mTLS PoP (guidance, vanilla, fic-two-leg)
  • All 13 shared resources linked correctly
  • Cross-references validated between skills

Performance impact

None. Documentation structure only.

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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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>
This was referenced Mar 9, 2026
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.

4 participants