diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 18c0d9eee9..40c057b3de 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1 +1,335 @@ -Carefully review all markdown documents in the ../.clinerules folder. Those are your custom instructions. \ No newline at end of file +Carefully review all markdown documents in the ../.clinerules folder. Those are your custom instructions. + +--- + +# GitHub Copilot Agent Skills (Repository Skills) + +This repository defines **Copilot Agent Skills** under `.github/skills/`. + +## How skills work +- A **skill** is defined by a folder: `.github/skills//` +- Each skill must contain a file named **`SKILL.md`** +- `SKILL.md` must start with YAML frontmatter that includes at least: + - `name` + - `description` + +> Note: Copilot does **not** read `copilot-instructions.md` files inside subfolders. +> Only `.github/copilot-instructions.md` is treated as the repo-wide instructions file. +> Skill content must be inside `.github/skills/**/SKILL.md`. + +## How to use skills in Copilot Chat +- Prefer invoking a relevant skill explicitly when available: + - `@ ...your question...` +- If unsure which skill applies, ask: + - “What skills are available in this repo?” + - “Which skill should I use for this task?” + +## Expectations when using skills +- Follow the skill’s guidance and patterns exactly (APIs, naming, examples). +- If code is requested, provide complete, runnable code with required imports. +- If multiple approaches exist, explain the tradeoffs and recommend one. + +--- + +# Copilot Instructions for MSAL.NET mTLS Proof-of-Possession + +## 🚀 Quick Start: Discover Available Skills + +**Ask these questions in VS Code Copilot Chat to discover and explore all available skills:** + +What can you tell me about mTLS PoP in MSAL.NET? + +Code + +Copilot will automatically reference and describe: +- `@msal-mtls-pop-guidance` - Foundational concepts +- `@msal-mtls-pop-vanilla` - Direct token acquisition +- `@msal-mtls-pop-fic-two-leg` - Token exchange patterns + +--- + +## 📚 Available Skills Overview + +This repository contains **three GitHub Agent Skills** for mTLS Proof-of-Possession (PoP) authentication: + +| Skill | Purpose | Best For | +|-------|---------|----------| +| **@msal-mtls-pop-guidance** | Foundational concepts, terminology, decision frameworks | Learning the fundamentals, comparing approaches | +| **@msal-mtls-pop-vanilla** | Direct single-step token acquisition with complete code | Quick implementation with MSI or Confidential Client | +| **@msal-mtls-pop-fic-two-leg** | Two-step token exchange patterns | Complex scenarios requiring token exchange | + +--- + +## 🔍 Discovery Prompts: Explore Each Skill + +### **Discover Skill 1: Guidance & Concepts** +@msal-mtls-pop-guidance What is mTLS PoP and why do I need it? @msal-mtls-pop-guidance What are the main concepts I need to understand? @msal-mtls-pop-guidance Explain vanilla vs FIC two-leg flows @msal-mtls-pop-guidance What are the MSI limitations? @msal-mtls-pop-guidance Which approach should I use for my scenario? @msal-mtls-pop-guidance What version requirements exist? + +Code + +### **Discover Skill 2: Vanilla (Direct) Token Acquisition** +@msal-mtls-pop-vanilla What code examples do you have for mTLS PoP? @msal-mtls-pop-vanilla Show me System-Assigned Managed Identity (SAMI) example @msal-mtls-pop-vanilla Show me User-Assigned Managed Identity (UAMI) example @msal-mtls-pop-vanilla Show me Confidential Client with certificate example @msal-mtls-pop-vanilla How do I configure the HttpClient for mTLS? @msal-mtls-pop-vanilla What helper classes are available? @msal-mtls-pop-vanilla How do I handle certificate binding safely? + +Code + +### **Discover Skill 3: FIC Two-Leg Token Exchange** +@msal-mtls-pop-fic-two-leg What is the two-leg token exchange pattern? @msal-mtls-pop-fic-two-leg Show me a complete end-to-end example @msal-mtls-pop-fic-two-leg How does certificate binding work between legs? @msal-mtls-pop-fic-two-leg What's the difference between Leg 1 and Leg 2? @msal-mtls-pop-fic-two-leg What helper classes are available? @msal-mtls-pop-fic-two-leg Why is MSI limited to Leg 1 only? + +Code + +--- + +## 🎯 Comprehensive Question Bank + +Use these questions to explore the full depth of available skills: + +### **Foundation & Architecture Questions** + +@msal-mtls-pop-guidance What is mTLS Proof-of-Possession (PoP)? @msal-mtls-pop-guidance Why would I use mTLS PoP instead of bearer tokens? @msal-mtls-pop-guidance What are the security benefits of mTLS PoP? @msal-mtls-pop-guidance What MSAL.NET version do I need? @msal-mtls-pop-guidance What namespaces do I need to import? @msal-mtls-pop-guidance What are the three UAMI identifier types? @msal-mtls-pop-guidance Explain the difference between SAMI and UAMI @msal-mtls-pop-guidance What's the api://AzureADTokenExchange resource? + +Code + +### **Vanilla Flow - Quick Implementation** + +@msal-mtls-pop-vanilla How do I get started with mTLS PoP in 5 minutes? @msal-mtls-pop-vanilla Show me the simplest working example @msal-mtls-pop-vanilla What's the bare minimum code I need? @msal-mtls-pop-vanilla How do I test my implementation? + +Code + +### **Vanilla Flow - System-Assigned Managed Identity (SAMI)** + +@msal-mtls-pop-vanilla What is SAMI and when should I use it? @msal-mtls-pop-vanilla Show me how to create a ManagedIdentityApplicationBuilder for SAMI @msal-mtls-pop-vanilla What's the complete code for SAMI mTLS PoP? @msal-mtls-pop-vanilla How do I acquire a token for System-Assigned Managed Identity? @msal-mtls-pop-vanilla Show me the SAMI example with Credential Guard attestation @msal-mtls-pop-vanilla How do I use the binding certificate from SAMI token? @msal-mtls-pop-vanilla What errors might I encounter with SAMI and how do I fix them? + +Code + +### **Vanilla Flow - User-Assigned Managed Identity (UAMI)** + +@msal-mtls-pop-vanilla What is UAMI and when should I use it? @msal-mtls-pop-vanilla Show me the three ways to identify a UAMI @msal-mtls-pop-vanilla How do I use UAMI by ClientId? @msal-mtls-pop-vanilla How do I use UAMI by ResourceId? @msal-mtls-pop-vanilla How do I use UAMI by ObjectId? @msal-mtls-pop-vanilla What's the complete code for UAMI mTLS PoP? @msal-mtls-pop-vanilla Show me how to handle different UAMI identifier types @msal-mtls-pop-vanilla How do I know which UAMI identifier to use? + +Code + +### **Vanilla Flow - Confidential Client** + +@msal-mtls-pop-vanilla What is Confidential Client and when should I use it? @msal-mtls-pop-vanilla How do I configure a Confidential Client with certificate (SNI)? @msal-mtls-pop-vanilla Show me the complete Confidential Client mTLS PoP example @msal-mtls-pop-vanilla How do I load a certificate for mTLS PoP? @msal-mtls-pop-vanilla What's the difference between SAMI, UAMI, and Confidential Client? @msal-mtls-pop-vanilla When should I use Confidential Client instead of MSI? + +Code + +### **Certificate & HTTP Configuration** + +@msal-mtls-pop-vanilla How do I get the binding certificate from the token result? @msal-mtls-pop-vanilla What is a binding certificate and why do I need it? @msal-mtls-pop-vanilla How do I add the certificate to HttpClientHandler? @msal-mtls-pop-vanilla What are null-safe certificate handling best practices? @msal-mtls-pop-vanilla How do I avoid compiler warnings with certificate binding? @msal-mtls-pop-vanilla Show me the complete HttpClient setup for mTLS PoP @msal-mtls-pop-vanilla What's the correct pattern for checking if certificate is null? @msal-mtls-pop-vanilla How do I dispose of HttpClient properly? + +Code + +### **Authorization & Endpoints** + +@msal-mtls-pop-vanilla What's the correct Authorization header for mTLS PoP? @msal-mtls-pop-vanilla Why is the "mtls_pop" scheme important? @msal-mtls-pop-vanilla What's the mTLS-specific endpoint for Microsoft Graph? @msal-mtls-pop-vanilla Why use https://mtlstb.graph.microsoft.com instead of regular endpoint? @msal-mtls-pop-vanilla Should I use /applications or /me for service-to-service calls? @msal-mtls-pop-vanilla How do I call Microsoft Graph with mTLS PoP tokens? @msal-mtls-pop-vanilla What other endpoints support mTLS PoP? @msal-mtls-pop-vanilla How do I verify my endpoint is correct? + +Code + +### **Production Patterns & Best Practices** + +@msal-mtls-pop-vanilla What production-grade patterns should I follow? @msal-mtls-pop-vanilla Why should I use ConfigureAwait(false)? @msal-mtls-pop-vanilla How do I add CancellationToken support? @msal-mtls-pop-vanilla How do I implement IDisposable correctly? @msal-mtls-pop-vanilla What validation should I do with ArgumentNullException? @msal-mtls-pop-vanilla Show me the complete production helper class pattern @msal-mtls-pop-vanilla How do I add proper error handling? @msal-mtls-pop-vanilla What logging should I add for debugging? + +Code + +### **Credential Guard & Attestation** + +@msal-mtls-pop-vanilla What is Credential Guard attestation? @msal-mtls-pop-vanilla How do I enable .WithAttestationSupport()? @msal-mtls-pop-vanilla Why should I use attestation support? @msal-mtls-pop-vanilla What's the security benefit of attestation? + +Code + +### **FIC Two-Leg Flow - Concepts** + +@msal-mtls-pop-fic-two-leg What is FIC (Federated Identity Credentials)? @msal-mtls-pop-fic-two-leg What is a two-leg token exchange pattern? @msal-mtls-pop-fic-two-leg When should I use FIC two-leg instead of vanilla? @msal-mtls-pop-fic-two-leg What are the four valid FIC scenario combinations? @msal-mtls-pop-fic-two-leg Show me the FIC matrix (MSI/ConfApp × Bearer/PoP) @msal-mtls-pop-fic-two-leg What's the difference between vanilla and FIC flows? @msal-mtls-pop-fic-two-leg Why is FIC two-leg more complex? + +Code + +### **FIC Two-Leg Flow - Leg 1 (Acquisition)** + +@msal-mtls-pop-fic-two-leg What happens in Leg 1? @msal-mtls-pop-fic-two-leg How do I acquire a Leg 1 token? @msal-mtls-pop-fic-two-leg Can I use MSI for Leg 1? @msal-mtls-pop-fic-two-leg Can I use Confidential Client for Leg 1? @msal-mtls-pop-fic-two-leg What resource should I request in Leg 1? @msal-mtls-pop-fic-two-leg Show me complete MSI Leg 1 code @msal-mtls-pop-fic-two-leg Show me complete Confidential Client Leg 1 code @msal-mtls-pop-fic-two-leg What's the api://AzureADTokenExchange resource? @msal-mtls-pop-fic-two-leg How do I extract the binding certificate from Leg 1 result? + +Code + +### **FIC Two-Leg Flow - Certificate Binding** + +@msal-mtls-pop-fic-two-leg What is certificate binding between legs? @msal-mtls-pop-fic-two-leg Why must I pass TokenBindingCertificate to Leg 2? @msal-mtls-pop-fic-two-leg How do I include the certificate in Leg 2? @msal-mtls-pop-fic-two-leg What happens if I forget the certificate binding? @msal-mtls-pop-fic-two-leg How do I extract and pass the certificate safely? @msal-mtls-pop-fic-two-leg Is certificate binding required in all scenarios? @msal-mtls-pop-fic-two-leg Show me the complete certificate binding pattern + +Code + +### **FIC Two-Leg Flow - Leg 2 (Exchange)** + +@msal-mtls-pop-fic-two-leg What happens in Leg 2? @msal-mtls-pop-fic-two-leg Why can only Confidential Client do Leg 2? @msal-mtls-pop-fic-two-leg Why can't MSI perform Leg 2? @msal-mtls-pop-fic-two-leg How do I acquire a Leg 2 token? @msal-mtls-pop-fic-two-leg What does .WithAzureRegion() do? @msal-mtls-pop-fic-two-leg Why is region specification important in Leg 2? @msal-mtls-pop-fic-two-leg Show me complete Leg 2 Confidential Client code @msal-mtls-pop-fic-two-leg How do I use ClientSignedAssertion in Leg 2? @msal-mtls-pop-fic-two-leg Can I use bearer tokens in Leg 2? @msal-mtls-pop-fic-two-leg Can I use mTLS PoP tokens in Leg 2? + +Code + +### **FIC Two-Leg Flow - Complete Scenarios** + +@msal-mtls-pop-fic-two-leg Show me MSI Leg 1 → ConfApp Leg 2 with Bearer token @msal-mtls-pop-fic-two-leg Show me MSI Leg 1 → ConfApp Leg 2 with mTLS PoP token @msal-mtls-pop-fic-two-leg Show me ConfApp Leg 1 → ConfApp Leg 2 with Bearer token @msal-mtls-pop-fic-two-leg Show me ConfApp Leg 1 → ConfApp Leg 2 with mTLS PoP token @msal-mtls-pop-fic-two-leg Show me the complete end-to-end FIC flow @msal-mtls-pop-fic-two-leg How do I integrate Leg 1 and Leg 2 together? @msal-mtls-pop-fic-two-leg What's the complete flow from start to API call? + +Code + +### **FIC Two-Leg Flow - Helper Classes** + +@msal-mtls-pop-fic-two-leg What helper classes are available? @msal-mtls-pop-fic-two-leg Show me FicLeg1Acquirer usage @msal-mtls-pop-fic-two-leg Show me FicAssertionProvider usage @msal-mtls-pop-fic-two-leg Show me FicLeg2Exchanger usage @msal-mtls-pop-fic-two-leg Show me ResourceCaller usage @msal-mtls-pop-fic-two-leg How do these helper classes work together? @msal-mtls-pop-fic-two-leg Can I use these classes as-is or do I need to modify them? + +Code + +### **Error Handling & Troubleshooting** + +@msal-mtls-pop-vanilla What errors might I encounter? @msal-mtls-pop-vanilla How do I debug certificate binding issues? @msal-mtls-pop-vanilla What does "certificate not found" error mean? @msal-mtls-pop-vanilla How do I verify my token is actually a PoP token? @msal-mtls-pop-vanilla What should I check if my API call fails with mTLS PoP? @msal-mtls-pop-fic-two-leg What are common FIC two-leg errors? @msal-mtls-pop-fic-two-leg What does "certificate binding mismatch" mean? @msal-mtls-pop-fic-two-leg How do I troubleshoot token exchange failures? + +Code + +### **Testing & Validation** + +@msal-mtls-pop-vanilla How do I test my mTLS PoP implementation? @msal-mtls-pop-vanilla Where are the integration tests? @msal-mtls-pop-vanilla Can I run the tests locally? @msal-mtls-pop-vanilla How do I verify my certificate binding is working? @msal-mtls-pop-vanilla What test scenarios should I cover? @msal-mtls-pop-fic-two-leg How do I test FIC two-leg flows? @msal-mtls-pop-fic-two-leg Are there E2E test examples? + +Code + +--- + +## 📖 Complete Reference Guide + +### Key Concepts + +**mTLS Proof-of-Possession (PoP)** +- Token bound to a specific client certificate +- More secure than bearer tokens +- Requires certificate in HTTP request +- Cannot be replayed without the certificate + +**Vanilla Flow** +- Single-step direct token acquisition +- MSI (SAMI/UAMI) or Confidential Client +- Fastest path to mTLS PoP tokens +- Recommended for most use cases + +**FIC Two-Leg Flow** +- First leg: Get token for `api://AzureADTokenExchange` +- Second leg: Exchange for actual resource access +- MSI can do Leg 1 only +- Confidential Client required for Leg 2 +- Certificate binding between legs is critical + +**Version Requirements** +- MSAL.NET 4.82.1+ +- Namespaces: `Microsoft.Identity.Client.AppConfig`, `Microsoft.Identity.Client.KeyAttestation` + +### Capability Comparison + +| Feature | SAMI | UAMI | ConfApp | +|---------|------|------|---------| +| Vanilla mTLS PoP | ✅ | ✅ | ✅ | +| FIC Leg 1 | ✅ | ✅ | ✅ | +| FIC Leg 2 | ❌ | ❌ | ✅ | +| Custom Certificate | ❌ | ❌ | ✅ | +| Region Specification | ❌ | ❌ | ✅ | + +### Endpoints + +- **mTLS Graph**: `https://mtlstb.graph.microsoft.com` +- **Token Exchange**: `api://AzureADTokenExchange` +- **Token Scheme**: `mtls_pop` (authorization header) + +--- + +## 🎓 Learning Paths + +### **Path 1: New to mTLS PoP (30 minutes)** +1. `@msal-mtls-pop-guidance What is mTLS PoP?` +2. `@msal-mtls-pop-guidance Explain vanilla vs FIC flows` +3. `@msal-mtls-pop-vanilla How do I get started in 5 minutes?` +4. `@msal-mtls-pop-vanilla Show me SAMI example` +5. Implement SAMI example locally + +### **Path 2: UAMI Implementation (20 minutes)** +1. `@msal-mtls-pop-guidance What are the three UAMI identifier types?` +2. `@msal-mtls-pop-vanilla Show me UAMI by ClientId example` +3. `@msal-mtls-pop-vanilla Show me UAMI by ResourceId example` +4. `@msal-mtls-pop-vanilla Show me UAMI by ObjectId example` +5. Choose and implement one identifier type + +### **Path 3: Confidential Client Setup (25 minutes)** +1. `@msal-mtls-pop-vanilla What is Confidential Client?` +2. `@msal-mtls-pop-vanilla How do I load a certificate?` +3. `@msal-mtls-pop-vanilla Show me complete ConfApp example` +4. `@msal-mtls-pop-vanilla How do I handle certificate safely?` +5. Implement Confidential Client locally + +### **Path 4: FIC Two-Leg Deep Dive (45 minutes)** +1. `@msal-mtls-pop-fic-two-leg What is FIC two-leg?` +2. `@msal-mtls-pop-fic-two-leg Show me the four scenario combinations` +3. `@msal-mtls-pop-fic-two-leg Show me MSI Leg 1 → ConfApp Leg 2` +4. `@msal-mtls-pop-fic-two-leg How does certificate binding work?` +5. `@msal-mtls-pop-fic-two-leg Show complete end-to-end flow` +6. Implement two-leg flow locally + +### **Path 5: Production Ready (60 minutes)** +1. Complete one of the above paths +2. `@msal-mtls-pop-vanilla What production patterns should I follow?` +3. `@msal-mtls-pop-vanilla How do I add error handling?` +4. `@msal-mtls-pop-vanilla How do I add proper logging?` +5. Refactor your implementation with production patterns +6. Add comprehensive error handling + +--- + +## 🚀 Pro Tips + +✅ **Start with `@msal-mtls-pop-guidance`** if you're new +✅ **Use discovery prompts** from the "Discovery Prompts" section to explore +✅ **Follow a learning path** based on your use case +✅ **Enable `.WithAttestationSupport()`** for Credential Guard +✅ **Always check null** before adding certificates to HttpClientHandler +✅ **Use `ConfigureAwait(false)`** in production code +✅ **Add `CancellationToken`** support for better control +✅ **Implement `IDisposable`** correctly for HttpClient +✅ **Test locally first** before deploying to Azure + +--- + +## 💬 Quick Chat Commands + +Copy and paste these directly into VS Code Copilot Chat: + +@msal-mtls-pop-guidance What can you tell me about mTLS PoP? + +@msal-mtls-pop-vanilla Show me how to get started in 5 minutes + +@msal-mtls-pop-fic-two-leg Show me the complete end-to-end flow + +@workspace How do I choose between vanilla and FIC flows? + +Code + +--- + +## 📚 Available Helper Classes + +### Vanilla Flow +- `VanillaMsiMtlsPop.cs` - MSI token acquisition wrapper +- `MtlsPopTokenAcquirer.cs` - Generic token acquisition +- `ResourceCaller.cs` - HTTP client configuration and API calls + +### FIC Two-Leg Flow +- `FicLeg1Acquirer.cs` - Leg 1 token acquisition +- `FicAssertionProvider.cs` - Client assertion generation +- `FicLeg2Exchanger.cs` - Leg 2 token exchange +- `ResourceCaller.cs` - HTTP client configuration and API calls + +--- + +## 🔗 Related Resources + +- **PR #5733**: This implementation +- **Integration Tests**: `ClientCredentialsMtlsPopTests.cs` +- **MSAL.NET Docs**: Official documentation +- **Credential Guard**: Windows security feature +- **mTLS Spec**: RFC 8705 OAUTH 2.0 Mutual-TLS Client Authentication + +--- + +## ❓ Still Have Questions? + +Use the **Question Bank** above to discover answers. Most questions are already covered in one of the three skills! + +**Happy exploring!** 🚀 diff --git a/.github/skills/README.md b/.github/skills/README.md new file mode 100644 index 0000000000..af9227f33b --- /dev/null +++ b/.github/skills/README.md @@ -0,0 +1,292 @@ +# GitHub Copilot Agent Skills for MSAL.NET + +This directory contains GitHub Copilot Agent Skills that provide expert guidance for using Microsoft Authentication Library (MSAL) for .NET in various authentication scenarios. + +## What are GitHub Copilot Agent Skills? + +GitHub Copilot Agent Skills are specialized knowledge modules that help Copilot provide more accurate and context-aware assistance for specific technologies and scenarios. Each skill contains: + +- **SKILL.md**: Structured documentation with YAML frontmatter for Copilot integration +- **Helper Classes**: Production-ready C# code examples that follow MSAL.NET best practices +- **Examples**: Real-world scenarios with complete, tested code samples +- **Shared Resources**: Reusable patterns, credential setup guides, and troubleshooting content (DRY principle) + +## Available Skills + +### 1. Confidential Client Authentication (`msal-confidential-auth/`) + +> **📖 [Full documentation](msal-confidential-auth/README.md)** + +A comprehensive skill set for confidential client authentication patterns in MSAL.NET, covering three core flows with granularized, reusable credential setup patterns. + +#### Authentication Flows + +- **[Authorization Code Flow](msal-confidential-auth/auth-code-flow/SKILL.md)** - Web applications with user sign-in +- **[On-Behalf-Of (OBO) Flow](msal-confidential-auth/obo-flow/SKILL.md)** - Multi-tier services acting on behalf of users +- **[Client Credentials Flow](msal-confidential-auth/client-credentials/SKILL.md)** - Service-to-service daemon applications + +#### Shared Resources (DRY Principle) + +All skills reference these granularized patterns: + +**Credential Setup:** +- [Certificate Setup](msal-confidential-auth/shared/credential-setup/certificate-setup.md) - Load from file, store, or Key Vault +- [Certificate SNI Setup](msal-confidential-auth/shared/credential-setup/certificate-sni-setup.md) - Subject Name Identifier configuration +- [Federated Identity Credentials](msal-confidential-auth/shared/credential-setup/federated-identity-credentials.md) - Keyless authentication + +**Patterns & Best Practices:** +- [Token Caching Strategies](msal-confidential-auth/shared/patterns/token-caching-strategies.md) - Cache management +- [Error Handling Patterns](msal-confidential-auth/shared/patterns/error-handling-patterns.md) - Common error scenarios +- [Troubleshooting Guide](msal-confidential-auth/shared/patterns/troubleshooting.md) - Comprehensive troubleshooting + +**Code Examples:** +- [with-certificate.cs](msal-confidential-auth/shared/code-examples/with-certificate.cs) - Standard certificate authentication +- [with-certificate-sni.cs](msal-confidential-auth/shared/code-examples/with-certificate-sni.cs) - Certificate with SNI +- [with-federated-identity-credentials.cs](msal-confidential-auth/shared/code-examples/with-federated-identity-credentials.cs) - FIC authentication + +#### Agent Capabilities + +For each flow, agents can help with: + +1. **Generate Code Snippet** - Show code for [flow] with [credential type] +2. **Setup Guidance** - When do I set up [credential type]? +3. **Error Resolution** - I'm getting [error], what's the solution? +4. **Best Practices** - What are the best practices for [scenario]? +5. **Explain the Flow** - Explain how [flow] works +6. **Decision Help** - Which flow should I use for [scenario]? +7. **Validate Code** - Review and validate MSAL implementation for correctness + +#### When to Use + +- **Web Application with User Sign-In** → Authorization Code Flow +- **Multi-Tier Service (API calling another API)** → On-Behalf-Of Flow +- **Daemon/Background Service** → Client Credentials Flow + +--- + +### 2. mTLS Proof-of-Possession (PoP) Skills + +Specialized skills for mTLS PoP authentication with Managed Identity and Confidential Client support. These skills **reference the shared patterns** from `msal-confidential-auth/shared/` for DRY compliance. + +#### `msal-mtls-pop-guidance/` + +**Shared terminology, conventions, and patterns** for mTLS PoP flows. + +**Provides:** +- Common terminology definitions (vanilla vs FIC two-leg) +- Authentication method comparison (MSI vs Confidential Client) +- MSI limitations (no `WithClientAssertion()` API - cannot perform FIC Leg 2) +- All 3 UAMI identifier types with real example IDs from PR #5726 +- FIC valid combinations matrix (4 scenarios: MSI/ConfApp × Bearer/PoP) +- Version requirements (MSAL.NET 4.82.1+) and reviewer expectations + +**When to use:** Reference this when working with any mTLS PoP scenario to understand terminology and conventions. + +**📄 [View Skill](msal-mtls-pop-guidance/SKILL.md)** + +--- + +#### `msal-mtls-pop-vanilla/` + +**Direct mTLS PoP token acquisition** for target resources (single-step, no token exchange). + +**Covers:** +- System-Assigned Managed Identity (SAMI) - Azure environments only +- User-Assigned Managed Identity (UAMI) - By ClientId, ResourceId, or ObjectId +- Confidential Client with certificate (SNI) - Works anywhere +- Credential Guard attestation via `.WithAttestationSupport()` +- mTLS-specific endpoints (e.g., `https://mtlstb.graph.microsoft.com`) +- Self-contained Quick Start examples with complete inline HTTP calls +- Null-safe certificate handling with proper checks + +**Helper Classes:** +- `VanillaMsiMtlsPop.cs` - MSI implementation (SAMI + all 3 UAMI ID types) +- `MtlsPopTokenAcquirer.cs` - Generic token acquisition with attestation +- `ResourceCaller.cs` - HTTP client configuration and API calls with mTLS binding + +**When to use:** Acquire an mTLS PoP token directly for a target resource (Microsoft Graph, Azure Key Vault, Azure Storage, custom APIs). + +**References:** [Certificate Setup](msal-confidential-auth/shared/credential-setup/certificate-setup.md), [Certificate SNI Setup](msal-confidential-auth/shared/credential-setup/certificate-sni-setup.md), [Troubleshooting](msal-confidential-auth/shared/patterns/troubleshooting.md) + +**📄 [View Skill](msal-mtls-pop-vanilla/SKILL.md)** + +--- + +#### `msal-mtls-pop-fic-two-leg/` + +**Federated Identity Credential (FIC) token exchange** with assertions (two-step pattern). + +**Covers:** +- **Leg 1:** MSI or Confidential Client → `api://AzureADTokenExchange` (with PoP + Attestation) +- **Leg 2:** Confidential Client ONLY → Final resource (Bearer or mTLS PoP) +- Certificate binding requirement: ALL scenarios pass `TokenBindingCertificate` from Leg 1 +- All 4 valid combinations (MSI/ConfApp Leg 1 × Bearer/PoP Leg 2) +- Region specification: All Leg 2 Confidential Client apps include `.WithAzureRegion()` + +**Helper Classes:** +- `FicLeg1Acquirer.cs` - Leg 1 token acquisition (MSI or Confidential Client) +- `FicAssertionProvider.cs` - Constructs `ClientSignedAssertion` from Leg 1 token +- `FicLeg2Exchanger.cs` - Leg 2 token exchange (Confidential Client only) +- `ResourceCaller.cs` - HTTP client configuration for final resource calls + +**When to use:** Workload identity federation scenarios requiring two-leg token exchange (Kubernetes workload identity, multi-tenant authentication chains, cross-tenant scenarios). + +**References:** [Federated Identity Credentials Setup](msal-confidential-auth/shared/credential-setup/federated-identity-credentials.md), [Certificate Setup](msal-confidential-auth/shared/credential-setup/certificate-setup.md), [Troubleshooting](msal-confidential-auth/shared/patterns/troubleshooting.md) + +**📄 [View Skill](msal-mtls-pop-fic-two-leg/SKILL.md)** + +--- + +## Quick Start Guide + +### Choose Your Scenario + +| Scenario | Skill to Use | +|----------|--------------| +| Web app with user sign-in | [Authorization Code Flow](msal-confidential-auth/auth-code-flow/SKILL.md) | +| API acting on behalf of user | [On-Behalf-Of Flow](msal-confidential-auth/obo-flow/SKILL.md) | +| Daemon/background service | [Client Credentials Flow](msal-confidential-auth/client-credentials/SKILL.md) | +| Direct mTLS PoP token (MSI/SNI) | [Vanilla mTLS PoP](msal-mtls-pop-vanilla/SKILL.md) | +| FIC token exchange with mTLS PoP | [FIC Two-Leg mTLS PoP](msal-mtls-pop-fic-two-leg/SKILL.md) | + +### Choose Your Credential Type + +| Credential Type | Setup Guide | +|-----------------|-------------| +| Standard Certificate | [Certificate Setup](msal-confidential-auth/shared/credential-setup/certificate-setup.md) | +| Certificate with SNI | [Certificate SNI Setup](msal-confidential-auth/shared/credential-setup/certificate-sni-setup.md) | +| Federated Identity Credentials | [FIC Setup](msal-confidential-auth/shared/credential-setup/federated-identity-credentials.md) | +| System-Assigned Managed Identity | [Vanilla mTLS PoP](msal-mtls-pop-vanilla/SKILL.md) (Azure only) | +| User-Assigned Managed Identity | [Vanilla mTLS PoP](msal-mtls-pop-vanilla/SKILL.md) (3 ID types) | + +## Requirements + +### By Skill Set + +**Confidential Client Authentication:** +- MSAL.NET 4.61.0 or later +- .NET 6.0+ recommended +- Appropriate credential type configured (certificate, FIC, etc.) + +**mTLS PoP Skills:** +- MSAL.NET 4.82.1 or later (for `WithMtlsProofOfPossession()`, `WithAttestationSupport()`) +- Microsoft.Identity.Client.KeyAttestation NuGet package +- .NET 8.0 recommended +- Azure environment for MSI (SAMI/UAMI) or certificate for local/on-premises + +## Using These Skills + +### In GitHub Copilot Chat + +GitHub Copilot automatically discovers and uses these skills when you ask questions. Simply ask natural language questions: + +**Confidential Client Examples:** +- "How do I implement authorization code flow with certificate?" +- "Show me OBO flow with managed identity" +- "What's the difference between standard cert and SNI?" +- "How do I set up federated identity credentials?" + +**mTLS PoP Examples:** +- "How do I acquire an mTLS PoP token using Managed Identity?" +- "Show me FIC two-leg token exchange with MSI and Confidential Client" +- "What's the difference between SAMI and UAMI?" +- "How do I call Microsoft Graph with mTLS PoP?" + +### Direct Skill Reference + +Reference specific skills in your prompts for targeted assistance: +``` +@workspace Use the msal-mtls-pop-vanilla skill to implement token acquisition +@workspace Use the client-credentials skill to set up a daemon app +@workspace Use the auth-code-flow skill for my web application +``` + +### Code Examples + +Each skill includes production-ready C# helper classes following MSAL.NET conventions: +- Async/await with `ConfigureAwait(false)` +- `CancellationToken` support with defaults +- Full `IDisposable` implementation +- Input validation (`ArgumentNullException.ThrowIfNull`) +- Disposal checks (`ObjectDisposedException.ThrowIf`) + +## Architecture & Design + +### DRY Principle + +Skills follow the **Don't Repeat Yourself (DRY)** principle: +- **Shared patterns** live in `msal-confidential-auth/shared/` (single source of truth) +- **Individual skills** reference shared patterns via links (no duplication) +- **Updates** to credential setup, error handling, or troubleshooting happen once +- **Composition** is easy - mix and match patterns from multiple skills + +### Skill Structure + +#### Individual Skills +``` +skill-name/ +├── SKILL.md # Main documentation with YAML frontmatter +├── HelperClass1.cs # Optional production helper class +└── HelperClass2.cs # Optional production helper class +``` + +#### Skill Sets with Shared Resources +``` +skill-set-name/ +├── README.md # Skill set-specific overview +├── flow1/ +│ └── SKILL.md # Flow-specific documentation +├── flow2/ +│ └── SKILL.md +└── shared/ # Reusable patterns (referenced by all) + ├── code-examples/ # Copy-paste code snippets + ├── credential-setup/ # Setup guides by credential type + └── patterns/ # Common patterns, troubleshooting +``` + +### YAML Frontmatter Format + +Each SKILL.md begins with YAML frontmatter for Copilot integration: + +```yaml +--- +skill_name: unique-skill-identifier +version: 1.0 +description: Brief description of what this skill covers +applies_to: + - Area/Feature this skill applies to +tags: + - Relevant + - Search + - Keywords +--- +``` + +## Contributing + +When adding new skills: + +1. **Follow structure** - Use existing naming conventions and directory structure +2. **Include YAML frontmatter** - Every SKILL.md needs complete frontmatter +3. **Provide tested examples** - All code must be production-ready and tested +4. **Add troubleshooting** - Include common issues and solutions +5. **Document requirements** - List NuGet packages, MSAL versions, target frameworks +6. **Use real IDs** - When applicable, use example IDs from E2E tests +7. **Follow conventions** - Adhere to MSAL.NET coding conventions in helper classes +8. **Reference shared patterns** - Link to `msal-confidential-auth/shared/` instead of duplicating content (DRY principle) +9. **Update catalog** - Add new skills to this README for discoverability + +## Additional Resources + +### MSAL.NET Documentation +- [MSAL.NET Official Documentation](https://aka.ms/msal-net) +- [MSAL.NET GitHub Repository](https://github.com/AzureAD/microsoft-authentication-library-for-dotnet) + +### OAuth & Identity Standards +- [OAuth 2.0 Specification](https://tools.ietf.org/html/draft-ietf-oauth-v2-1-01) +- [OAuth 2.0 Authorization Code Flow](https://tools.ietf.org/html/draft-ietf-oauth-v2-1-01) +- [Azure Managed Identity](https://learn.microsoft.com/en-us/azure/app-service/overview-managed-identity) + +### Test References +- [mTLS PoP Integration Tests](../../tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/ClientCredentialsMtlsPopTests.cs) +- [Managed Identity E2E Tests](../../tests/Microsoft.Identity.Test.E2e/) diff --git a/.github/skills/msal-mtls-pop-fic-two-leg/FicAssertionProvider.cs b/.github/skills/msal-mtls-pop-fic-two-leg/FicAssertionProvider.cs new file mode 100644 index 0000000000..6942ef3787 --- /dev/null +++ b/.github/skills/msal-mtls-pop-fic-two-leg/FicAssertionProvider.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Identity.Client; +using Microsoft.Identity.Client.Extensibility; + +namespace MsalMtlsPop.Fic +{ + /// + /// Helper for creating ClientSignedAssertion from Leg 1 authentication result. + /// Supports optional certificate binding for jwt-pop scenarios. + /// + /// + /// Production-ready implementation following MSAL.NET conventions: + /// - Input validation with ArgumentNullException.ThrowIfNull + /// - Proper certificate handling for PoP scenarios + /// + public sealed class FicAssertionProvider + { + private readonly AuthenticationResult _leg1Result; + + /// + /// Creates an assertion provider from Leg 1 authentication result. + /// + /// Leg 1 authentication result containing the assertion token. + /// Thrown when leg1Result is null. + public FicAssertionProvider(AuthenticationResult leg1Result) + { + ArgumentNullException.ThrowIfNull(leg1Result); + + if (string.IsNullOrEmpty(leg1Result.AccessToken)) + { + throw new ArgumentException( + "Leg 1 authentication result does not contain an access token.", + nameof(leg1Result)); + } + + _leg1Result = leg1Result; + } + + /// + /// Creates a ClientSignedAssertion for use in Leg 2 token exchange. + /// + /// + /// If true, binds Leg 1's BindingCertificate to the assertion for jwt-pop. + /// Use true when Leg 2 will request mTLS PoP token, false for Bearer token. + /// + /// ClientSignedAssertion containing Leg 1's token and optional certificate binding. + /// + /// Thrown when bindCertificate is true but Leg 1's BindingCertificate is null. + /// + public ClientSignedAssertion CreateAssertion(bool bindCertificate = false) + { + if (bindCertificate && _leg1Result.BindingCertificate == null) + { + throw new InvalidOperationException( + "Cannot bind certificate: Leg 1's BindingCertificate is null. " + + "Ensure Leg 1 used .WithMtlsProofOfPossession() to acquire a PoP token."); + } + + return new ClientSignedAssertion + { + Assertion = _leg1Result.AccessToken, + TokenBindingCertificate = bindCertificate ? _leg1Result.BindingCertificate : null + }; + } + + /// + /// Creates an assertion provider callback for use with ConfidentialClientApplicationBuilder.WithClientAssertion(). + /// + /// + /// If true, binds Leg 1's BindingCertificate to the assertion for jwt-pop. + /// + /// + /// A callback function that returns ClientSignedAssertion for assertion-based authentication. + /// + public Func> CreateAssertionCallback( + bool bindCertificate = false) + { + return (options, ct) => Task.FromResult(CreateAssertion(bindCertificate)); + } + } +} diff --git a/.github/skills/msal-mtls-pop-fic-two-leg/FicLeg1Acquirer.cs b/.github/skills/msal-mtls-pop-fic-two-leg/FicLeg1Acquirer.cs new file mode 100644 index 0000000000..c8bb0b9220 --- /dev/null +++ b/.github/skills/msal-mtls-pop-fic-two-leg/FicLeg1Acquirer.cs @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Identity.Client; + +namespace MsalMtlsPop.Fic +{ + /// + /// Helper for acquiring Leg 1 tokens in FIC two-leg flow. + /// Supports both Managed Identity and Confidential Client authentication. + /// Always targets api://AzureADTokenExchange with mTLS PoP and attestation support. + /// + /// + /// Production-ready implementation following MSAL.NET conventions: + /// - ConfigureAwait(false) on all awaits + /// - CancellationToken support with defaults + /// - Proper IDisposable implementation + /// - Input validation and disposal checks + /// + public sealed class FicLeg1Acquirer : IDisposable + { + private const string TokenExchangeResource = "api://AzureADTokenExchange"; + + private readonly IManagedIdentityApplication _msiApp; + private readonly IConfidentialClientApplication _confApp; + private readonly bool _isManagedIdentity; + private bool _disposed; + + /// + /// Creates a Leg 1 acquirer for Managed Identity scenarios. + /// + /// Configured Managed Identity application. + /// Thrown when msiApp is null. + public FicLeg1Acquirer(IManagedIdentityApplication msiApp) + { + ArgumentNullException.ThrowIfNull(msiApp); + + _msiApp = msiApp; + _isManagedIdentity = true; + } + + /// + /// Creates a Leg 1 acquirer for Confidential Client scenarios. + /// + /// Configured Confidential Client application. + /// Thrown when confApp is null. + public FicLeg1Acquirer(IConfidentialClientApplication confApp) + { + ArgumentNullException.ThrowIfNull(confApp); + + _confApp = confApp; + _isManagedIdentity = false; + } + + /// + /// Acquires a Leg 1 token for api://AzureADTokenExchange with mTLS PoP and attestation. + /// + /// Cancellation token for the operation. + /// + /// Authentication result containing mTLS PoP token for api://AzureADTokenExchange. + /// This token will be used as the assertion in Leg 2. + /// + /// Thrown when the acquirer has been disposed. + /// Thrown when token acquisition fails. + public async Task AcquireTokenAsync( + CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + AuthenticationResult result; + + if (_isManagedIdentity) + { + // MSI path with attestation support + result = await _msiApp + .AcquireTokenForManagedIdentity(TokenExchangeResource) + .WithMtlsProofOfPossession() + .WithAttestationSupport() // Credential Guard attestation + .ExecuteAsync(cancellationToken) + .ConfigureAwait(false); + } + else + { + // Confidential Client path + result = await _confApp + .AcquireTokenForClient(new[] { $"{TokenExchangeResource}/.default" }) + .WithMtlsProofOfPossession() + .ExecuteAsync(cancellationToken) + .ConfigureAwait(false); + } + + // Validate result + if (result.BindingCertificate == null) + { + throw new InvalidOperationException( + "BindingCertificate is null after Leg 1 token acquisition. " + + "This should not happen if .WithMtlsProofOfPossession() was called correctly."); + } + + if (string.IsNullOrEmpty(result.AccessToken)) + { + throw new InvalidOperationException( + "AccessToken is null or empty after Leg 1 token acquisition."); + } + + return result; + } + + /// + /// Disposes the acquirer. Note that the underlying MSAL application + /// instances are NOT disposed, as they may be reused elsewhere. + /// + public void Dispose() + { + if (_disposed) + { + return; + } + + // Note: We don't dispose _msiApp or _confApp as they may be reused + // Caller is responsible for disposing those when appropriate + _disposed = true; + } + } +} diff --git a/.github/skills/msal-mtls-pop-fic-two-leg/FicLeg2Exchanger.cs b/.github/skills/msal-mtls-pop-fic-two-leg/FicLeg2Exchanger.cs new file mode 100644 index 0000000000..e9b9a5e377 --- /dev/null +++ b/.github/skills/msal-mtls-pop-fic-two-leg/FicLeg2Exchanger.cs @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Identity.Client; + +namespace MsalMtlsPop.Fic +{ + /// + /// Helper for executing Leg 2 token exchange in FIC two-leg flow. + /// MUST use Confidential Client (MSI does not have WithClientAssertion API). + /// Supports both Bearer and mTLS PoP final tokens. + /// + /// + /// Production-ready implementation following MSAL.NET conventions: + /// - ConfigureAwait(false) on all awaits + /// - CancellationToken support with defaults + /// - Proper IDisposable implementation + /// - Input validation and disposal checks + /// + /// IMPORTANT: Only IConfidentialClientApplication can perform Leg 2 exchange. + /// IManagedIdentityApplication does NOT have WithClientAssertion() method. + /// + public sealed class FicLeg2Exchanger : IDisposable + { + private readonly IConfidentialClientApplication _confApp; + private readonly FicAssertionProvider _assertionProvider; + private bool _disposed; + + /// + /// Creates a Leg 2 exchanger for Confidential Client. + /// + /// + /// Configured Confidential Client application with WithClientAssertion callback set. + /// This MUST be a Confidential Client - MSI cannot perform Leg 2 exchange. + /// + /// Provider that creates ClientSignedAssertion from Leg 1 result. + /// Thrown when confApp or assertionProvider is null. + public FicLeg2Exchanger( + IConfidentialClientApplication confApp, + FicAssertionProvider assertionProvider) + { + ArgumentNullException.ThrowIfNull(confApp); + ArgumentNullException.ThrowIfNull(assertionProvider); + + _confApp = confApp; + _assertionProvider = assertionProvider; + } + + /// + /// Exchanges the Leg 1 token for a final target resource token. + /// + /// + /// Target resource scopes (e.g., "https://graph.microsoft.com/.default"). + /// Must include ".default" suffix. + /// + /// + /// If true, requests mTLS PoP token and binds with Leg 1's certificate. + /// If false, requests standard Bearer token. + /// + /// Cancellation token for the operation. + /// + /// Authentication result containing the final token (Bearer or mTLS PoP). + /// If mTLS PoP was requested, BindingCertificate will match Leg 1's certificate. + /// + /// Thrown when scopes is null. + /// Thrown when scopes array is empty. + /// Thrown when the exchanger has been disposed. + /// Thrown when token exchange fails. + public async Task ExchangeTokenAsync( + string[] scopes, + bool requestMtlsPop = false, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(scopes); + ObjectDisposedException.ThrowIf(_disposed, this); + + if (scopes.Length == 0) + { + throw new ArgumentException("Scopes array cannot be empty.", nameof(scopes)); + } + + // Validate scopes format + if (!scopes.Any(s => s.EndsWith("/.default", StringComparison.OrdinalIgnoreCase))) + { + throw new ArgumentException( + "Scopes must include '.default' suffix (e.g., 'https://graph.microsoft.com/.default').", + nameof(scopes)); + } + + var requestBuilder = _confApp.AcquireTokenForClient(scopes); + + if (requestMtlsPop) + { + // Request mTLS PoP token - certificate binding happens automatically + // via ClientSignedAssertion.TokenBindingCertificate + requestBuilder = requestBuilder.WithMtlsProofOfPossession(); + } + + var result = await requestBuilder + .ExecuteAsync(cancellationToken) + .ConfigureAwait(false); + + // Validate result based on request type + if (requestMtlsPop) + { + if (result.TokenType != "mtls_pop") + { + throw new InvalidOperationException( + $"Expected token type 'mtls_pop' but got '{result.TokenType}'. " + + "Verify that .WithMtlsProofOfPossession() was called correctly."); + } + + if (result.BindingCertificate == null) + { + throw new InvalidOperationException( + "BindingCertificate is null after mTLS PoP token acquisition. " + + "Verify that Leg 1's BindingCertificate was passed correctly in ClientSignedAssertion."); + } + } + + return result; + } + + /// + /// Disposes the exchanger. Note that the underlying Confidential Client application + /// instance is NOT disposed, as it may be reused elsewhere. + /// + public void Dispose() + { + if (_disposed) + { + return; + } + + // Note: We don't dispose _confApp as it may be reused + // Caller is responsible for disposing it when appropriate + _disposed = true; + } + } +} diff --git a/.github/skills/msal-mtls-pop-fic-two-leg/ResourceCaller.cs b/.github/skills/msal-mtls-pop-fic-two-leg/ResourceCaller.cs new file mode 100644 index 0000000000..b4ba40b0c8 --- /dev/null +++ b/.github/skills/msal-mtls-pop-fic-two-leg/ResourceCaller.cs @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Identity.Client; + +namespace MsalMtlsPop.Fic +{ + /// + /// Helper class for calling protected resources using mTLS PoP tokens. + /// Handles HttpClient configuration with mTLS binding certificate. + /// This is a reused pattern from vanilla flow - works identically for FIC Leg 2 results. + /// + /// + /// Production-ready implementation following MSAL.NET conventions: + /// - ConfigureAwait(false) on all awaits + /// - CancellationToken support with defaults + /// - Proper IDisposable implementation + /// - Input validation and disposal checks + /// + public sealed class ResourceCaller : IDisposable + { + private readonly HttpClient _httpClient; + private readonly string _accessToken; + private readonly string _tokenType; + private bool _disposed; + + /// + /// Creates a ResourceCaller from an mTLS PoP authentication result. + /// + /// Authentication result containing mTLS PoP token and binding certificate. + /// Thrown when authResult is null. + /// Thrown when token type is not mtls_pop or BindingCertificate is null. + public ResourceCaller(AuthenticationResult authResult) + { + ArgumentNullException.ThrowIfNull(authResult); + + if (string.IsNullOrEmpty(authResult.TokenType) || + !authResult.TokenType.Equals("mtls_pop", StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException( + $"Expected token type 'mtls_pop', but got '{authResult.TokenType}'. " + + "Ensure .WithMtlsProofOfPossession() was called during token acquisition.", + nameof(authResult)); + } + + if (authResult.BindingCertificate == null) + { + throw new ArgumentException( + "BindingCertificate is null. This certificate is required for mTLS calls. " + + "Ensure .WithMtlsProofOfPossession() was called before ExecuteAsync().", + nameof(authResult)); + } + + _accessToken = authResult.AccessToken; + _tokenType = authResult.TokenType; + + // Configure HttpClientHandler with mTLS binding certificate + var handler = new HttpClientHandler(); + handler.ClientCertificates.Add(authResult.BindingCertificate); + + _httpClient = new HttpClient(handler); + + // Set mTLS PoP authorization header + _httpClient.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("mtls_pop", _accessToken); + } + + /// + /// Calls a protected resource endpoint with mTLS PoP authentication. + /// + /// The URI of the protected resource to call. + /// Cancellation token for the operation. + /// Response body as a string. + /// Thrown when resourceUri is null. + /// Thrown when the caller has been disposed. + /// Thrown when the HTTP request fails. + public async Task CallResourceAsync( + string resourceUri, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(resourceUri); + ObjectDisposedException.ThrowIf(_disposed, this); + + var response = await _httpClient + .GetAsync(resourceUri, cancellationToken) + .ConfigureAwait(false); + + response.EnsureSuccessStatusCode(); + + var content = await response.Content + .ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); + + return content; + } + + /// + /// Calls a protected resource endpoint with mTLS PoP authentication and returns the full response. + /// + /// The URI of the protected resource to call. + /// Cancellation token for the operation. + /// The complete HTTP response message. + /// Thrown when resourceUri is null. + /// Thrown when the caller has been disposed. + public async Task CallResourceFullResponseAsync( + string resourceUri, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(resourceUri); + ObjectDisposedException.ThrowIf(_disposed, this); + + var response = await _httpClient + .GetAsync(resourceUri, cancellationToken) + .ConfigureAwait(false); + + return response; + } + + /// + /// Disposes the ResourceCaller and its underlying HttpClient. + /// + public void Dispose() + { + if (_disposed) + { + return; + } + + _httpClient?.Dispose(); + _disposed = true; + } + } +} diff --git a/.github/skills/msal-mtls-pop-fic-two-leg/SKILL.md b/.github/skills/msal-mtls-pop-fic-two-leg/SKILL.md new file mode 100644 index 0000000000..2f2e8e07eb --- /dev/null +++ b/.github/skills/msal-mtls-pop-fic-two-leg/SKILL.md @@ -0,0 +1,350 @@ +--- +skill_name: msal-mtls-pop-fic-two-leg +version: 1.0 +description: FIC token exchange pattern using assertions for mTLS PoP with MSI and Confidential Client support +applies_to: + - MSAL.NET/mTLS-PoP + - MSAL.NET/Managed-Identity + - MSAL.NET/Confidential-Client + - MSAL.NET/FIC +tags: + - msal + - mtls + - pop + - proof-of-possession + - fic + - token-exchange + - two-leg + - workload-identity +--- + +# MSAL.NET mTLS PoP - FIC Two-Leg Flow (Token Exchange) + +This skill covers Federated Identity Credential (FIC) token exchange using assertions with mTLS Proof-of-Possession. Use this for workload identity federation scenarios in Kubernetes, multi-tenant authentication chains, or any case requiring token exchange. + +> **Note:** This skill focuses on FIC two-leg flow patterns with mTLS PoP. For general credential setup (certificates, FIC configuration, etc.), see the [Confidential Auth Skill](../msal-confidential-auth/shared/) for reusable patterns. For FIC Azure Portal configuration, see [Federated Identity Credentials Setup](../msal-confidential-auth/shared/credential-setup/federated-identity-credentials.md). + +## What is FIC Two-Leg Flow? + +**FIC two-leg flow** is a two-step token exchange process: + +1. **Leg 1**: Acquire token for `api://AzureADTokenExchange` (MSI or Confidential Client) + - Always targets `api://AzureADTokenExchange` + - Can use Managed Identity (SAMI/UAMI) OR Confidential Client + - Includes `.WithMtlsProofOfPossession()` and `.WithAttestationSupport()` + +2. **Leg 2**: Exchange Leg 1 token for final target resource (Confidential Client ONLY) + - Uses Leg 1's AccessToken as the `Assertion` in `ClientSignedAssertion` + - **MUST use Confidential Client** (MSI does NOT have `WithClientAssertion()` API) + - Can request Bearer OR mTLS PoP final token + - If mTLS PoP: Uses Leg 1's `BindingCertificate` as `TokenBindingCertificate` + +## Valid Combinations + +| Leg 1 Auth Method | Leg 1 Token Type | Leg 2 Auth Method | Leg 2 Token Type | Valid? | +|-------------------|------------------|-------------------|------------------|--------| +| MSI (SAMI/UAMI) | mTLS PoP | Confidential Client | Bearer | ✅ Yes | +| MSI (SAMI/UAMI) | mTLS PoP | Confidential Client | mTLS PoP | ✅ Yes | +| Confidential Client | mTLS PoP | Confidential Client | Bearer | ✅ Yes | +| Confidential Client | mTLS PoP | Confidential Client | mTLS PoP | ✅ Yes | +| MSI | mTLS PoP | **MSI** | Any | ❌ **NO** - MSI lacks WithClientAssertion | + +## Requirements + +- **MSAL.NET**: 4.82.1 minimum +- **NuGet Packages**: + ```bash + dotnet add package Microsoft.Identity.Client --version 4.82.1 + dotnet add package Microsoft.Identity.Client.KeyAttestation + ``` +- **Target Framework**: net8.0 recommended +- **Namespaces**: + ```csharp + using Microsoft.Identity.Client; + using Microsoft.Identity.Client.AppConfig; // For ManagedIdentityId + using Microsoft.Identity.Client.KeyAttestation; // For WithAttestationSupport() + using Microsoft.Identity.Client.Extensibility; // For ClientSignedAssertion + ``` + +## Complete Examples + +### Scenario 1: MSI Leg 1 → Confidential Client Leg 2 → Bearer Token + +```csharp +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 token for api://AzureADTokenExchange with PoP +var leg1App = ManagedIdentityApplicationBuilder.Create( + ManagedIdentityId.WithUserAssignedClientId("6325cd32-9911-41f3-819c-416cdf9104e7")) + .Build(); + +var leg1Result = await leg1App + .AcquireTokenForManagedIdentity("api://AzureADTokenExchange") + .WithMtlsProofOfPossession() + .WithAttestationSupport() // Credential Guard attestation + .ExecuteAsync(); + +Console.WriteLine($"Leg 1 Token Type: {leg1Result.TokenType}"); // "mtls_pop" + +// Leg 2: Confidential Client exchanges token for final resource (Bearer) +var leg2App = ConfidentialClientApplicationBuilder + .Create("your-leg2-client-id") + .WithAuthority("https://login.microsoftonline.com/your-tenant-id") + .WithAzureRegion("westus3") // Specify region of your Azure resource + .WithClientAssertion((options, ct) => + { + return Task.FromResult(new ClientSignedAssertion + { + Assertion = leg1Result.AccessToken, // Use Leg 1's token + TokenBindingCertificate = leg1Result.BindingCertificate // Always pass Leg 1's cert + }); + }) + .Build(); + +var leg2Result = await leg2App + .AcquireTokenForClient(new[] { "https://vault.azure.net/.default" }) + .ExecuteAsync(); // No .WithMtlsProofOfPossession() → Bearer token + +Console.WriteLine($"Leg 2 Token Type: {leg2Result.TokenType}"); // "Bearer" +``` + +### Scenario 2: MSI Leg 1 → Confidential Client Leg 2 → mTLS PoP Token + +```csharp +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 token for api://AzureADTokenExchange with PoP +var leg1App = ManagedIdentityApplicationBuilder.Create( + ManagedIdentityId.SystemAssigned) + .Build(); + +var leg1Result = await leg1App + .AcquireTokenForManagedIdentity("api://AzureADTokenExchange") + .WithMtlsProofOfPossession() + .WithAttestationSupport() + .ExecuteAsync(); + +// Leg 2: Confidential Client exchanges token for final resource (mTLS PoP) +var leg2App = ConfidentialClientApplicationBuilder + .Create("your-leg2-client-id") + .WithAuthority("https://login.microsoftonline.com/your-tenant-id") + .WithAzureRegion("westus3") // Specify region of your Azure resource + .WithClientAssertion((options, ct) => + { + return Task.FromResult(new ClientSignedAssertion + { + Assertion = leg1Result.AccessToken, + TokenBindingCertificate = leg1Result.BindingCertificate // ← Bind with Leg 1's cert + }); + }) + .Build(); + +var leg2Result = await leg2App + .AcquireTokenForClient(new[] { "https://graph.microsoft.com/.default" }) + .WithMtlsProofOfPossession() // ← Request PoP token + .ExecuteAsync(); + +Console.WriteLine($"Leg 2 Token Type: {leg2Result.TokenType}"); // "mtls_pop" +Console.WriteLine($"Cert matches Leg 1: {leg2Result.BindingCertificate?.Thumbprint == leg1Result.BindingCertificate?.Thumbprint}"); +``` + +### Scenario 3: Confidential Client Leg 1 → Confidential Client Leg 2 → Bearer Token + +```csharp +using System; +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; +using Microsoft.Identity.Client; +using Microsoft.Identity.Client.Extensibility; + +// Load certificate for Leg 1 - see ../msal-confidential-auth/shared/credential-setup/certificate-setup.md +var leg1Cert = GetCertificateFromStore("CN=Leg1Certificate"); + +// Leg 1: Confidential Client acquires token for api://AzureADTokenExchange +// For certificate and SNI setup details, see: +// - ../msal-confidential-auth/shared/credential-setup/certificate-setup.md +// - ../msal-confidential-auth/shared/credential-setup/certificate-sni-setup.md +var leg1App = ConfidentialClientApplicationBuilder + .Create("your-leg1-client-id") + .WithAuthority("https://login.microsoftonline.com/your-tenant-id") + .WithAzureRegion("westus3") + .WithCertificate(leg1Cert, sendX5c: true) + .Build(); + +var leg1Result = await leg1App + .AcquireTokenForClient(new[] { "api://AzureADTokenExchange/.default" }) + .WithMtlsProofOfPossession() + .ExecuteAsync(); + +// Leg 2: Different Confidential Client exchanges token (Bearer) +var leg2App = ConfidentialClientApplicationBuilder + .Create("your-leg2-client-id") + .WithAuthority("https://login.microsoftonline.com/your-tenant-id") + .WithAzureRegion("westus3") // Specify region of your Azure resource + .WithClientAssertion((options, ct) => + { + return Task.FromResult(new ClientSignedAssertion + { + Assertion = leg1Result.AccessToken, + TokenBindingCertificate = leg1Result.BindingCertificate // Always pass Leg 1's cert + }); + }) + .Build(); + +var leg2Result = await leg2App + .AcquireTokenForClient(new[] { "https://storage.azure.com/.default" }) + .ExecuteAsync(); // No .WithMtlsProofOfPossession() → Bearer token + +Console.WriteLine($"Leg 2 Token Type: {leg2Result.TokenType}"); // "Bearer" +``` + +### Scenario 4: Confidential Client Leg 1 → Confidential Client Leg 2 → mTLS PoP Token + +```csharp +using System; +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; +using Microsoft.Identity.Client; +using Microsoft.Identity.Client.Extensibility; + +// Load certificate for Leg 1 - see ../msal-confidential-auth/shared/credential-setup/certificate-setup.md +var leg1Cert = GetCertificateFromStore("CN=Leg1Certificate"); + +// Leg 1: Confidential Client acquires token for api://AzureADTokenExchange +// For certificate and SNI setup details, see: +// - ../msal-confidential-auth/shared/credential-setup/certificate-setup.md +// - ../msal-confidential-auth/shared/credential-setup/certificate-sni-setup.md +var leg1App = ConfidentialClientApplicationBuilder + .Create("your-leg1-client-id") + .WithAuthority("https://login.microsoftonline.com/your-tenant-id") + .WithAzureRegion("westus3") + .WithCertificate(leg1Cert, sendX5c: true) + .Build(); + +var leg1Result = await leg1App + .AcquireTokenForClient(new[] { "api://AzureADTokenExchange/.default" }) + .WithMtlsProofOfPossession() + .ExecuteAsync(); + +// Leg 2: Different Confidential Client exchanges token (mTLS PoP) +var leg2App = ConfidentialClientApplicationBuilder + .Create("your-leg2-client-id") + .WithAuthority("https://login.microsoftonline.com/your-tenant-id") + .WithAzureRegion("westus3") // Specify region of your Azure resource + .WithClientAssertion((options, ct) => + { + return Task.FromResult(new ClientSignedAssertion + { + Assertion = leg1Result.AccessToken, + TokenBindingCertificate = leg1Result.BindingCertificate // ← Bind with Leg 1's cert + }); + }) + .Build(); + +var leg2Result = await leg2App + .AcquireTokenForClient(new[] { "https://management.azure.com/.default" }) + .WithMtlsProofOfPossession() + .ExecuteAsync(); + +Console.WriteLine($"Leg 2 Token Type: {leg2Result.TokenType}"); // "mtls_pop" +``` + +## Production Helper Classes + +This skill includes four production-ready helper classes: + +### 1. FicLeg1Acquirer.cs +Handles Leg 1 token acquisition for both MSI and Confidential Client with attestation support. + +### 2. FicAssertionProvider.cs +Builds `ClientSignedAssertion` from Leg 1 token with optional certificate binding. + +### 3. FicLeg2Exchanger.cs +Handles Leg 2 token exchange (Confidential Client only) with Bearer or PoP support. + +### 4. ResourceCaller.cs +Helper for calling protected resources with mTLS PoP tokens (reuses vanilla pattern). + +See the `.cs` files in this directory for complete implementations. + +## Usage Pattern + +```csharp +// 1. Leg 1: Acquire token for api://AzureADTokenExchange +var leg1Acquirer = new FicLeg1Acquirer(msiApp); // or confApp +var leg1Result = await leg1Acquirer.AcquireTokenAsync(); + +// 2. Build assertion from Leg 1 result +var assertionProvider = new FicAssertionProvider(leg1Result); +var assertion = assertionProvider.CreateAssertion( + bindCertificate: true); // true for PoP, false for Bearer + +// 3. Leg 2: Exchange for final resource +var leg2Exchanger = new FicLeg2Exchanger(leg2ConfApp, assertionProvider); +var leg2Result = await leg2Exchanger.ExchangeTokenAsync( + new[] { "https://graph.microsoft.com/.default" }, + requestMtlsPop: true); // true for PoP, false for Bearer + +// 4. Call resource with final token +if (leg2Result.TokenType == "mtls_pop") +{ + using var caller = new ResourceCaller(leg2Result); + string response = await caller.CallResourceAsync("https://mtlstb.graph.microsoft.com/v1.0/applications"); +} +``` + +## Key Points + +1. **Leg 1 always targets `api://AzureADTokenExchange`**: This is the FIC token exchange endpoint +2. **Leg 2 MUST be Confidential Client**: MSI does NOT have `WithClientAssertion()` API +3. **Four valid combinations**: All permutations of MSI/ConfApp Leg 1 × Bearer/PoP Leg 2 +4. **Always pass Leg 1's certificate**: Include `TokenBindingCertificate = leg1Result.BindingCertificate` in `ClientSignedAssertion` for all scenarios (both ****** PoP Leg 2) +5. **Always include `.WithAttestationSupport()`** in Leg 1 for production Credential Guard support +6. **Test slice region**: Use "westus3" for MSAL.NET integration tests + +## Why MSI Cannot Do Leg 2 + +The `IManagedIdentityApplication` interface does NOT provide a `WithClientAssertion()` method: +- MSI is designed for direct authentication only +- Assertion-based auth requires `IConfidentialClientApplication` +- This is a fundamental MSAL.NET API limitation, not a configuration issue + +## Troubleshooting + +### FIC Two-Leg Specific Issues + +| Error/Issue | Solution | +|-------------|----------| +| "MSI doesn't have WithClientAssertion" | Use Confidential Client for Leg 2 (MSI can only do Leg 1) | +| `ClientSignedAssertion` is not defined | Add `using Microsoft.Identity.Client.Extensibility;` | +| `ManagedIdentityId` is not defined | Add `using Microsoft.Identity.Client.AppConfig;` | +| `WithMtlsProofOfPossession()` not found | Upgrade to MSAL.NET 4.82.1+ | +| `WithAttestationSupport()` not found | Add NuGet: `Microsoft.Identity.Client.KeyAttestation` | +| Leg 2 cert mismatch | Ensure Leg 1's `BindingCertificate` is passed as `TokenBindingCertificate` | +| "urn:ietf:params:oauth:client-assertion-type:jwt-pop" error | Certificate binding is automatic when `TokenBindingCertificate` is set | + +### General Credential and Authentication Issues + +For certificate loading, FIC setup, token caching, error handling, and general troubleshooting, see: +- [Certificate Setup](../msal-confidential-auth/shared/credential-setup/certificate-setup.md) - Loading certificates from file, store, or Key Vault +- [Certificate SNI Setup](../msal-confidential-auth/shared/credential-setup/certificate-sni-setup.md) - SNI configuration details +- [Federated Identity Credentials Setup](../msal-confidential-auth/shared/credential-setup/federated-identity-credentials.md) - FIC configuration in Azure Portal +- [Error Handling Patterns](../msal-confidential-auth/shared/patterns/error-handling-patterns.md) - Common error scenarios +- [Troubleshooting](../msal-confidential-auth/shared/patterns/troubleshooting.md) - Comprehensive troubleshooting guide +- [Token Caching Strategies](../msal-confidential-auth/shared/patterns/token-caching-strategies.md) - Cache management best practices + +## Additional Resources + +- [Shared Guidance Skill](../msal-mtls-pop-guidance/SKILL.md) - Terminology and conventions +- [Vanilla Flow Skill](../msal-mtls-pop-vanilla/SKILL.md) - Direct token acquisition +- [ClientCredentialsMtlsPopTests.cs](../../../tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/ClientCredentialsMtlsPopTests.cs) - See `Sni_AssertionFlow_Uses_JwtPop_And_Succeeds_TestAsync` diff --git a/.github/skills/msal-mtls-pop-guidance/SKILL.md b/.github/skills/msal-mtls-pop-guidance/SKILL.md new file mode 100644 index 0000000000..e4081c68df --- /dev/null +++ b/.github/skills/msal-mtls-pop-guidance/SKILL.md @@ -0,0 +1,261 @@ +--- +skill_name: msal-mtls-pop-guidance +version: 1.0 +description: Shared terminology, conventions, and patterns for mTLS Proof-of-Possession (PoP) flows in MSAL.NET +applies_to: + - MSAL.NET/mTLS-PoP + - MSAL.NET/Managed-Identity + - MSAL.NET/Confidential-Client +tags: + - msal + - mtls + - pop + - proof-of-possession + - terminology + - conventions +--- + +# MSAL.NET mTLS PoP Guidance - Shared Terminology & Conventions + +This skill provides shared terminology, conventions, and patterns for working with mTLS Proof-of-Possession (PoP) flows in MSAL.NET. Use this as a reference when implementing or reviewing any mTLS PoP scenario. + +## Core Terminology + +### Authentication Methods + +**MSI (Managed Identity)** +- Cloud-native identity for Azure resources that eliminates credential management +- Two variants: + - **SAMI (System-Assigned Managed Identity)**: Automatically created with Azure resource, tied to resource lifecycle + - **UAMI (User-Assigned Managed Identity)**: Standalone identity that can be shared across multiple resources +- Works in: Azure VMs, App Service, Functions, Container Instances, AKS, Azure Arc +- **Limitation**: MSI does NOT have `WithClientAssertion()` API - cannot be used for Leg 2 in FIC flows + +**Confidential Client** +- Traditional application identity using certificates or secrets +- Uses `IConfidentialClientApplication` from MSAL.NET +- Required for: FIC Leg 2, local development, non-Azure environments +- Supports: Certificate-based SNI (Subject Name/Issuer) authentication + +### Flow Patterns + +**Vanilla Flow (Single-Step, No "Legs")** +- Direct token acquisition from Azure AD for a target resource +- One call: `AcquireTokenForManagedIdentity()` or `AcquireTokenForClient()` +- Example: Acquire token directly for `https://graph.microsoft.com` +- **Never** refer to vanilla flow as having "legs" - it's a single direct acquisition + +**FIC Two-Leg Flow (Token Exchange)** +- Two-step process using Federated Identity Credentials (workload identity) +- **Leg 1**: Acquire token for `api://AzureADTokenExchange` (MSI or Confidential Client) +- **Leg 2**: Exchange Leg 1 token for final target resource (Confidential Client ONLY) +- Used in: Kubernetes workload identity, multi-tenant scenarios, complex authentication chains + +### Token Types + +**Bearer Token** +- Standard OAuth 2.0 token type +- Sent as `Authorization: Bearer ` header +- No cryptographic binding to client + +**mTLS PoP Token** +- Proof-of-Possession token cryptographically bound to a certificate +- Prevents token theft/replay attacks +- Requires mTLS (mutual TLS) when calling target resource +- Token type in response: `"mtls_pop"` +- Enabled via `.WithMtlsProofOfPossession()` API + +### Key Concepts + +**SNI (Subject Name/Issuer)** +- Certificate authentication method using X.509 certificate subject and issuer +- Configured at app builder level: `.WithCertificate(cert, sendX5c: true)` +- Used with Confidential Client only + +**BindingCertificate** +- Certificate that was cryptographically bound to a PoP token +- Accessed via `AuthenticationResult.BindingCertificate` property +- Required for making mTLS calls to target resources +- In FIC Leg 2: Can reuse Leg 1's `BindingCertificate` by passing it as `TokenBindingCertificate` + +**Credential Guard Attestation** +- Windows security feature that protects credentials in virtualized containers +- Enabled via `.WithAttestationSupport()` API +- Requires: `Microsoft.Identity.Client.KeyAttestation` NuGet package +- Supported: MSI flows (SAMI, UAMI) and Confidential Client flows +- **Always include in production code** for enhanced security + +## UAMI Identifier Types + +User-Assigned Managed Identities can be specified using any of three ID types: + +### 1. Client ID (Application ID) +```csharp +ManagedIdentityId.WithUserAssignedClientId("6325cd32-9911-41f3-819c-416cdf9104e7") +``` +- Most commonly used +- Same as the "Application (client) ID" in Azure Portal + +### 2. Resource ID (ARM Path) +```csharp +ManagedIdentityId.WithUserAssignedResourceId( + "/subscriptions/c1686c51-b717-4fe0-9af3-24a20a41fb0c/resourcegroups/MSIV2-Testing-MSALNET/providers/Microsoft.ManagedIdentity/userAssignedIdentities/msiv2uami") +``` +- Full Azure Resource Manager path +- Useful in ARM templates or scripts + +### 3. Object ID (Principal ID) +```csharp +ManagedIdentityId.WithUserAssignedObjectId("ecb2ad92-3e30-4505-b79f-ac640d069f24") +``` +- Azure AD object ID of the managed identity +- Same as the "Object (principal) ID" in Azure Portal + +**Note**: All three types refer to the same identity and are functionally equivalent. Use whichever is most convenient for your scenario. + +## FIC Two-Leg Flow - Valid Combinations + +### Four Valid Scenarios + +| Leg 1 Auth Method | Leg 1 Token Type | Leg 2 Auth Method | Leg 2 Token Type | Valid? | +|-------------------|------------------|-------------------|------------------|--------| +| MSI | mTLS PoP | Confidential Client | Bearer | ✅ Yes | +| MSI | mTLS PoP | Confidential Client | mTLS PoP | ✅ Yes | +| Confidential Client | mTLS PoP | Confidential Client | Bearer | ✅ Yes | +| Confidential Client | mTLS PoP | Confidential Client | mTLS PoP | ✅ Yes | +| MSI | mTLS PoP | **MSI** | Any | ❌ **NO** - MSI lacks WithClientAssertion | + +### Key Rules +1. **Leg 1** can use MSI or Confidential Client +2. **Leg 2 MUST be Confidential Client** - MSI cannot perform assertion-based authentication +3. Leg 2 can request ****** mTLS PoP final token +4. **Always pass Leg 1's certificate**: Include `TokenBindingCertificate = leg1Result.BindingCertificate` in `ClientSignedAssertion` for all scenarios (both ****** PoP Leg 2) + +## Required Namespaces + +Always include these namespaces in mTLS PoP code: + +```csharp +using System; +using System.Net.Http; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Identity.Client; +using Microsoft.Identity.Client.AppConfig; // ← For ManagedIdentityId +using Microsoft.Identity.Client.KeyAttestation; // ← For WithAttestationSupport() +``` + +## Version Requirements + +- **MSAL.NET**: 4.82.1 minimum (earlier versions lack PoP + attestation APIs) +- **Target Framework**: net8.0 recommended (LTS, best performance) +- **NuGet Packages**: + ```bash + dotnet add package Microsoft.Identity.Client --version 4.82.1 + dotnet add package Microsoft.Identity.Client.KeyAttestation + ``` + +## Code Conventions + +All helper classes and examples follow MSAL.NET conventions: + +1. **Async/Await**: Use `ConfigureAwait(false)` on all awaits +2. **Cancellation**: Accept `CancellationToken` with default `= default` +3. **Disposal**: Implement `IDisposable` with `_disposed` flag +4. **Validation**: Use `ArgumentNullException.ThrowIfNull()` for inputs +5. **Disposal Checks**: Use `ObjectDisposedException.ThrowIf()` before operations + +### Example Pattern +```csharp +public async Task AcquireTokenAsync( + string resource, + CancellationToken cancellationToken = default) +{ + ArgumentNullException.ThrowIfNull(resource); + ObjectDisposedException.ThrowIf(_disposed, this); + + var result = await _app + .AcquireTokenForManagedIdentity(resource) + .WithMtlsProofOfPossession() + .WithAttestationSupport() + .ExecuteAsync(cancellationToken) + .ConfigureAwait(false); + + return result; +} +``` + +## Reviewer Expectations + +When reviewing mTLS PoP code, check for: + +### Must Have +- [ ] MSAL.NET version 4.82.1 or later documented +- [ ] `.WithMtlsProofOfPossession()` called on token requests +- [ ] `.WithAttestationSupport()` included (production code) +- [ ] Complete namespace declarations (including `AppConfig` and `KeyAttestation`) +- [ ] Correct flow terminology (vanilla vs FIC two-leg, no "legs" in vanilla) +- [ ] MSI limitation documented (no WithClientAssertion for Leg 2) +- [ ] All 3 UAMI ID types shown in examples + +### Should Have +- [ ] `ConfigureAwait(false)` on all awaits +- [ ] `CancellationToken` parameters with defaults +- [ ] Proper `IDisposable` implementation +- [ ] Input validation with `ArgumentNullException.ThrowIfNull` +- [ ] Disposal checks with `ObjectDisposedException.ThrowIf` +- [ ] Certificate null checks after PoP acquisition +- [ ] Proper HttpClient disposal patterns + +### Common Mistakes to Avoid +- ❌ Using MSI for FIC Leg 2 (doesn't have WithClientAssertion) +- ❌ Referring to vanilla flow as having "legs" +- ❌ Missing `using Microsoft.Identity.Client.AppConfig;` +- ❌ Forgetting `.WithAttestationSupport()` in production code +- ❌ Using MSAL version < 4.82.1 +- ❌ Not checking `BindingCertificate` for null +- ❌ Disposing RSA keys from `GetRSAPrivateKey()` (handled by cert) + +## Testing Guidance + +### Local Development +- **SAMI**: Not available locally (requires Azure environment) +- **UAMI**: Not available locally without special setup +- **Confidential Client**: Works locally with certificate from Windows Certificate Store + +### Azure Environments +- **SAMI**: Azure VM, App Service, Functions, Container Instances, AKS +- **UAMI**: Same as SAMI, plus requires UAMI assignment to resource +- **Region**: Use actual region (e.g., "westus3") for SNI scenarios + +### Test Slice Region +For MSAL.NET integration tests, the test slice region is **westus3**. + +## Troubleshooting Quick Reference + +### mTLS PoP-Specific Issues + +| Error/Issue | Solution | +|-------------|----------| +| `ManagedIdentityId` is not defined | Add `using Microsoft.Identity.Client.AppConfig;` | +| `WithMtlsProofOfPossession()` not found | Upgrade to MSAL.NET 4.82.1+ | +| `BindingCertificate` is null | Ensure `.WithMtlsProofOfPossession()` was called | +| `WithAttestationSupport()` not found | Add `Microsoft.Identity.Client.KeyAttestation` NuGet | +| IMDS timeout (local machine) | Use UAMI or Confidential Client for local dev | +| Unable to get UAMI token | Check UAMI exists, assigned to resource, correct ID type | + +### General Credential and Authentication Issues + +For comprehensive troubleshooting, certificate setup, error handling, and token caching guidance, see: +- [Troubleshooting Guide](../msal-confidential-auth/shared/patterns/troubleshooting.md) - Comprehensive troubleshooting for all credential types +- [Certificate Setup](../msal-confidential-auth/shared/credential-setup/certificate-setup.md) - Certificate loading and validation +- [Error Handling Patterns](../msal-confidential-auth/shared/patterns/error-handling-patterns.md) - Common error scenarios and solutions +- [Token Caching Strategies](../msal-confidential-auth/shared/patterns/token-caching-strategies.md) - Cache management best practices + +## Additional Resources + +- [Vanilla Flow Skill](../msal-mtls-pop-vanilla/SKILL.md) +- [FIC Two-Leg Flow Skill](../msal-mtls-pop-fic-two-leg/SKILL.md) +- [MSAL.NET mTLS PoP Integration Tests](../../../tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/ClientCredentialsMtlsPopTests.cs) +- [MSAL.NET Managed Identity E2E Tests](../../../tests/Microsoft.Identity.Test.E2e/) diff --git a/.github/skills/msal-mtls-pop-vanilla/MtlsPopTokenAcquirer.cs b/.github/skills/msal-mtls-pop-vanilla/MtlsPopTokenAcquirer.cs new file mode 100644 index 0000000000..107c6d73e9 --- /dev/null +++ b/.github/skills/msal-mtls-pop-vanilla/MtlsPopTokenAcquirer.cs @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Identity.Client; + +namespace MsalMtlsPop.Vanilla +{ + /// + /// Unified helper for acquiring mTLS PoP tokens with Credential Guard attestation support. + /// Supports both Managed Identity and Confidential Client authentication methods. + /// + /// + /// Production-ready implementation following MSAL.NET conventions: + /// - ConfigureAwait(false) on all awaits + /// - CancellationToken support with defaults + /// - Proper IDisposable implementation + /// - Input validation and disposal checks + /// + public sealed class MtlsPopTokenAcquirer : IDisposable + { + private readonly IManagedIdentityApplication _msiApp; + private readonly IConfidentialClientApplication _confApp; + private readonly bool _isManagedIdentity; + private bool _disposed; + + /// + /// Creates an acquirer for Managed Identity scenarios. + /// + /// Configured Managed Identity application. + /// Thrown when msiApp is null. + public MtlsPopTokenAcquirer(IManagedIdentityApplication msiApp) + { + ArgumentNullException.ThrowIfNull(msiApp); + + _msiApp = msiApp; + _isManagedIdentity = true; + } + + /// + /// Creates an acquirer for Confidential Client scenarios. + /// + /// Configured Confidential Client application. + /// Thrown when confApp is null. + public MtlsPopTokenAcquirer(IConfidentialClientApplication confApp) + { + ArgumentNullException.ThrowIfNull(confApp); + + _confApp = confApp; + _isManagedIdentity = false; + } + + /// + /// Acquires an mTLS PoP token for the specified resource with Credential Guard attestation. + /// + /// Target resource URI (e.g., "https://graph.microsoft.com"). + /// Cancellation token for the operation. + /// Authentication result containing mTLS PoP token and binding certificate. + /// Thrown when resource is null. + /// Thrown when the acquirer has been disposed. + /// Thrown when token acquisition fails. + public async Task AcquireTokenAsync( + string resource, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(resource); + ObjectDisposedException.ThrowIf(_disposed, this); + + AuthenticationResult result; + + if (_isManagedIdentity) + { + // MSI path with attestation support + result = await _msiApp + .AcquireTokenForManagedIdentity(resource) + .WithMtlsProofOfPossession() + .WithAttestationSupport() // Credential Guard attestation + .ExecuteAsync(cancellationToken) + .ConfigureAwait(false); + } + else + { + // Confidential Client path + string[] scopes = resource.EndsWith("/.default", StringComparison.OrdinalIgnoreCase) + ? new[] { resource } + : new[] { $"{resource}/.default" }; + + result = await _confApp + .AcquireTokenForClient(scopes) + .WithMtlsProofOfPossession() + .ExecuteAsync(cancellationToken) + .ConfigureAwait(false); + } + + // Validate result + if (result.BindingCertificate == null) + { + throw new InvalidOperationException( + "BindingCertificate is null after token acquisition. " + + "This should not happen if .WithMtlsProofOfPossession() was called correctly."); + } + + return result; + } + + /// + /// Disposes the acquirer. Note that the underlying MSAL application + /// instances are NOT disposed, as they may be reused elsewhere. + /// + public void Dispose() + { + if (_disposed) + { + return; + } + + // Note: We don't dispose _msiApp or _confApp as they may be reused + // Caller is responsible for disposing those when appropriate + _disposed = true; + } + } +} diff --git a/.github/skills/msal-mtls-pop-vanilla/ResourceCaller.cs b/.github/skills/msal-mtls-pop-vanilla/ResourceCaller.cs new file mode 100644 index 0000000000..f23ac8e1d0 --- /dev/null +++ b/.github/skills/msal-mtls-pop-vanilla/ResourceCaller.cs @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Identity.Client; + +namespace MsalMtlsPop.Vanilla +{ + /// + /// Helper class for calling protected resources using mTLS PoP tokens. + /// Handles HttpClient configuration with mTLS binding certificate. + /// + /// + /// Production-ready implementation following MSAL.NET conventions: + /// - ConfigureAwait(false) on all awaits + /// - CancellationToken support with defaults + /// - Proper IDisposable implementation + /// - Input validation and disposal checks + /// + public sealed class ResourceCaller : IDisposable + { + private readonly HttpClient _httpClient; + private readonly string _accessToken; + private readonly string _tokenType; + private bool _disposed; + + /// + /// Creates a ResourceCaller from an mTLS PoP authentication result. + /// + /// Authentication result containing mTLS PoP token and binding certificate. + /// Thrown when authResult is null. + /// Thrown when token type is not mtls_pop or BindingCertificate is null. + public ResourceCaller(AuthenticationResult authResult) + { + ArgumentNullException.ThrowIfNull(authResult); + + if (string.IsNullOrEmpty(authResult.TokenType) || + !authResult.TokenType.Equals("mtls_pop", StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException( + $"Expected token type 'mtls_pop', but got '{authResult.TokenType}'. " + + "Ensure .WithMtlsProofOfPossession() was called during token acquisition.", + nameof(authResult)); + } + + if (authResult.BindingCertificate == null) + { + throw new ArgumentException( + "BindingCertificate is null. This certificate is required for mTLS calls. " + + "Ensure .WithMtlsProofOfPossession() was called before ExecuteAsync().", + nameof(authResult)); + } + + _accessToken = authResult.AccessToken; + _tokenType = authResult.TokenType; + + // Configure HttpClientHandler with mTLS binding certificate + var handler = new HttpClientHandler(); + handler.ClientCertificates.Add(authResult.BindingCertificate); + + _httpClient = new HttpClient(handler); + + // Set mTLS PoP authorization header + _httpClient.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("mtls_pop", _accessToken); + } + + /// + /// Calls a protected resource endpoint with mTLS PoP authentication. + /// + /// The URI of the protected resource to call. + /// Cancellation token for the operation. + /// Response body as a string. + /// Thrown when resourceUri is null. + /// Thrown when the caller has been disposed. + /// Thrown when the HTTP request fails. + public async Task CallResourceAsync( + string resourceUri, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(resourceUri); + ObjectDisposedException.ThrowIf(_disposed, this); + + var response = await _httpClient + .GetAsync(resourceUri, cancellationToken) + .ConfigureAwait(false); + + response.EnsureSuccessStatusCode(); + + var content = await response.Content + .ReadAsStringAsync(cancellationToken) + .ConfigureAwait(false); + + return content; + } + + /// + /// Calls a protected resource endpoint with mTLS PoP authentication and returns the full response. + /// + /// The URI of the protected resource to call. + /// Cancellation token for the operation. + /// The complete HTTP response message. + /// Thrown when resourceUri is null. + /// Thrown when the caller has been disposed. + public async Task CallResourceFullResponseAsync( + string resourceUri, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(resourceUri); + ObjectDisposedException.ThrowIf(_disposed, this); + + var response = await _httpClient + .GetAsync(resourceUri, cancellationToken) + .ConfigureAwait(false); + + return response; + } + + /// + /// Disposes the ResourceCaller and its underlying HttpClient. + /// + public void Dispose() + { + if (_disposed) + { + return; + } + + _httpClient?.Dispose(); + _disposed = true; + } + } +} diff --git a/.github/skills/msal-mtls-pop-vanilla/SKILL.md b/.github/skills/msal-mtls-pop-vanilla/SKILL.md new file mode 100644 index 0000000000..5efcb9dfae --- /dev/null +++ b/.github/skills/msal-mtls-pop-vanilla/SKILL.md @@ -0,0 +1,366 @@ +--- +skill_name: msal-mtls-pop-vanilla +version: 1.0 +description: Direct mTLS PoP token acquisition for target resources using Managed Identity or Confidential Client +applies_to: + - MSAL.NET/mTLS-PoP + - MSAL.NET/Managed-Identity + - MSAL.NET/Confidential-Client +tags: + - msal + - mtls + - pop + - proof-of-possession + - managed-identity + - confidential-client + - vanilla-flow +--- + +# MSAL.NET mTLS PoP - Vanilla Flow (Direct Token Acquisition) + +This skill covers direct mTLS Proof-of-Possession (PoP) token acquisition for target resources without intermediate token exchanges. Use this when you need to acquire an mTLS PoP token directly for a resource like Microsoft Graph, Azure Key Vault, or custom APIs. + +> **Note:** This skill focuses on mTLS PoP-specific APIs and patterns. For general credential setup (certificates, FIC, etc.), see the [Confidential Auth Skill](../msal-confidential-auth/shared/credential-setup/) for reusable, granularized patterns. + +## What is Vanilla Flow? + +**Vanilla flow** is a single-step, direct token acquisition from Azure AD for a target resource: +- **One call**: `AcquireTokenForManagedIdentity()` or `AcquireTokenForClient()` +- **No intermediate steps**: Direct to target resource (e.g., `https://graph.microsoft.com`) +- **No "legs"**: This is NOT a multi-step process (do not confuse with FIC two-leg flow) + +## Authentication Methods Supported + +### 1. System-Assigned Managed Identity (SAMI) +Works in Azure environments only (VM, App Service, Functions, Container Instances, AKS). + +### 2. User-Assigned Managed Identity (UAMI) +Can be specified using any of three ID types (all refer to the same identity): + +### 3. Confidential Client with Certificate (SNI) +Works anywhere with certificate access (local dev, Azure, on-premises). + +## Requirements + +- **MSAL.NET**: 4.82.1 minimum +- **NuGet Packages**: + ```bash + dotnet add package Microsoft.Identity.Client --version 4.82.1 + dotnet add package Microsoft.Identity.Client.KeyAttestation + ``` +- **Target Framework**: net8.0 recommended +- **Namespaces**: + ```csharp + using Microsoft.Identity.Client; + using Microsoft.Identity.Client.AppConfig; // For ManagedIdentityId + using Microsoft.Identity.Client.KeyAttestation; // For WithAttestationSupport() + ``` + +## Quick Start Examples + +### SAMI (System-Assigned Managed Identity) + +```csharp +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(); + +Console.WriteLine($"Token Type: {result.TokenType}"); // "mtls_pop" +Console.WriteLine($"Certificate Thumbprint: {result.BindingCertificate?.Thumbprint}"); + +// 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 +var response = await httpClient.GetAsync("https://mtlstb.graph.microsoft.com/v1.0/applications"); +response.EnsureSuccessStatusCode(); + +string json = await response.Content.ReadAsStringAsync(); +Console.WriteLine(json); +``` + +### UAMI by Client ID + +```csharp +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 UAMI app with Client ID +var app = ManagedIdentityApplicationBuilder.Create( + ManagedIdentityId.WithUserAssignedClientId("6325cd32-9911-41f3-819c-416cdf9104e7")) + .Build(); + +// Acquire mTLS PoP token +var result = await app + .AcquireTokenForManagedIdentity("https://vault.azure.net") + .WithMtlsProofOfPossession() + .WithAttestationSupport() + .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 Azure Key Vault +var response = await httpClient.GetAsync("https://your-vault.vault.azure.net/secrets/my-secret?api-version=7.4"); +response.EnsureSuccessStatusCode(); + +string json = await response.Content.ReadAsStringAsync(); +Console.WriteLine(json); +``` + +### UAMI by Resource ID + +```csharp +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 UAMI app with Resource ID (ARM path) +var app = ManagedIdentityApplicationBuilder.Create( + ManagedIdentityId.WithUserAssignedResourceId( + "/subscriptions/c1686c51-b717-4fe0-9af3-24a20a41fb0c/resourcegroups/MSIV2-Testing-MSALNET/providers/Microsoft.ManagedIdentity/userAssignedIdentities/msiv2uami")) + .Build(); + +// Acquire mTLS PoP token +var result = await app + .AcquireTokenForManagedIdentity("https://storage.azure.com") + .WithMtlsProofOfPossession() + .WithAttestationSupport() + .ExecuteAsync(); + +Console.WriteLine($"Token Type: {result.TokenType}"); // "mtls_pop" +Console.WriteLine($"Certificate Thumbprint: {result.BindingCertificate?.Thumbprint}"); + +// 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 Azure Storage +var response = await httpClient.GetAsync("https://your-storage-account.blob.core.windows.net/?comp=list"); +response.EnsureSuccessStatusCode(); + +string json = await response.Content.ReadAsStringAsync(); +Console.WriteLine(json); +``` + +### UAMI by Object ID + +```csharp +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 UAMI app with Object ID (Principal ID) +var app = ManagedIdentityApplicationBuilder.Create( + ManagedIdentityId.WithUserAssignedObjectId("ecb2ad92-3e30-4505-b79f-ac640d069f24")) + .Build(); + +// Acquire mTLS PoP token +var result = await app + .AcquireTokenForManagedIdentity("https://management.azure.com") + .WithMtlsProofOfPossession() + .WithAttestationSupport() + .ExecuteAsync(); + +Console.WriteLine($"Token Type: {result.TokenType}"); // "mtls_pop" +Console.WriteLine($"Certificate Thumbprint: {result.BindingCertificate?.Thumbprint}"); + +// 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 Azure Resource Manager +var response = await httpClient.GetAsync("https://management.azure.com/subscriptions?api-version=2021-04-01"); +response.EnsureSuccessStatusCode(); + +string json = await response.Content.ReadAsStringAsync(); +Console.WriteLine(json); +``` + +### Confidential Client with Certificate (SNI) + +```csharp +using System; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; +using Microsoft.Identity.Client; +using Microsoft.Identity.Client.KeyAttestation; + +// Load certificate - see ../msal-confidential-auth/shared/credential-setup/certificate-setup.md for details +var cert = GetCertificateFromStore("CN=MyAppCertificate"); + +// Build Confidential Client with SNI - see ../msal-confidential-auth/shared/credential-setup/certificate-sni-setup.md +var app = ConfidentialClientApplicationBuilder + .Create("your-client-id") + .WithAuthority("https://login.microsoftonline.com/your-tenant-id") + .WithAzureRegion("westus3") // Use actual region + .WithCertificate(cert, sendX5c: true) // SNI: send X.509 chain + .Build(); + +// Acquire mTLS PoP token +var result = await app + .AcquireTokenForClient(new[] { "https://graph.microsoft.com/.default" }) + .WithMtlsProofOfPossession() + .ExecuteAsync(); + +Console.WriteLine($"Token Type: {result.TokenType}"); // "mtls_pop" +Console.WriteLine($"Binding Certificate matches SNI cert: {result.BindingCertificate.Thumbprint == cert.Thumbprint}"); + +// 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 +var response = await httpClient.GetAsync("https://mtlstb.graph.microsoft.com/v1.0/applications"); +response.EnsureSuccessStatusCode(); + +string json = await response.Content.ReadAsStringAsync(); +Console.WriteLine(json); +``` + +## Production Helper Classes + +This skill includes three production-ready helper classes: + +### 1. VanillaMsiMtlsPop.cs +Complete MSI implementation supporting SAMI and all 3 UAMI ID types with Credential Guard attestation. + +### 2. MtlsPopTokenAcquirer.cs +Unified token acquisition for both MSI and Confidential Client with attestation support. + +### 3. ResourceCaller.cs +Helper for calling protected resources with mTLS PoP tokens. + +See the `.cs` files in this directory for complete implementations. + +## Usage Pattern + +```csharp +// 1. Acquire token with PoP +var result = await app + .AcquireTokenForManagedIdentity("https://graph.microsoft.com") + .WithMtlsProofOfPossession() + .WithAttestationSupport() + .ExecuteAsync(); + +// 2. Verify PoP token +if (result.TokenType != "mtls_pop") +{ + throw new InvalidOperationException("Expected mTLS PoP token"); +} + +if (result.BindingCertificate == null) +{ + throw new InvalidOperationException("BindingCertificate is required for mTLS calls"); +} + +// 3. Call resource with mTLS binding +using var caller = new ResourceCaller(result); +string response = await caller.CallResourceAsync("https://mtlstb.graph.microsoft.com/v1.0/applications"); +``` + +## Key Points + +1. **Vanilla flow is NOT multi-step**: Direct acquisition, no "legs" +2. **Always include `.WithAttestationSupport()`**: Required for Credential Guard in production +3. **SAMI only works in Azure**: Use UAMI or Confidential Client for local development +4. **All 3 UAMI ID types are equivalent**: Use whichever is most convenient +5. **Check `BindingCertificate` for null**: Required for making mTLS calls to target resource +6. **SNI requires `sendX5c: true`**: In `.WithCertificate(cert, sendX5c: true)` +7. **Use actual Azure region**: e.g., "westus3", not placeholders + +## Troubleshooting + +### mTLS PoP-Specific Issues + +| Error/Issue | Solution | +|-------------|----------| +| `ManagedIdentityId` is not defined | Add `using Microsoft.Identity.Client.AppConfig;` | +| `WithMtlsProofOfPossession()` not found | Upgrade to MSAL.NET 4.82.1+ | +| `BindingCertificate` is null | Ensure `.WithMtlsProofOfPossession()` was called before `ExecuteAsync()` | +| `WithAttestationSupport()` not found | Add NuGet: `Microsoft.Identity.Client.KeyAttestation` | +| "Timeout calling IMDS endpoint" (local) | SAMI doesn't work locally. Use UAMI or Confidential Client | +| "Unable to get UAMI token" | Check: UAMI exists, assigned to resource, correct ID type used | + +### General Credential and Authentication Issues + +For certificate loading, token caching, error handling, and general troubleshooting, see: +- [Certificate Setup](../msal-confidential-auth/shared/credential-setup/certificate-setup.md) - Loading certificates from file, store, or Key Vault +- [Certificate SNI Setup](../msal-confidential-auth/shared/credential-setup/certificate-sni-setup.md) - SNI configuration details +- [Error Handling Patterns](../msal-confidential-auth/shared/patterns/error-handling-patterns.md) - Common error scenarios +- [Troubleshooting](../msal-confidential-auth/shared/patterns/troubleshooting.md) - Comprehensive troubleshooting guide +- [Token Caching Strategies](../msal-confidential-auth/shared/patterns/token-caching-strategies.md) - Cache management best practices + +## Additional Resources + +- [Shared Guidance Skill](../msal-mtls-pop-guidance/SKILL.md) - Terminology and conventions +- [FIC Two-Leg Flow Skill](../msal-mtls-pop-fic-two-leg/SKILL.md) - Token exchange scenarios +- [ClientCredentialsMtlsPopTests.cs](../../../tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/ClientCredentialsMtlsPopTests.cs) - Integration test examples diff --git a/.github/skills/msal-mtls-pop-vanilla/VanillaMsiMtlsPop.cs b/.github/skills/msal-mtls-pop-vanilla/VanillaMsiMtlsPop.cs new file mode 100644 index 0000000000..8fc19a2bf6 --- /dev/null +++ b/.github/skills/msal-mtls-pop-vanilla/VanillaMsiMtlsPop.cs @@ -0,0 +1,148 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Identity.Client; +using Microsoft.Identity.Client.AppConfig; + +namespace MsalMtlsPop.Vanilla +{ + /// + /// Production implementation of vanilla mTLS PoP flow using Managed Identity. + /// Supports SAMI (System-Assigned) and all 3 UAMI (User-Assigned) identifier types. + /// + /// + /// Example IDs used are from PR #5726 E2E tests: + /// - UAMI Client ID: 6325cd32-9911-41f3-819c-416cdf9104e7 + /// - UAMI Resource ID: /subscriptions/c1686c51-b717-4fe0-9af3-24a20a41fb0c/resourcegroups/MSIV2-Testing-MSALNET/providers/Microsoft.ManagedIdentity/userAssignedIdentities/msiv2uami + /// - UAMI Object ID: ecb2ad92-3e30-4505-b79f-ac640d069f24 + /// + public sealed class VanillaMsiMtlsPop + { + /// + /// Acquires an mTLS PoP token using System-Assigned Managed Identity (SAMI). + /// + /// Target resource URI (e.g., "https://graph.microsoft.com"). + /// Cancellation token for the operation. + /// Authentication result containing mTLS PoP token and binding certificate. + /// Thrown when token acquisition fails. + /// + /// SAMI only works in Azure environments (VM, App Service, Functions, AKS, etc.). + /// For local development, use UAMI or Confidential Client instead. + /// + public static async Task AcquireTokenWithSamiAsync( + string resource, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(resource); + + var app = ManagedIdentityApplicationBuilder.Create( + ManagedIdentityId.SystemAssigned) + .Build(); + + var result = await app + .AcquireTokenForManagedIdentity(resource) + .WithMtlsProofOfPossession() + .WithAttestationSupport() // Credential Guard attestation + .ExecuteAsync(cancellationToken) + .ConfigureAwait(false); + + return result; + } + + /// + /// Acquires an mTLS PoP token using User-Assigned Managed Identity by Client ID. + /// + /// UAMI Client ID (Application ID), e.g., "6325cd32-9911-41f3-819c-416cdf9104e7". + /// Target resource URI (e.g., "https://vault.azure.net"). + /// Cancellation token for the operation. + /// Authentication result containing mTLS PoP token and binding certificate. + /// Thrown when token acquisition fails. + public static async Task AcquireTokenWithUamiByClientIdAsync( + string clientId, + string resource, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(clientId); + ArgumentNullException.ThrowIfNull(resource); + + var app = ManagedIdentityApplicationBuilder.Create( + ManagedIdentityId.WithUserAssignedClientId(clientId)) + .Build(); + + var result = await app + .AcquireTokenForManagedIdentity(resource) + .WithMtlsProofOfPossession() + .WithAttestationSupport() // Credential Guard attestation + .ExecuteAsync(cancellationToken) + .ConfigureAwait(false); + + return result; + } + + /// + /// Acquires an mTLS PoP token using User-Assigned Managed Identity by Resource ID. + /// + /// UAMI Azure Resource Manager path. + /// Target resource URI (e.g., "https://storage.azure.com"). + /// Cancellation token for the operation. + /// Authentication result containing mTLS PoP token and binding certificate. + /// Thrown when token acquisition fails. + /// + /// Example Resource ID: "/subscriptions/{sub-id}/resourcegroups/{rg-name}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identity-name}" + /// + public static async Task AcquireTokenWithUamiByResourceIdAsync( + string resourceId, + string resource, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(resourceId); + ArgumentNullException.ThrowIfNull(resource); + + var app = ManagedIdentityApplicationBuilder.Create( + ManagedIdentityId.WithUserAssignedResourceId(resourceId)) + .Build(); + + var result = await app + .AcquireTokenForManagedIdentity(resource) + .WithMtlsProofOfPossession() + .WithAttestationSupport() // Credential Guard attestation + .ExecuteAsync(cancellationToken) + .ConfigureAwait(false); + + return result; + } + + /// + /// Acquires an mTLS PoP token using User-Assigned Managed Identity by Object ID. + /// + /// UAMI Object ID (Principal ID), e.g., "ecb2ad92-3e30-4505-b79f-ac640d069f24". + /// Target resource URI (e.g., "https://management.azure.com"). + /// Cancellation token for the operation. + /// Authentication result containing mTLS PoP token and binding certificate. + /// Thrown when token acquisition fails. + public static async Task AcquireTokenWithUamiByObjectIdAsync( + string objectId, + string resource, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(objectId); + ArgumentNullException.ThrowIfNull(resource); + + var app = ManagedIdentityApplicationBuilder.Create( + ManagedIdentityId.WithUserAssignedObjectId(objectId)) + .Build(); + + var result = await app + .AcquireTokenForManagedIdentity(resource) + .WithMtlsProofOfPossession() + .WithAttestationSupport() // Credential Guard attestation + .ExecuteAsync(cancellationToken) + .ConfigureAwait(false); + + return result; + } + } +}