diff --git a/docs/design/capab1.png b/design-docs/capab1.png similarity index 100% rename from docs/design/capab1.png rename to design-docs/capab1.png diff --git a/docs/design/managed_identity_capabilities_devex.md b/design-docs/managed_identity_capabilities_devex.md similarity index 100% rename from docs/design/managed_identity_capabilities_devex.md rename to design-docs/managed_identity_capabilities_devex.md diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..6c6a81e88 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,285 @@ +# Microsoft.Identity.Web Documentation + +Microsoft.Identity.Web is a set of libraries that simplifies adding authentication and authorization support to services (confidential client applications) integrating with the Microsoft identity platform (formerly Azure AD v2.0). It supports: + +- **ASP.NET Core** web applications and web APIs +- **OWIN** applications on .NET Framework +- **.NET** daemon applications and background services + +Whether you're building web apps that sign in users, web APIs that validate tokens, or background services that call protected APIs, Microsoft.Identity.Web handles the authentication complexity for you, including the client credentials. + +## π Quick Start + +Choose your scenario: + +- **[Web App - Sign in users](./getting-started/quickstart-webapp.md)** - Add authentication to your ASP.NET Core web application +- **[Web API - Protect your API](./getting-started/quickstart-webapi.md)** - Secure your ASP.NET Core web API with bearer tokens +- **[Daemon App - Call APIs](./getting-started/daemon-app.md)** - Build background services that call protected APIs + +## π¦ What's Included + +Microsoft.Identity.Web provides: + +β **Simplified Authentication** - Minimal configuration for signing in users and validating tokens +β **Downstream API Calls** - Call Microsoft Graph, Azure SDKs, or your own protected APIs with automatic token management + - **Token Acquisition** - Acquire tokens on behalf of users or your application + - **Token Cache Management** - Distributed cache support with Redis, SQL Server, Cosmos DB +β **Multiple Credential Types** - Support for certificates, managed identities, and certificateless authentication +β **Automatic Authorization Headers** - Authentication is handled transparently when calling APIs +β **Production-Ready** - Used by thousands of Microsoft and customer applications + +See **[NuGet Packages](./getting-started/packages.md)** - Overview of all available packages and when to use them. + +### Calling APIs with Automatic Authentication + +Microsoft.Identity.Web makes it easy to call protected APIs without manually managing tokens: + +- **Microsoft Graph** - Use `GraphServiceClient` with automatic token acquisition +- **Azure SDKs** - Use `TokenCredential` implementations that integrate with Microsoft.Identity.Web +- **Your Own APIs** - Use `IDownstreamApi` or `IAuthorizationHeaderProvider` for seamless API calls +- **Agent Identity APIs** - Call APIs on behalf of managed identities or service principals with automatic credential handling + +Authentication headers are automatically added to your requests, and tokens are acquired and cached transparently. See the [Calling Downstream APIs documentation](./calling-downstream-apis/calling-downstream-apis-README.md) and [Daemon Applications](./getting-started/daemon-app.md) and [Agent Identities guide](./calling-downstream-apis/AgentIdentities-Readme.md) for complete details. + +## βοΈ Configuration Approaches + +Microsoft.Identity.Web supports flexible configuration for all scenarios: + +### Configuration by File (Recommended) + +All scenarios can be configured using `appsettings.json`: + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id" + } +} +``` + +**Important for daemon apps and console applications:** Ensure your `appsettings.json` file is copied to the output directory. In Visual Studio, set the **"Copy to Output Directory"** property to **"Copy if newer"** or **"Copy always"**, or add this to your `.csproj`: + +```xml + + + PreserveNewest + + +``` + +### Configuration by Code + +You can also configure authentication programmatically: + +```csharp +builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(options => + { + options.Instance = "https://login.microsoftonline.com/"; + options.TenantId = "your-tenant-id"; + options.ClientId = "your-client-id"; + }); +``` + +Both approaches are available for all scenarios (web apps, web APIs, and daemon applications). + +## π― Core Scenarios + +### Web Applications + +Build web apps that sign in users with work/school accounts or personal Microsoft accounts. + +```csharp +// In Program.cs +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.Identity.Web; + +var builder = WebApplication.CreateBuilder(args); + +// Add authentication with explicit scheme +builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd")); + +builder.Services.AddRazorPages(); +``` + +**Learn more:** [Web Apps Scenario](./getting-started/quickstart-webapp.md) + +### Protected Web APIs + +Secure your APIs and validate access tokens from clients. + +```csharp +// In Program.cs +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Identity.Web; + +var builder = WebApplication.CreateBuilder(args); + +// Add authentication with explicit scheme +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")); + +builder.Services.AddControllers(); +``` + +**Learn more:** [Web APIs Scenario](./getting-started/quickstart-webapi.md) + +### Daemon Applications + +Build background services, console apps, and autonomous agents that call APIs using application identity or agent identities. + +```csharp +// In Program.cs +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Web; + +// Get the Token acquirer factory instance +var tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance(); + +// Configure downstream API +tokenAcquirerFactory.Services.AddDownstreamApi("MyApi", + tokenAcquirerFactory.Configuration.GetSection("MyWebApi")); + +var sp = tokenAcquirerFactory.Build(); + +// Call API - authentication is automatic +var api = sp.GetRequiredService(); +var result = await api.GetForAppAsync>("MyApi"); +``` + +**Configuration (appsettings.json):** + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + "ClientCredentials": [ + { + "SourceType": "ClientSecret", + "ClientSecret": "your-client-secret" + } + ] + }, + "MyWebApi": { + "BaseUrl": "https://myapi.example.com/", + "RelativePath": "api/data", + "RequestAppToken": true, + "Scopes": [ "api://your-api-id/.default" ] + } +} +``` + +> **Note:** `ClientCredentials` supports multiple authentication methods including certificates, Key Vault, managed identities, and certificateless authentication (FIC+MSI). See the [Credentials Guide](./authentication/credentials/credentials-README.md) for all options. + +**Supported Scenarios:** +- **Standard Daemon** - Client credentials for app-only tokens +- **Autonomous Agents** - Agent identities for app-only tokens with isolated identity. +- **Agent User Identity** - Agent identities for user agent tokens without user interaction (Same thing) + +β οΈ For agent scenarios, be sure to run them in a secure environment. That's a confidential client! + +**Learn more:** [Daemon Applications & Agent Identities](./getting-started/daemon-app.md) + +## ποΈ Package Architecture + +Microsoft.Identity.Web is composed of several NuGet packages to support different scenarios: + +| Package | Purpose | Target Frameworks | +|---------|---------|-------------------| +| **[Microsoft.Identity.Web](https://www.nuget.org/packages/Microsoft.Identity.Web)** | Core library for ASP.NET Core web apps | .NET 6.0+, .NET Framework 4.6.2+ | +| **[Microsoft.Identity.Web.TokenAcquisition](https://www.nuget.org/packages/Microsoft.Identity.Web.TokenAcquisition)** | Token acquisition services | .NET 6.0+ | +| **[Microsoft.Identity.Web.TokenCache](https://www.nuget.org/packages/Microsoft.Identity.Web.TokenCache)** | Token cache serialization | .NET Standard 2.0+ | +| **[Microsoft.Identity.Web.DownstreamApi](https://www.nuget.org/packages/Microsoft.Identity.Web.DownstreamApi)** | Helper for calling downstream APIs | .NET 6.0+ | +| **[Microsoft.Identity.Web.UI](https://www.nuget.org/packages/Microsoft.Identity.Web.UI)** | UI components for web apps | .NET 6.0+ | +| **[Microsoft.Identity.Web.GraphServiceClient](https://www.nuget.org/packages/Microsoft.Identity.Web.GraphServiceClient)** | Microsoft Graph SDK integration | .NET 6.0+ | +| **[Microsoft.Identity.Web.Certificate](https://www.nuget.org/packages/Microsoft.Identity.Web.Certificate)** | Certificate loading helpers | .NET Standard 2.0+ | +| **[Microsoft.Identity.Web.Certificateless](https://www.nuget.org/packages/Microsoft.Identity.Web.Certificateless)** | Certificateless authentication | .NET 6.0+ | +| **[Microsoft.Identity.Web.OWIN](https://www.nuget.org/packages/Microsoft.Identity.Web.OWIN)** | OWIN/ASP.NET Framework support | .NET Framework 4.6.2+ | + +## π Authentication Credentials + +Microsoft.Identity.Web supports multiple ways to authenticate your application: + +**Recommended for Production:** +- **[Certificateless (FIC + Managed Identity)](./authentication/credentials/certificateless.md)** β - Zero certificate management, automatic rotation +- **[Certificates from Key Vault](./authentication/credentials/certificates.md#key-vault)** - Centralized certificate management with Azure Key Vault + +**For Development:** +- **[Client Secrets](./authentication/credentials/client-secrets.md)** - Simple shared secrets (not for production) +- **[Certificates from Files](./authentication/credentials/certificates.md#file-path)** - PFX/P12 files on disk + +**See:** [Credential Decision Guide](./authentication/credentials/credentials-README.md) for choosing the right approach. + +## π Supported .NET Versions + +| .NET Version | Support Status | Notes | +|--------------|----------------|-------| +| **.NET 9** | β Supported | Latest release, recommended for new projects | +| **.NET 8** | β Supported (LTS) | Long-term support until November 2026 | +| **.NET 6** | β οΈ Deprecated | Support ending in version 4.0.0 (use .NET 8 LTS) | +| **.NET 7** | β οΈ Deprecated | Support ending in version 4.0.0 | +| **.NET Framework 4.7.2** | β Supported | For OWIN applications (via specific packages) | +| **.NET Framework 4.6.2** | β Supported | For OWIN applications (via specific packages) | + +**Current stable version:** 3.14.1 +**Upcoming:** Version 4.0.0 will remove .NET 6.0 and .NET 7.0 support + +## π Documentation Structure + +### Getting Started +- [Quickstart: Web App](./getting-started/quickstart-webapp.md) - Sign in users in 10 minutes +- [Quickstart: Web API](./getting-started/quickstart-webapi.md) - Protect your API in 10 minutes +- [Daemon Applications](./getting-started/daemon-app.md) - Call downstream APIs on behalf of a service. + +### Scenarios +- [Web Applications](./getting-started/quickstart-webapp.md) - Sign-in users, call APIs +- [Web APIs](./getting-started/quickstart-webapi.md) - Protect APIs, call downstream services +- [Daemon Applications](./getting-started/daemon-app.md) - Background services, autonomous agents, agent user identities +- [Agent identities](./calling-downstream-apis/AgentIdentities-Readme.md) for protected web APIs interpersonating agent identities or validating tokens from agent identities. + + +### Authentication & Tokens +- [Credentials Guide](./authentication/credentials/credentials-README.md) - Choose and configure credentials +- [Token Cache](./authentication/token-cache/token-cache-README.md) - Configure distributed caching +- [Token Decryption](./authentication/credentials/token-decryption.md) - Decrypt encrypted tokens +- [Authorization](./authentication/authorization.md) - Scope validation, authorization policies, tenant filtering + +### Advanced Topics +- [Customization](./advanced/customization.md) - Configure options, event handlers, login hints +- [Logging & Diagnostics](./advanced/logging.md) - PII logging, correlation IDs, troubleshooting +- [Multiple Authentication Schemes](./advanced/multiple-auth-schemes.md) +- [Incremental Consent & Conditional Access](./calling-downstream-apis/from-web-apps.md#incremental-consent--conditional-access) +- [Long-Running Processes](./calling-downstream-apis/from-web-apis.md#long-running-processes-with-obo) +- [APIs Behind Gateways](./advanced/api-gateways.md) + +### .NET Framework Support +- [ASP.NET Framework & .NET Standard](./frameworks/aspnet-framework.md) - Overview and package guide +- [MSAL.NET with Microsoft.Identity.Web](./frameworks/msal-dotnet-framework.md) - Token cache and certificates for console/daemon apps +- [OWIN Integration](./frameworks/owin.md) - ASP.NET MVC and Web API integration +- [Entra ID sidecar](./sidecar/Sidecar.md) - Microsoft Entra Identity Sidecar documentation when you want to protect web APIs in other languages than .NET + +## π External Resources + +- **[NuGet Packages](https://www.nuget.org/packages?q=Microsoft.Identity.Web)** - Download packages +- **[API Reference](https://learn.microsoft.com/dotnet/api/microsoft.identity.web)** - Complete API documentation +- **[Samples Repository](https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2)** - Working code examples +- **[GitHub Issues](https://github.com/AzureAD/microsoft-identity-web/issues)** - Report bugs or request features +- **[Stack Overflow](https://stackoverflow.com/questions/tagged/microsoft-identity-web)** - Community support + +## π€ Contributing + +We welcome contributions! See our [Contributing Guide](https://github.com/AzureAD/microsoft-identity-web/blob/master/CONTRIBUTING.md) for details. + +## π License + +This project is licensed under the MIT License. See [LICENSE](https://github.com/AzureAD/microsoft-identity-web/blob/master/LICENSE) for details. + +--- + +**Need help?** Start with our [Quickstart Guides](./getting-started/) to find your use case and learn from there. \ No newline at end of file diff --git a/docs/advanced/api-gateways.md b/docs/advanced/api-gateways.md new file mode 100644 index 000000000..73335e85e --- /dev/null +++ b/docs/advanced/api-gateways.md @@ -0,0 +1,1047 @@ +# Deploying Protected APIs Behind Gateways + +This guide explains how to deploy ASP.NET Core web APIs protected with Microsoft.Identity.Web behind Azure API gateways and reverse proxies, including Azure API Management (APIM), Azure Front Door, and Azure Application Gateway. + +## Overview + +When deploying protected APIs behind gateways, you need to handle: + +- **Forwarded headers** - Preserve original request context (scheme, host, IP) +- **Token validation** - Ensure audience claims match gateway URLs +- **CORS configuration** - Handle cross-origin requests correctly +- **Health endpoints** - Provide unauthenticated health checks +- **Path-based routing** - Support gateway-level path prefixes +- **SSL/TLS termination** - Handle HTTPS properly when gateway terminates SSL + +## Common Gateway Scenarios + +### Azure API Management (APIM) + +**Use case:** Enterprise API gateway with policies, rate limiting, transformation + +**Architecture:** +``` +Client β Azure AD β Token +Client β APIM (apim.azure-api.net) β Backend API (app.azurewebsites.net) +``` + +**Key considerations:** +- Some APIM policies can validate JWT tokens before forwarding to backend +- Backend API still validates tokens +- Audience claim must match APIM URL or backend URL (configure accordingly) + +### Azure Front Door + +**Use case:** Global load balancing, CDN, DDoS protection + +**Architecture:** +``` +Client β Azure AD β Token +Client β Front Door (azurefd.net) β Backend API (regional endpoints) +``` + +**Key considerations:** +- Front Door forwards requests with `X-Forwarded-*` headers +- SSL/TLS termination at Front Door +- Token audience validation needs configuration + +### Azure Application Gateway + +**Use case:** Regional load balancing, WAF, path-based routing + +**Architecture:** +``` +Client β Azure AD β Token +Client β Application Gateway β Backend API (multiple instances) +``` + +**Key considerations:** +- Web Application Firewall (WAF) integration +- Path-based routing rules +- Backend health probes need unauthenticated endpoints + +--- + +## Configuration Patterns + +### 1. Forwarded Headers Middleware + +Always configure forwarded headers middleware when behind a gateway: + +```csharp +using Microsoft.AspNetCore.HttpOverrides; + +var builder = WebApplication.CreateBuilder(args); + +// Configure forwarded headers BEFORE authentication +builder.Services.Configure(options => +{ + options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | + ForwardedHeaders.XForwardedProto | + ForwardedHeaders.XForwardedHost; + + // Clear known networks/proxies to accept forwarded headers from any source + // (Azure infrastructure will be the proxy) + options.KnownNetworks.Clear(); + options.KnownProxies.Clear(); + + // Limit to specific headers if needed + options.ForwardedForHeaderName = "X-Forwarded-For"; + options.ForwardedProtoHeaderName = "X-Forwarded-Proto"; + options.ForwardedHostHeaderName = "X-Forwarded-Host"; +}); + +// Add authentication +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")); + +var app = builder.Build(); + +// USE forwarded headers BEFORE authentication middleware +app.UseForwardedHeaders(); +app.UseAuthentication(); +app.UseAuthorization(); + +app.Run(); +``` + +**Why this matters:** +- Preserves original client IP address for logging +- Ensures `HttpContext.Request.Scheme` reflects original HTTPS +- Correct `Host` header for redirect URLs and token validation + +### 2. Token Audience Configuration + +#### Option A: Accept Both Gateway and Backend URLs + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + "Audience": "api://your-client-id", + "TokenValidationParameters": { + "ValidAudiences": [ + "api://your-client-id", + "https://your-backend.azurewebsites.net", + "https://your-apim.azure-api.net" + ] + } + } +} +``` + +**Code configuration:** + +```csharp +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Identity.Web; + +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")) + .EnableTokenAcquisitionToCallDownstreamApi() + .AddInMemoryTokenCaches(); + +// Customize token validation to accept multiple audiences +builder.Services.Configure(JwtBearerDefaults.AuthenticationScheme, options => +{ + var existingValidation = options.TokenValidationParameters.AudienceValidator; + + options.TokenValidationParameters.AudienceValidator = (audiences, token, parameters) => + { + var validAudiences = new[] + { + "api://your-client-id", + "https://your-backend.azurewebsites.net", + "https://your-apim.azure-api.net", + builder.Configuration["AzureAd:ClientId"] // Also accept ClientId + }; + + return audiences.Any(a => validAudiences.Contains(a, StringComparer.OrdinalIgnoreCase)); + }; +}); +``` + +#### Option B: Rewrite Audience in APIM Policy + +Configure APIM to rewrite the audience claim before forwarding: + +```xml + + + + + + api://your-client-id + + + + + + true + + + +``` + +### 3. Health Endpoint Configuration + +Gateways need unauthenticated health endpoints for probes: + +```csharp +var app = builder.Build(); + +// Health endpoint BEFORE authentication middleware +app.MapGet("/health", () => Results.Ok(new { status = "healthy" })) + .AllowAnonymous(); + +app.UseForwardedHeaders(); +app.UseAuthentication(); +app.UseAuthorization(); + +// Protected endpoints require authentication +app.MapControllers(); + +app.Run(); +``` + +**Alternative with ASP.NET Core Health Checks:** + +```csharp +using Microsoft.Extensions.Diagnostics.HealthChecks; + +builder.Services.AddHealthChecks() + .AddCheck("api", () => HealthCheckResult.Healthy()); + +var app = builder.Build(); + +app.MapHealthChecks("/health").AllowAnonymous(); +app.MapHealthChecks("/ready").AllowAnonymous(); + +app.UseForwardedHeaders(); +app.UseAuthentication(); +app.UseAuthorization(); +app.MapControllers(); + +app.Run(); +``` + +### 4. CORS Configuration Behind Gateways + +When using Azure Front Door or APIM with frontend applications: + +```csharp +builder.Services.AddCors(options => +{ + options.AddPolicy("AllowGateway", policy => + { + policy.WithOrigins( + "https://your-apim.azure-api.net", + "https://your-frontend.azurefd.net", + "https://your-app.azurewebsites.net" + ) + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials(); // If using cookies + }); +}); + +var app = builder.Build(); + +app.UseForwardedHeaders(); +app.UseCors("AllowGateway"); +app.UseAuthentication(); +app.UseAuthorization(); + +app.Run(); +``` + +**Important:** CORS must be configured **after** forwarded headers and **before** authentication. + +--- + +## Azure API Management (APIM) Integration + +### Complete APIM Configuration + +#### 1. Backend API Configuration + +**Program.cs:** + +```csharp +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.Identity.Web; + +var builder = WebApplication.CreateBuilder(args); + +// Forwarded headers for APIM +builder.Services.Configure(options => +{ + options.ForwardedHeaders = ForwardedHeaders.All; + options.KnownNetworks.Clear(); + options.KnownProxies.Clear(); +}); + +// Authentication +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")); + +builder.Services.AddControllers(); + +var app = builder.Build(); + +// Middleware order matters +app.UseForwardedHeaders(); +app.UseHttpsRedirection(); +app.UseAuthentication(); +app.UseAuthorization(); +app.MapControllers(); + +app.Run(); +``` + +**appsettings.json:** + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-backend-api-client-id", + "Audience": "api://your-backend-api-client-id" + } +} +``` + +#### 2. APIM Inbound Policy (Validate JWT) + +```xml + + + + + + + + + api://your-backend-api-client-id + + + https://login.microsoftonline.com/{your-tenant-id}/v2.0 + + + + access_as_user + + + + + + + + + + @(context.Request.OriginalUrl.Host) + + + + + + + + + + + + + + + + + + +``` + +#### 3. APIM API Configuration + +**Named Values (for reusability):** +- `tenant-id`: Your Azure AD tenant ID +- `backend-api-client-id`: Backend API's client ID +- `backend-base-url`: `https://your-backend.azurewebsites.net` + +**API Settings:** +- **API URL suffix**: `/api` (optional path prefix) +- **Web service URL**: Set via policy using named values +- **Subscription required**: Yes (adds another layer of security) + +#### 4. Client Application Configuration + +Client apps request tokens for the **backend API**, not APIM: + +```csharp +// Client app requests token +var result = await app.AcquireTokenSilent( + scopes: new[] { "api://your-backend-api-client-id/access_as_user" }, + account) + .ExecuteAsync(); + +// Call APIM URL with token +var client = new HttpClient(); +client.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", result.AccessToken); + +// Add APIM subscription key +client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", "your-subscription-key"); + +var response = await client.GetAsync("https://your-apim.azure-api.net/api/weatherforecast"); +``` + +--- + +## Azure Front Door Integration + +### Configuration for Global Distribution + +#### 1. Backend API Configuration + +**Program.cs:** + +```csharp +using Microsoft.AspNetCore.HttpOverrides; + +var builder = WebApplication.CreateBuilder(args); + +// Configure for Azure Front Door +builder.Services.Configure(options => +{ + options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | + ForwardedHeaders.XForwardedProto | + ForwardedHeaders.XForwardedHost; + + // Accept headers from any source (Azure Front Door) + options.KnownNetworks.Clear(); + options.KnownProxies.Clear(); + + // Front Door specific headers + options.ForwardedForHeaderName = "X-Forwarded-For"; + options.ForwardedProtoHeaderName = "X-Forwarded-Proto"; +}); + +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")); + +var app = builder.Build(); + +app.UseForwardedHeaders(); +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); +``` + +#### 2. Front Door Origin Configuration + +**Azure Portal Settings:** +1. Create Front Door profile +2. Add origin group with your backend API instances +3. Configure health probes to `/health` endpoint +4. Set HTTPS only forwarding +5. Enable WAF policy (optional) + +**Health Probe Settings:** +- **Path**: `/health` +- **Protocol**: HTTPS +- **Method**: GET +- **Interval**: 30 seconds + +#### 3. Handling Multiple Regions + +When deploying to multiple regions behind Front Door: + +```csharp +// Add region awareness for logging/diagnostics +builder.Services.AddSingleton(); + +app.Use(async (context, next) => +{ + // Log the actual client IP and region + var clientIp = context.Connection.RemoteIpAddress?.ToString(); + var forwardedFor = context.Request.Headers["X-Forwarded-For"].ToString(); + var frontDoorId = context.Request.Headers["X-Azure-FDID"].ToString(); + + // Add to logger scope or response headers + context.Response.Headers.Add("X-Served-By-Region", + builder.Configuration["Region"] ?? "unknown"); + + await next(); +}); +``` + +#### 4. Front Door and Token Validation + +Token audiences should include Front Door URL if clients request tokens for it: + +```csharp +builder.Services.Configure(JwtBearerDefaults.AuthenticationScheme, options => +{ + options.TokenValidationParameters.ValidAudiences = new[] + { + "api://your-backend-api-client-id", + "https://your-frontend.azurefd.net", // Front Door URL + builder.Configuration["AzureAd:ClientId"] + }; +}); +``` + +--- + +## Azure Application Gateway Integration + +### Configuration with WAF + +#### 1. Backend API Configuration + +**Program.cs:** + +```csharp +using Microsoft.AspNetCore.HttpOverrides; + +var builder = WebApplication.CreateBuilder(args); + +// Application Gateway uses standard forwarded headers +builder.Services.Configure(options => +{ + options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | + ForwardedHeaders.XForwardedProto; + options.KnownNetworks.Clear(); + options.KnownProxies.Clear(); +}); + +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")); + +builder.Services.AddHealthChecks(); + +var app = builder.Build(); + +// Health endpoint for Application Gateway probes +app.MapHealthChecks("/health").AllowAnonymous(); + +app.UseForwardedHeaders(); +app.UseAuthentication(); +app.UseAuthorization(); +app.MapControllers(); + +app.Run(); +``` + +#### 2. Application Gateway Configuration + +**Backend Settings:** +- **Protocol**: HTTPS (recommended) or HTTP +- **Port**: 443 or 80 +- **Override backend path**: No (unless needed) +- **Custom probe**: Yes, pointing to `/health` + +**Health Probe:** +- **Protocol**: HTTPS or HTTP +- **Host**: Leave default or specify +- **Path**: `/health` +- **Interval**: 30 seconds +- **Unhealthy threshold**: 3 + +**WAF Policy:** +- Enable WAF with OWASP 3.2 ruleset +- **Important**: Ensure JWT tokens in Authorization headers are not blocked +- May need to create WAF exclusions for `RequestHeaderNames` containing "Authorization" + +#### 3. Path-Based Routing + +When using path-based routing rules: + +```csharp +// Backend API should work regardless of path prefix +var app = builder.Build(); + +// Option 1: Use path base (if gateway adds prefix) +app.UsePathBase("/api/v1"); + +// Option 2: Configure routing explicitly +app.UseForwardedHeaders(); +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); +``` + +**Application Gateway Rule:** +- **Path**: `/api/v1/*` +- **Backend target**: Your backend pool +- **Backend settings**: Use configured settings + +--- + +## Troubleshooting + +### Problem: 401 Unauthorized after deployment behind gateway + +**Symptoms:** +- API works locally but returns 401 behind gateway +- Token seems valid when decoded at jwt.ms + +**Possible causes:** + +1. **Audience claim mismatch** + ```bash + # Check token audience + # Decode token and verify 'aud' claim matches one of: + # - api://your-client-id + # - https://your-backend.azurewebsites.net + # - https://your-gateway-url + ``` + +2. **Missing forwarded headers middleware** + ```csharp + // Ensure this is BEFORE authentication + app.UseForwardedHeaders(); + app.UseAuthentication(); + ``` + +3. **HTTPS redirection issues** + ```csharp + // If gateway terminates SSL, may need to disable or configure carefully + if (!app.Environment.IsDevelopment()) + { + app.UseHttpsRedirection(); + } + ``` + +**Solution:** +- Enable debug logging to see token validation details +- Add multiple valid audiences in token validation +- Check X-Forwarded-* headers are being forwarded by gateway + +### Problem: Health probes failing + +**Symptoms:** +- Gateway marks backend as unhealthy +- Health endpoint returns 401 + +**Solution:** + +```csharp +// Ensure health endpoint is BEFORE authentication +app.MapHealthChecks("/health").AllowAnonymous(); + +// Alternative: Use custom middleware +app.Map("/health", healthApp => +{ + healthApp.Run(async context => + { + context.Response.StatusCode = 200; + await context.Response.WriteAsync("healthy"); + }); +}); + +app.UseAuthentication(); // Health endpoint bypasses this +``` + +### Problem: CORS errors behind Front Door + +**Symptoms:** +- Preflight OPTIONS requests fail +- Browser console shows CORS errors + +**Solution:** + +```csharp +builder.Services.AddCors(options => +{ + options.AddDefaultPolicy(policy => + { + policy.WithOrigins( + "https://your-frontend.azurefd.net", + "https://your-app.com" + ) + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials(); + }); +}); + +var app = builder.Build(); + +app.UseForwardedHeaders(); +app.UseCors(); // Before authentication +app.UseAuthentication(); +app.UseAuthorization(); +``` + +### Problem: Token validation logs show "Forwarded header" warnings + +**Symptoms:** +``` +Microsoft.AspNetCore.HttpOverrides.ForwardedHeadersMiddleware: Unknown proxy +``` + +**Solution:** + +```csharp +builder.Services.Configure(options => +{ + // Clear known networks to accept from any proxy + options.KnownNetworks.Clear(); + options.KnownProxies.Clear(); + + // Or explicitly add Azure IP ranges (more secure but complex) + // options.KnownProxies.Add(IPAddress.Parse("20.x.x.x")); +}); +``` + +### Problem: APIM returns 401 but backend returns 200 + +**Symptoms:** +- Token is valid for backend +- APIM validate-jwt policy fails + +**Solution:** + +Check APIM policy audience matches token audience: + +```xml + + + + + api://your-backend-api-client-id + + +``` + +### Problem: Multiple authentication schemes conflict + +**Symptoms:** +- Using both JWT bearer and other schemes +- Wrong scheme selected + +**Solution:** + +```csharp +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Identity.Web; + +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")) + .AddScheme("CustomScheme", options => {}); + +// In controller, specify scheme explicitly +[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] +public class WeatherForecastController : ControllerBase +{ + // ... +} +``` + +--- + +## Best Practices + +### 1. Defense in Depth + +β **Always validate tokens in backend API**, even if gateway validates them + +```csharp +// Gateway validates token (APIM policy) +// Backend ALSO validates token (Microsoft.Identity.Web) +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")); +``` + +**Why:** Gateway configuration can change, tokens can be replayed, defense in depth is critical for security. + +### 2. Use Managed Identities for Gateway-to-Backend + +If your gateway needs to call backend with its own identity: + +```csharp +// Backend accepts both user tokens and gateway's managed identity +builder.Services.Configure(JwtBearerDefaults.AuthenticationScheme, options => +{ + options.TokenValidationParameters.ValidAudiences = new[] + { + "api://backend-api-client-id", // User tokens + "https://management.azure.com" // Managed identity tokens (if applicable) + }; +}); +``` + +### 3. Monitor Gateway Metrics + +- Track 401/403 error rates +- Monitor token validation failures +- Alert on health probe failures +- Log forwarded headers for debugging + +### 4. Use Application Insights + +```csharp +builder.Services.AddApplicationInsightsTelemetry(); + +// Log custom properties +app.Use(async (context, next) => +{ + var telemetry = context.RequestServices.GetRequiredService(); + telemetry.TrackEvent("ApiRequest", new Dictionary + { + ["ForwardedFor"] = context.Request.Headers["X-Forwarded-For"], + ["OriginalHost"] = context.Request.Headers["X-Forwarded-Host"], + ["Gateway"] = "APIM" // or "FrontDoor", "AppGateway" + }); + + await next(); +}); +``` + +### 5. Separate Health from Ready + +```csharp +// Health: Is the service running? +app.MapGet("/health", () => Results.Ok()).AllowAnonymous(); + +// Ready: Can the service accept traffic? +app.MapHealthChecks("/ready", new HealthCheckOptions +{ + Predicate = check => check.Tags.Contains("ready") +}).AllowAnonymous(); + +builder.Services.AddHealthChecks() + .AddCheck("database", () => /* check DB */ , tags: new[] { "ready" }) + .AddCheck("cache", () => /* check cache */ , tags: new[] { "ready" }); +``` + +### 6. Document Your Gateway Configuration + +Create a README or wiki page documenting: +- β Which gateway(s) are in use +- β Token audience expectations +- β CORS configuration +- β Health probe endpoints +- β Forwarded headers configuration +- β Emergency rollback procedures + +--- + +## Complete Example: API Behind Azure API Management + +### Backend API (ASP.NET Core) + +**Program.cs:** + +```csharp +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.Identity.Web; + +var builder = WebApplication.CreateBuilder(args); + +// Forwarded headers for APIM +builder.Services.Configure(options => +{ + options.ForwardedHeaders = ForwardedHeaders.All; + options.KnownNetworks.Clear(); + options.KnownProxies.Clear(); +}); + +// Authentication +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")) + .EnableTokenAcquisitionToCallDownstreamApi() + .AddMicrosoftGraph() + .AddInMemoryTokenCaches(); + +// Application Insights +builder.Services.AddApplicationInsightsTelemetry(); + +// Health checks +builder.Services.AddHealthChecks(); + +builder.Services.AddControllers(); + +var app = builder.Build(); + +// Health endpoint (unauthenticated) +app.MapHealthChecks("/health").AllowAnonymous(); + +// Middleware order is critical +app.UseForwardedHeaders(); +app.UseHttpsRedirection(); +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); +``` + +**appsettings.json:** + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "backend-api-client-id", + "Audience": "api://backend-api-client-id" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.Identity.Web": "Debug" + } + }, + "ApplicationInsights": { + "ConnectionString": "your-connection-string" + } +} +``` + +**WeatherForecastController.cs:** + +```csharp +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Identity.Web.Resource; + +[Authorize] +[ApiController] +[Route("[controller]")] +[RequiredScope("access_as_user")] +public class WeatherForecastController : ControllerBase +{ + private readonly ILogger _logger; + + public WeatherForecastController(ILogger logger) + { + _logger = logger; + } + + [HttpGet] + public IActionResult Get() + { + // Log forwarded headers for debugging + var forwardedFor = HttpContext.Request.Headers["X-Forwarded-For"]; + var forwardedHost = HttpContext.Request.Headers["X-Forwarded-Host"]; + + _logger.LogInformation( + "Request from {ForwardedFor} via {ForwardedHost}", + forwardedFor, + forwardedHost); + + return Ok(new[] { "Weather", "Forecast", "Data" }); + } +} +``` + +### APIM Configuration + +**Inbound Policy:** + +```xml + + + + + + + + + + + + api://backend-api-client-id + + + https://login.microsoftonline.com/{tenant-id}/v2.0 + + + + access_as_user + + + + + + + @(context.Request.OriginalUrl.Host) + + + @(context.Request.OriginalUrl.Scheme) + + + + + + + + + + + + + + + + + https://your-frontend.com + + + GET + POST + + + * + + + + + + + + +``` + +--- + +## See Also + +- **[Web Apps Behind Proxies](web-apps-behind-proxies.md)** - Web app redirect URI handling and proxy configuration +- **[Quickstart: Web API](../getting-started/quickstart-webapi.md)** - Basic API authentication setup +- **[Calling Downstream APIs from Web APIs](../calling-downstream-apis/from-web-apis.md)** - OBO flow and token acquisition +- **[Authorization Guide](../authentication/authorization.md)** - RequiredScope and authorization policies +- **[Logging & Diagnostics](logging.md)** - Troubleshooting authentication issues +- **[Multiple Authentication Schemes](multiple-auth-schemes.md)** - Using multiple auth schemes in one API + +--- + +## Additional Resources + +- [Azure API Management Documentation](https://learn.microsoft.com/azure/api-management/) +- [Azure Front Door Documentation](https://learn.microsoft.com/azure/frontdoor/) +- [Azure Application Gateway Documentation](https://learn.microsoft.com/azure/application-gateway/) +- [ASP.NET Core Forwarded Headers Middleware](https://learn.microsoft.com/aspnet/core/host-and-deploy/proxy-load-balancer) +- [JWT Token Validation](https://learn.microsoft.com/azure/active-directory/develop/access-tokens) + +--- + +**Microsoft.Identity.Web Version:** 3.14.1+ +**Last Updated:** October 28, 2025 diff --git a/docs/advanced/customization.md b/docs/advanced/customization.md new file mode 100644 index 000000000..b9fbaf049 --- /dev/null +++ b/docs/advanced/customization.md @@ -0,0 +1,796 @@ +# Customizing Authentication with Microsoft.Identity.Web + +This guide explains how to customize authentication behavior in ASP.NET Core applications using Microsoft.Identity.Web while preserving the library's built-in security features. + +--- + +## π Table of Contents + +- [Overview](#overview) +- [Configuration Customization](#configuration-customization) +- [Event Handler Customization](#event-handler-customization) +- [Token Acquisition Customization](#token-acquisition-customization) +- [UI Customization](#ui-customization) +- [Sign-In Experience Customization](#sign-in-experience-customization) +- [Best Practices](#best-practices) + +--- + +## Overview + +Microsoft.Identity.Web provides secure defaults for authentication and authorization. However, you can customize many aspects while maintaining security: + +### What Can Be Customized? + +| Area | Customization Options | +|------|----------------------| +| **Configuration** | All `MicrosoftIdentityOptions`, `OpenIdConnectOptions`, `JwtBearerOptions` properties | +| **Events** | OpenID Connect events (`OnTokenValidated`, `OnRedirectToIdentityProvider`, etc.) | +| **Token Acquisition** | Correlation IDs, extra query parameters | +| **Claims** | Add custom claims to `ClaimsPrincipal` | +| **UI** | Sign-out pages, redirect behavior | +| **Sign-In** | Login hints, domain hints | + +### Customization Methods + +**Two approaches:** + +1. **`Configure`** - Configures options before they're used +2. **`PostConfigure`** - Configures options after all `Configure` calls + +**Order of execution:** +``` +Configure β Configure β ... β PostConfigure β PostConfigure β ... β Options used +``` + +--- + +## Configuration Customization + +### Understanding Configuration Mapping + +The `"AzureAd"` section in `appsettings.json` maps to multiple classes: + +- [`MicrosoftIdentityOptions`](https://learn.microsoft.com/dotnet/api/microsoft.identity.web.microsoftidentityoptions) +- [`ConfidentialClientApplicationOptions`](https://learn.microsoft.com/dotnet/api/microsoft.identity.client.confidentialclientapplicationoptions) + +You can use any property from these classes in your configuration. + +### Pattern 1: Configure MicrosoftIdentityOptions + +```csharp +using Microsoft.Identity.Web; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd")); + +// Customize Microsoft Identity options +builder.Services.Configure(options => +{ + // Enable PII logging (development only!) + options.EnablePiiLogging = true; + + // Custom client capabilities + options.ClientCapabilities = new[] { "CP1", "CP2" }; + + // Override token validation parameters + options.TokenValidationParameters.ValidateLifetime = true; + options.TokenValidationParameters.ClockSkew = TimeSpan.FromMinutes(5); +}); + +var app = builder.Build(); +``` + +### Pattern 2: Configure OpenIdConnectOptions (Web Apps) + +```csharp +builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd")); + +// Customize OpenIdConnect options +builder.Services.Configure( + OpenIdConnectDefaults.AuthenticationScheme, + options => +{ + // Override response type + options.ResponseType = "code id_token"; + + // Add extra scopes + options.Scope.Add("offline_access"); + options.Scope.Add("profile"); + + // Customize token validation + options.TokenValidationParameters.NameClaimType = "preferred_username"; + options.TokenValidationParameters.RoleClaimType = "roles"; + + // Set redirect URI + options.CallbackPath = "/signin-oidc"; + + // Configure cookie options + options.Cookie.HttpOnly = true; + options.Cookie.SecurePolicy = CookieSecurePolicy.Always; + options.Cookie.SameSite = SameSiteMode.Lax; +}); +``` + +### Pattern 3: Configure JwtBearerOptions (Web APIs) + +```csharp +using Microsoft.AspNetCore.Authentication.JwtBearer; + +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")); + +// Customize JWT Bearer options +builder.Services.Configure( + JwtBearerDefaults.AuthenticationScheme, + options => +{ + // Customize audience validation + options.TokenValidationParameters.ValidAudiences = new[] + { + "api://your-api-client-id", + "https://your-api.com" + }; + + // Set custom claim mappings + options.TokenValidationParameters.NameClaimType = "name"; + options.TokenValidationParameters.RoleClaimType = "roles"; + + // Customize token validation + options.TokenValidationParameters.ValidateLifetime = true; + options.TokenValidationParameters.ClockSkew = TimeSpan.Zero; // No tolerance +}); +``` + +### Pattern 4: Configure Cookie Options + +```csharp +using Microsoft.AspNetCore.Authentication.Cookies; + +// Configure cookie policy +builder.Services.Configure(options => +{ + options.MinimumSameSitePolicy = SameSiteMode.Lax; + options.Secure = CookieSecurePolicy.Always; + options.HttpOnly = Microsoft.AspNetCore.CookiePolicy.HttpOnlyPolicy.Always; +}); + +// Configure cookie authentication options +builder.Services.Configure( + CookieAuthenticationDefaults.AuthenticationScheme, + options => +{ + options.Cookie.Name = "MyApp.Auth"; + options.Cookie.HttpOnly = true; + options.Cookie.SecurePolicy = CookieSecurePolicy.Always; + options.Cookie.SameSite = SameSiteMode.Lax; + options.ExpireTimeSpan = TimeSpan.FromHours(1); + options.SlidingExpiration = true; +}); +``` + +--- + +## Event Handler Customization + +OpenID Connect and JWT Bearer authentication provide events you can hook into. Microsoft.Identity.Web sets up event handlersβyou can extend them without losing built-in functionality. + +### Critical Pattern: Preserve Existing Handlers + +**β Wrong - Overwrites Microsoft.Identity.Web's handler:** +```csharp +services.Configure(JwtBearerDefaults.AuthenticationScheme, options => +{ + options.Events.OnTokenValidated = async context => + { + // Your code - but you LOST the built-in validation! + await Task.CompletedTask; + }; +}); +``` + +**β Correct - Chains with existing handler:** +```csharp +services.Configure(JwtBearerDefaults.AuthenticationScheme, options => +{ + var existingOnTokenValidatedHandler = options.Events.OnTokenValidated; + + options.Events.OnTokenValidated = async context => + { + // Call Microsoft.Identity.Web's handler FIRST + await existingOnTokenValidatedHandler(context); + + // Then your custom code + // (executes AFTER built-in security checks) + var identity = context.Principal.Identity as ClaimsIdentity; + identity?.AddClaim(new Claim("custom_claim", "custom_value")); + }; +}); +``` + +### Common Event Scenarios + +#### Add Custom Claims After Token Validation + +**Web API example:** + +```csharp +using Microsoft.AspNetCore.Authentication.JwtBearer; +using System.Security.Claims; + +builder.Services.Configure( + JwtBearerDefaults.AuthenticationScheme, + options => +{ + var existingHandler = options.Events.OnTokenValidated; + + options.Events.OnTokenValidated = async context => + { + // Preserve built-in validation + await existingHandler(context); + + // Add custom claims + var identity = context.Principal.Identity as ClaimsIdentity; + + // Example: Add department claim from database + var userObjectId = context.Principal.FindFirst("oid")?.Value; + if (!string.IsNullOrEmpty(userObjectId)) + { + var department = await GetUserDepartment(userObjectId); + identity?.AddClaim(new Claim("department", department)); + } + + // Example: Add application-specific role + var email = context.Principal.FindFirst("email")?.Value; + if (email?.EndsWith("@admin.com") == true) + { + identity?.AddClaim(new Claim(ClaimTypes.Role, "SuperAdmin")); + } + }; +}); +``` + +**Web App example:** + +```csharp +using Microsoft.AspNetCore.Authentication.OpenIdConnect; + +builder.Services.Configure( + OpenIdConnectDefaults.AuthenticationScheme, + options => +{ + var existingHandler = options.Events.OnTokenValidated; + + options.Events.OnTokenValidated = async context => + { + // Preserve built-in processing + await existingHandler(context); + + // Call Microsoft Graph to get additional user data + var graphClient = context.HttpContext.RequestServices + .GetRequiredService(); + + var user = await graphClient.Me.GetAsync(); + + var identity = context.Principal.Identity as ClaimsIdentity; + identity?.AddClaim(new Claim("jobTitle", user?.JobTitle ?? "")); + identity?.AddClaim(new Claim("department", user?.Department ?? "")); + }; +}); +``` + +#### Add Query Parameters to Authorization Request + +```csharp +builder.Services.Configure( + OpenIdConnectDefaults.AuthenticationScheme, + options => +{ + var existingHandler = options.Events.OnRedirectToIdentityProvider; + + options.Events.OnRedirectToIdentityProvider = async context => + { + // Preserve existing behavior + if (existingHandler != null) + { + await existingHandler(context); + } + + // Add custom query parameters + context.ProtocolMessage.Parameters.Add("slice", "testslice"); + context.ProtocolMessage.Parameters.Add("custom_param", "custom_value"); + + // Conditional parameters based on request + if (context.HttpContext.Request.Query.ContainsKey("prompt")) + { + context.ProtocolMessage.Prompt = context.HttpContext.Request.Query["prompt"]; + } + }; +}); +``` + +#### Customize Authentication Failure Handling + +```csharp +builder.Services.Configure( + OpenIdConnectDefaults.AuthenticationScheme, + options => +{ + options.Events.OnAuthenticationFailed = async context => + { + // Log the error + var logger = context.HttpContext.RequestServices + .GetRequiredService>(); + logger.LogError(context.Exception, "Authentication failed"); + + // Customize error response + context.Response.StatusCode = 401; + context.Response.ContentType = "application/json"; + await context.Response.WriteAsync($$""" + { + "error": "authentication_failed", + "error_description": "{{context.Exception.Message}}" + } + """); + + context.HandleResponse(); // Suppress default error handling + }; +}); +``` + +#### Handle Access Denied + +```csharp +builder.Services.Configure( + OpenIdConnectDefaults.AuthenticationScheme, + options => +{ + options.Events.OnAccessDenied = async context => + { + // User denied consent + context.Response.Redirect("/Home/AccessDenied"); + context.HandleResponse(); + await Task.CompletedTask; + }; +}); +``` + +--- + +## Token Acquisition Customization + +### Using IDownstreamApi with Custom Options + +```csharp +using Microsoft.Identity.Abstractions; + +public class TodoListController : ControllerBase +{ + private readonly IDownstreamApi _downstreamApi; + + public TodoListController(IDownstreamApi downstreamApi) + { + _downstreamApi = downstreamApi; + } + + [HttpGet("{id}")] + public async Task GetTodo(int id, Guid correlationId) + { + var result = await _downstreamApi.GetForUserAsync( + "TodoListService", + options => + { + options.RelativePath = $"api/todolist/{id}"; + + // Customize token acquisition + options.TokenAcquisitionOptions = new TokenAcquisitionOptions + { + CorrelationId = correlationId, + ExtraQueryParameters = new Dictionary + { + { "slice", "test_slice" } + } + }; + }); + + return Ok(result); + } +} +``` + +--- + +## UI Customization + +### Redirect to Specific Page After Sign-In + +Use the `redirectUri` parameter: + +```html + +Sign In + + +[HttpGet] +public IActionResult SignInToDashboard() +{ + return RedirectToAction("SignIn", "Account", new + { + area = "MicrosoftIdentity", + redirectUri = "/Dashboard" + }); +} +``` + +### Customize Signed-Out Page + +**Option 1: Override the Razor Page** + +Create `Areas/MicrosoftIdentity/Pages/Account/SignedOut.cshtml`: + +```cshtml +@page +@model Microsoft.Identity.Web.UI.Areas.MicrosoftIdentity.Pages.Account.SignedOutModel +@{ + ViewData["Title"] = "Signed out"; +} + + + You have been signed out + Thank you for using our application. + + Return to Home + + +``` + +**Option 2: Redirect to Custom Page** + +```csharp +builder.Services.Configure( + OpenIdConnectDefaults.AuthenticationScheme, + options => +{ + options.Events.OnSignedOutCallbackRedirect = context => + { + context.Response.Redirect("/Home/SignedOut"); + context.HandleResponse(); + return Task.CompletedTask; + }; +}); +``` + +--- + +## Sign-In Experience Customization + +### Login Hint & Domain Hint + +Streamline the sign-in experience by pre-populating usernames and directing to specific tenants. + +#### What Are Hints? + +| Hint | Purpose | Example | +|------|---------|---------| +| **loginHint** | Pre-populate username/email field | `"user@contoso.com"` | +| **domainHint** | Direct to specific tenant login page | `"contoso.com"` | + +#### Usage Patterns + +**Pattern 1: Controller-Based** + +```csharp +using Microsoft.AspNetCore.Mvc; + +public class AuthController : Controller +{ + [HttpGet] + public IActionResult SignIn() + { + // Standard sign-in + return RedirectToAction("SignIn", "Account", new + { + area = "MicrosoftIdentity", + redirectUri = "/Dashboard" + }); + } + + [HttpGet] + public IActionResult SignInWithLoginHint() + { + // Pre-populate username + return RedirectToAction("SignIn", "Account", new + { + area = "MicrosoftIdentity", + redirectUri = "/Dashboard", + loginHint = "user@contoso.com" + }); + } + + [HttpGet] + public IActionResult SignInWithDomainHint() + { + // Direct to Contoso tenant + return RedirectToAction("SignIn", "Account", new + { + area = "MicrosoftIdentity", + redirectUri = "/Dashboard", + domainHint = "contoso.com" + }); + } + + [HttpGet] + public IActionResult SignInWithBothHints() + { + // Pre-populate AND direct to tenant + return RedirectToAction("SignIn", "Account", new + { + area = "MicrosoftIdentity", + redirectUri = "/Dashboard", + loginHint = "user@contoso.com", + domainHint = "contoso.com" + }); + } +} +``` + +**Pattern 2: View-Based** + +```html + + Sign In Options + + + + Sign In + + + + + Sign In as user@contoso.com + + + + + Sign In (Contoso) + + +``` + +**Pattern 3: Programmatic with OnRedirectToIdentityProvider** + +```csharp +builder.Services.Configure( + OpenIdConnectDefaults.AuthenticationScheme, + options => +{ + var existingHandler = options.Events.OnRedirectToIdentityProvider; + + options.Events.OnRedirectToIdentityProvider = async context => + { + if (existingHandler != null) + { + await existingHandler(context); + } + + // Add hints based on application logic + if (context.HttpContext.Request.Query.TryGetValue("tenant", out var tenant)) + { + context.ProtocolMessage.DomainHint = tenant; + } + + // Get suggested user from cookie or session + var suggestedUser = context.HttpContext.Request.Cookies["LastSignedInUser"]; + if (!string.IsNullOrEmpty(suggestedUser)) + { + context.ProtocolMessage.LoginHint = suggestedUser; + } + }; +}); +``` + +#### Use Cases + +**E-commerce Platform:** +```csharp +// Pre-fill returning customer email +loginHint = customerEmail +``` + +**B2B Application:** +```csharp +// Direct to customer's tenant +domainHint = customerDomain +``` + +**Multi-Tenant SaaS:** +```csharp +// Route based on subdomain +domainHint = GetTenantFromSubdomain(Request.Host) +``` + +--- + +## Best Practices + +### β Do's + +**1. Always preserve existing event handlers:** +```csharp +var existingHandler = options.Events.OnTokenValidated; +options.Events.OnTokenValidated = async context => +{ + await existingHandler(context); // Call Microsoft.Identity.Web's handler + // Your custom code +}; +``` + +**2. Use correlation IDs for tracing:** +```csharp +var tokenOptions = new TokenAcquisitionOptions +{ + CorrelationId = Activity.Current?.Id ?? Guid.NewGuid() +}; +``` + +**3. Validate custom claims:** +```csharp +var department = context.Principal.FindFirst("department")?.Value; +if (!IsValidDepartment(department)) +{ + throw new UnauthorizedAccessException("Invalid department"); +} +``` + +**4. Log customization errors:** +```csharp +try +{ + // Custom logic +} +catch (Exception ex) +{ + logger.LogError(ex, "Custom authentication logic failed"); + throw; +} +``` + +**5. Test both success and failure paths:** +```csharp +// Test with valid tokens +// Test with missing claims +// Test with expired tokens +// Test with wrong audience +``` + +### β Don'ts + +**1. Don't skip Microsoft.Identity.Web's event handlers:** +```csharp +// β Wrong - loses built-in security checks +options.Events.OnTokenValidated = async context => { /* your code */ }; + +// β Correct - preserves security +var existing = options.Events.OnTokenValidated; +options.Events.OnTokenValidated = async context => +{ + await existing(context); + /* your code */ +}; +``` + +**2. Don't enable PII logging in production:** +```csharp +// β Wrong +options.EnablePiiLogging = true; // In production! + +// β Correct +if (builder.Environment.IsDevelopment()) +{ + options.EnablePiiLogging = true; +} +``` + +**3. Don't bypass token validation:** +```csharp +// β Wrong - insecure! +options.TokenValidationParameters.ValidateLifetime = false; +options.TokenValidationParameters.ValidateAudience = false; + +// β Correct - maintain security +options.TokenValidationParameters.ValidateLifetime = true; +options.TokenValidationParameters.ClockSkew = TimeSpan.FromMinutes(5); +``` + +**4. Don't hardcode sensitive values:** +```csharp +// β Wrong +options.ClientSecret = "mysecret123"; + +// β Correct +options.ClientSecret = builder.Configuration["AzureAd:ClientSecret"]; +``` + +**5. Don't modify authentication in middleware:** +```csharp +// β Wrong - configure in Startup, not middleware +app.Use(async (context, next) => +{ + // Modifying auth options here is too late! +}); +``` + +--- + +## Troubleshooting + +### Customization Not Working + +**Check execution order:** +1. `AddMicrosoftIdentityWebApp` / `AddMicrosoftIdentityWebApi` sets defaults +2. Your `Configure` calls run +3. `PostConfigure` calls run (if any) +4. Options are used + +**Solution:** Use `PostConfigure` if `Configure` isn't working: +```csharp +services.PostConfigure( + OpenIdConnectDefaults.AuthenticationScheme, + options => { /* your changes */ } +); +``` + +### Custom Claims Not Appearing + +**Check:** +1. Is `OnTokenValidated` handler chained correctly? +2. Is authentication successful before adding claims? +3. Are claims added to the correct identity? + +**Debug:** +```csharp +var claims = context.Principal.Claims.ToList(); +logger.LogInformation($"Claims count: {claims.Count}"); +foreach (var claim in claims) +{ + logger.LogInformation($"{claim.Type}: {claim.Value}"); +} +``` + +### Events Not Firing + +**Verify middleware order:** +```csharp +app.UseAuthentication(); // Must be first +app.UseAuthorization(); // Must be second +app.MapControllers(); // Then endpoints +``` + +--- + +## See Also + +- **[Authorization Guide](../authentication/authorization.md)** - Scope validation and authorization policies +- **[Logging & Diagnostics](logging.md)** - Debug customization issues with correlation IDs and detailed logging +- **[Token Cache](../authentication/token-cache/token-cache-README.md)** - Configure distributed token caching +- **[Quickstart: Web App](../getting-started/quickstart-webapp.md)** - Get started with web application authentication + +--- + +## Additional Resources + +- [ASP.NET Core Authentication](https://learn.microsoft.com/aspnet/core/security/authentication/) +- [OpenID Connect Events](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectevents) +- [JWT Bearer Events](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer.jwtbearerevents) +- [Claims Transformation](https://learn.microsoft.com/aspnet/core/security/authentication/claims) + +--- + +**Last Updated:** October 27, 2025 +**Microsoft.Identity.Web Version:** 3.14.1+ diff --git a/docs/advanced/logging.md b/docs/advanced/logging.md new file mode 100644 index 000000000..01bdf0232 --- /dev/null +++ b/docs/advanced/logging.md @@ -0,0 +1,750 @@ +# Logging and Diagnostics in Microsoft.Identity.Web + +This guide explains how to configure and use logging in Microsoft.Identity.Web to troubleshoot authentication and token acquisition issues. + +--- + +## π Table of Contents + +- [Overview](#overview) +- [Quick Start](#quick-start) +- [Configuration](#configuration) +- [Log Levels](#log-levels) +- [PII Logging](#pii-logging) +- [Correlation IDs](#correlation-ids) +- [Token Cache Logging](#token-cache-logging) +- [Troubleshooting](#troubleshooting) + +--- + +## Overview + +Microsoft.Identity.Web integrates with ASP.NET Core's logging infrastructure, providing visibility into: + +- **Authentication flows** - Sign-in, sign-out, token validation +- **Token acquisition** - Token cache hits/misses, MSAL operations +- **Downstream API calls** - HTTP requests, token acquisition for APIs +- **Error conditions** - Exceptions, validation failures + +### What Gets Logged? + +| Component | Log Source | Purpose | +|-----------|-----------|---------| +| **Microsoft.Identity.Web** | Core authentication logic | Configuration, token acquisition, API calls | +| **MSAL.NET** | `Microsoft.Identity.Client` | Token cache operations, authority validation | +| **IdentityModel** | Token validation | JWT parsing, signature validation, claims extraction | +| **ASP.NET Core Auth** | `Microsoft.AspNetCore.Authentication` | Cookie operations, challenge/forbid actions | + +--- + +## Quick Start + +### Minimal Configuration + +Add to `appsettings.json`: + +```json +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Identity": "Information" + } + } +} +``` + +This enables **Information-level** logging for Microsoft.Identity.Web and its dependencies (MSAL.NET, IdentityModel). + +### Development Configuration + +For detailed diagnostics during development: + +```json +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Identity": "Debug", + "Microsoft.AspNetCore.Authentication": "Information" + } + }, + "AzureAd": { + "EnablePiiLogging": true // Development only! + } +} +``` + +### Production Configuration + +For production, minimize log volume while capturing errors: + +```json +{ + "Logging": { + "LogLevel": { + "Default": "Warning", + "Microsoft": "Warning", + "Microsoft.Identity": "Warning" + } + }, + "AzureAd": { + "EnablePiiLogging": false // Never true in production + } +} +``` + +--- + +## Configuration + +### Namespace-Based Filtering + +Control log verbosity by namespace: + +```json +{ + "Logging": { + "LogLevel": { + "Default": "Information", + + // General Microsoft namespaces + "Microsoft": "Warning", + "Microsoft.AspNetCore": "Warning", + + // Identity-specific namespaces + "Microsoft.Identity": "Information", + "Microsoft.Identity.Web": "Information", + "Microsoft.Identity.Client": "Information", + + // ASP.NET Core authentication + "Microsoft.AspNetCore.Authentication": "Information", + "Microsoft.AspNetCore.Authentication.JwtBearer": "Information", + "Microsoft.AspNetCore.Authentication.OpenIdConnect": "Debug", + + // Token validation + "Microsoft.IdentityModel": "Warning" + } + } +} +``` + +### Disable Specific Logging + +To silence noisy components without affecting others: + +```json +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Identity.Web": "None", // Completely disable + "Microsoft.Identity.Client": "Warning" // Only errors/warnings + } + } +} +``` + +### Environment-Specific Configuration + +Use `appsettings.{Environment}.json` for per-environment settings: + +**appsettings.Development.json:** +```json +{ + "Logging": { + "LogLevel": { + "Microsoft.Identity": "Debug" + } + }, + "AzureAd": { + "EnablePiiLogging": true + } +} +``` + +**appsettings.Production.json:** +```json +{ + "Logging": { + "LogLevel": { + "Microsoft.Identity": "Warning" + } + }, + "AzureAd": { + "EnablePiiLogging": false + } +} +``` + +--- + +## Log Levels + +### ASP.NET Core Log Levels + +| Level | Usage | Volume | Production? | +|-------|-------|--------|-------------| +| **Trace** | Most detailed, every operation | Very High | β No | +| **Debug** | Detailed flow, useful for dev | High | β No | +| **Information** | General flow, key events | Moderate | β οΈ Selective | +| **Warning** | Unexpected but handled conditions | Low | β Yes | +| **Error** | Errors and exceptions | Very Low | β Yes | +| **Critical** | Unrecoverable failures | Very Low | β Yes | +| **None** | Disable logging | None | β οΈ Selective | + +### MSAL.NET to ASP.NET Core Mapping + +| MSAL.NET Level | ASP.NET Core Equivalent | Description | +|----------------|------------------------|-------------| +| `Verbose` | `Debug` or `Trace` | Most detailed messages | +| `Info` | `Information` | Key authentication events | +| `Warning` | `Warning` | Abnormal but handled conditions | +| `Error` | `Error` or `Critical` | Errors and exceptions | + +### Recommended Settings by Environment + +**Development:** +```json +{ + "Logging": { + "LogLevel": { + "Microsoft.Identity": "Debug", + "Microsoft.Identity.Client": "Information" + } + } +} +``` + +**Staging:** +```json +{ + "Logging": { + "LogLevel": { + "Microsoft.Identity": "Information", + "Microsoft.Identity.Client": "Warning" + } + } +} +``` + +**Production:** +```json +{ + "Logging": { + "LogLevel": { + "Microsoft.Identity": "Warning", + "Microsoft.Identity.Client": "Error" + } + } +} +``` + +--- + +## PII Logging + +### What is PII? + +**Personally Identifiable Information (PII)** includes: +- Usernames, email addresses +- Display names +- Object IDs, tenant IDs +- IP addresses +- Token values, claims + +### Security Warning + +> β οΈ **WARNING**: You and your application are responsible for complying with all applicable regulatory requirements including those set forth by [GDPR](https://www.microsoft.com/trust-center/privacy/gdpr-overview). Before enabling PII logging, ensure you can safely handle this potentially highly sensitive data. + +### Enable PII Logging (Development Only) + +**appsettings.Development.json:** +```json +{ + "AzureAd": { + "EnablePiiLogging": true // β οΈ Development/Testing ONLY + }, + "Logging": { + "LogLevel": { + "Microsoft.Identity": "Debug" + } + } +} +``` + +### Programmatic PII Control + +For conditional PII logging based on environment: + +```csharp +var builder = WebApplication.CreateBuilder(args); + +builder.Services.Configure(options => +{ + // Only enable PII in Development + options.EnablePiiLogging = builder.Environment.IsDevelopment(); +}); +``` + +### What Changes with PII Enabled? + +**Without PII logging:** +``` +[Information] Token validation succeeded for user '{hidden}' +[Information] Acquired token from cache for scopes '{hidden}' +``` + +**With PII enabled:** +``` +[Information] Token validation succeeded for user 'john.doe@contoso.com' +[Information] Acquired token from cache for scopes 'user.read api://my-api/.default' +``` + +### PII Redaction in Logs + +When PII logging is disabled, sensitive data is replaced with: +- `{hidden}` - Hides user identifiers +- `{hash:XXXX}` - Shows hash instead of actual value +- `***` - Obscures tokens + +--- + +## Correlation IDs + +Correlation IDs trace authentication requests across Microsoft's services, critical for support scenarios. + +### What Are Correlation IDs? + +A correlation ID is a **GUID** that uniquely identifies an authentication/token acquisition request across: +- Your application +- Microsoft Identity platform +- MSAL.NET library +- Microsoft backend services + +### Obtaining Correlation IDs + +**Method 1: From AuthenticationResult** + +```csharp +using Microsoft.Identity.Web; + +public class TodoController : ControllerBase +{ + private readonly ITokenAcquisition _tokenAcquisition; + private readonly ILogger _logger; + + public TodoController( + ITokenAcquisition tokenAcquisition, + ILogger logger) + { + _tokenAcquisition = tokenAcquisition; + _logger = logger; + } + + [HttpGet] + public async Task GetTodos() + { + var result = await _tokenAcquisition.GetAuthenticationResultForUserAsync( + new[] { "user.read" }); + + _logger.LogInformation( + "Token acquired. CorrelationId: {CorrelationId}, Source: {TokenSource}", + result.CorrelationId, + result.AuthenticationResultMetadata.TokenSource); + + return Ok(result.CorrelationId); + } +} +``` + +**Method 2: From MsalServiceException** + +```csharp +using Microsoft.Identity.Client; + +try +{ + var token = await _tokenAcquisition.GetAccessTokenForUserAsync( + new[] { "user.read" }); +} +catch (MsalServiceException ex) +{ + _logger.LogError(ex, + "Token acquisition failed. CorrelationId: {CorrelationId}, ErrorCode: {ErrorCode}", + ex.CorrelationId, + ex.ErrorCode); + + // Return correlation ID to user for support + return StatusCode(500, new { + error = "authentication_failed", + correlationId = ex.CorrelationId + }); +} +``` + +**Method 3: Set Custom Correlation ID** + +```csharp +[HttpGet("{id}")] +public async Task GetTodo(int id) +{ + // Use request trace ID as correlation ID + var correlationId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; + + var todo = await _downstreamApi.GetForUserAsync( + "TodoListService", + options => + { + options.RelativePath = $"api/todolist/{id}"; + options.TokenAcquisitionOptions = new TokenAcquisitionOptions + { + CorrelationId = Guid.Parse(correlationId) + }; + }); + + _logger.LogInformation( + "Called downstream API. TraceId: {TraceId}, CorrelationId: {CorrelationId}", + HttpContext.TraceIdentifier, + correlationId); + + return Ok(todo); +} +``` + +### Using Correlation IDs for Support + +When contacting Microsoft support, provide: + +1. **Correlation ID** - From logs or exception +2. **Timestamp** - When the error occurred (UTC) +3. **Tenant ID** - Your Azure AD tenant +4. **Error code** - If applicable (e.g., `AADSTS50058`) + +**Example support request:** +``` +Subject: Token acquisition failing for user.read scope + +Correlation ID: 12345678-1234-1234-1234-123456789012 +Timestamp: 2025-01-15 14:32:45 UTC +Tenant ID: contoso.onmicrosoft.com +Error Code: AADSTS50058 +``` + +--- + +## Token Cache Logging + +### Enable Token Cache Diagnostics + +For .NET Framework or .NET Core apps using distributed token caches: + +```csharp +using Microsoft.Extensions.Logging; +using Microsoft.Identity.Web.TokenCacheProviders; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddDistributedTokenCaches(); + +// Enable detailed token cache logging +builder.Services.AddLogging(configure => +{ + configure.AddConsole(); + configure.AddDebug(); +}) +.Configure(options => +{ + options.MinLevel = LogLevel.Debug; // Detailed cache operations +}); +``` + +### Token Cache Log Examples + +**Cache hit:** +``` +[Debug] Token cache: Token found in cache for scopes 'user.read' +[Information] Token source: Cache +``` + +**Cache miss:** +``` +[Debug] Token cache: No token found in cache for scopes 'user.read' +[Information] Token source: IdentityProvider +[Debug] Token cache: Token stored in cache +``` + +### Distributed Cache Troubleshooting + +**Redis cache:** +```csharp +builder.Services.AddStackExchangeRedisCache(options => +{ + options.Configuration = builder.Configuration["Redis:ConnectionString"]; +}); + +// Enable Redis logging +builder.Services.AddLogging(configure => +{ + configure.AddFilter("Microsoft.Extensions.Caching", LogLevel.Debug); +}); +``` + +**SQL Server cache:** +```csharp +builder.Services.AddDistributedSqlServerCache(options => +{ + options.ConnectionString = builder.Configuration["SqlCache:ConnectionString"]; + options.SchemaName = "dbo"; + options.TableName = "TokenCache"; +}); + +// Enable SQL cache logging +builder.Services.AddLogging(configure => +{ + configure.AddFilter("Microsoft.Extensions.Caching.SqlServer", LogLevel.Information); +}); +``` + +--- + +## Troubleshooting + +### Common Logging Scenarios + +#### Scenario 1: Token Validation Failures + +**Symptom:** 401 Unauthorized responses + +**Enable detailed logging:** +```json +{ + "Logging": { + "LogLevel": { + "Microsoft.AspNetCore.Authentication.JwtBearer": "Debug", + "Microsoft.IdentityModel": "Information" + } + } +} +``` + +**Look for:** +``` +[Information] Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler: + Failed to validate the token. +[Debug] Microsoft.IdentityModel.Tokens: IDX10230: Lifetime validation failed. + The token is expired. +``` + +#### Scenario 2: Token Acquisition Failures + +**Symptom:** `MsalServiceException` or `MsalUiRequiredException` + +**Enable detailed logging:** +```json +{ + "Logging": { + "LogLevel": { + "Microsoft.Identity.Web": "Debug", + "Microsoft.Identity.Client": "Information" + } + } +} +``` + +**Look for:** +``` +[Error] Microsoft.Identity.Web: Token acquisition failed. + ErrorCode: invalid_grant, CorrelationId: {guid} +[Information] Microsoft.Identity.Client: MSAL returned exception: + AADSTS50058: Silent sign-in failed. +``` + +#### Scenario 3: Downstream API Call Failures + +**Enable detailed logging:** +```json +{ + "Logging": { + "LogLevel": { + "Microsoft.Identity.Abstractions": "Debug", + "System.Net.Http": "Information" + } + } +} +``` + +**Custom logging in controllers:** +```csharp +[HttpGet] +public async Task GetUserProfile() +{ + try + { + _logger.LogInformation("Acquiring token for Microsoft Graph"); + + var user = await _downstreamApi.GetForUserAsync( + "MicrosoftGraph", + options => options.RelativePath = "me"); + + _logger.LogInformation( + "Successfully retrieved user profile for {UserPrincipalName}", + user.UserPrincipalName); + + return Ok(user); + } + catch (MsalUiRequiredException ex) + { + _logger.LogWarning(ex, + "User interaction required. CorrelationId: {CorrelationId}", + ex.CorrelationId); + return Challenge(); + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "Failed to call Microsoft Graph API"); + return StatusCode(502, "Downstream API error"); + } +} +``` + +### Interpreting Log Patterns + +**Successful authentication flow:** +``` +[Info] Authentication scheme OpenIdConnect: Authorization response received +[Debug] Correlation id: {guid} +[Info] Authorization code received +[Info] Token validated successfully +[Info] Authentication succeeded for user: {user} +``` + +**Consent required:** +``` +[Warning] Microsoft.Identity.Web: Incremental consent required +[Info] AADSTS65001: User consent is required for scopes: {scopes} +[Info] Redirecting to consent page +``` + +**Token refresh:** +``` +[Debug] Token expired, attempting silent token refresh +[Info] Token source: IdentityProvider +[Info] Token refreshed successfully +``` + +### Log Aggregation Best Practices + +**Application Insights integration:** +```csharp +using Microsoft.ApplicationInsights.Extensibility; + +builder.Services.AddApplicationInsightsTelemetry(); + +// Enrich telemetry with correlation IDs +builder.Services.AddSingleton(); +``` + +**Serilog integration:** +```csharp +using Serilog; + +Log.Logger = new LoggerConfiguration() + .MinimumLevel.Information() + .MinimumLevel.Override("Microsoft.Identity", Serilog.Events.LogEventLevel.Debug) + .Enrich.FromLogContext() + .WriteTo.Console() + .WriteTo.File("logs/identity-.txt", rollingInterval: RollingInterval.Day) + .CreateLogger(); + +builder.Host.UseSerilog(); +``` + +--- + +## Best Practices + +### β Do's + +**1. Use structured logging:** +```csharp +_logger.LogInformation( + "Token acquired for user {UserId} with scopes {Scopes}", + userId, string.Join(" ", scopes)); +``` + +**2. Log correlation IDs:** +```csharp +_logger.LogError(ex, + "Operation failed. CorrelationId: {CorrelationId}", + ex.CorrelationId); +``` + +**3. Use appropriate log levels:** +```csharp +_logger.LogDebug("Detailed diagnostic info"); // Development +_logger.LogInformation("Key application events"); // Selective production +_logger.LogWarning("Unexpected but handled"); // Production +_logger.LogError(ex, "Operation failed"); // Production +``` + +**4. Sanitize logs in production:** +```csharp +var sanitizedEmail = environment.IsProduction() + ? MaskEmail(email) + : email; +_logger.LogInformation("Processing request for {Email}", sanitizedEmail); +``` + +### β Don'ts + +**1. Don't enable PII in production:** +```csharp +// β Wrong +"EnablePiiLogging": true // In production config! + +// β Correct +"EnablePiiLogging": false +``` + +**2. Don't log secrets:** +```csharp +// β Wrong +_logger.LogInformation("Token: {Token}", accessToken); + +// β Correct +_logger.LogInformation("Token acquired, expires: {ExpiresOn}", expiresOn); +``` + +**3. Don't use verbose logging in production:** +```csharp +// β Wrong - production appsettings.json +"Microsoft.Identity": "Debug" + +// β Correct +"Microsoft.Identity": "Warning" +``` + +--- + +## See Also + +- **[Customization Guide](customization.md)** - Configure authentication options and event handlers +- **[Authorization Guide](../authentication/authorization.md)** - Troubleshoot scope validation and authorization issues +- **[Token Cache Troubleshooting](../authentication/token-cache/troubleshooting.md)** - Debug token cache issues +- **[Calling Downstream APIs](../calling-downstream-apis/calling-downstream-apis-README.md)** - Troubleshoot API calls and token acquisition + +--- + +## Additional Resources + +- [MSAL.NET Logging](https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/logging) +- [ASP.NET Core Logging](https://learn.microsoft.com/aspnet/core/fundamentals/logging/) +- [Application Insights](https://learn.microsoft.com/azure/azure-monitor/app/app-insights-overview) +- [Microsoft Identity Platform Error Codes](https://learn.microsoft.com/azure/active-directory/develop/reference-aadsts-error-codes) + +--- + +**Last Updated:** October 27, 2025 +**Microsoft.Identity.Web Version:** 3.14.1+ diff --git a/docs/advanced/multiple-auth-schemes.md b/docs/advanced/multiple-auth-schemes.md new file mode 100644 index 000000000..fbcbef516 --- /dev/null +++ b/docs/advanced/multiple-auth-schemes.md @@ -0,0 +1,460 @@ +# Multiple Authentication Schemes in Microsoft.Identity.Web + +This guide explains how to configure and use multiple authentication schemes in ASP.NET Core applications using Microsoft.Identity.Web. This is common when your application needs to handle different types of authentication simultaneously. + +--- + +## π Table of Contents + +- [Overview](#overview) +- [Common Scenarios](#common-scenarios) +- [Configuration](#configuration) +- [Specifying Schemes in Controllers](#specifying-schemes-in-controllers) +- [Specifying Schemes When Calling APIs](#specifying-schemes-when-calling-apis) +- [Troubleshooting](#troubleshooting) +- [Best Practices](#best-practices) + +--- + +## Overview + +By default, ASP.NET Core uses a single default authentication scheme. However, many real-world applications need multiple schemes: + +| Scenario | Schemes Involved | +|----------|-----------------| +| Web app that also exposes an API | OpenID Connect + JWT Bearer | +| API accepting tokens from multiple identity providers | Multiple JWT Bearer schemes | +| API with both user and service-to-service authentication | JWT Bearer + API Key/Certificate | +| Hybrid authentication for migration | Legacy scheme + Modern OAuth | + +### How Authentication Schemes Work + +```mermaid +flowchart LR + Request[Incoming Request] --> Middleware[Authentication Middleware] + Middleware --> Default{Default Scheme?} + Default -->|Yes| DefaultHandler[Default Handler] + Default -->|No| Explicit{Explicit SchemeSpecified?} + Explicit -->|Yes| SpecificHandler[Specific Handler] + Explicit -->|No| Error[401 Unauthorized] +``` + +--- + +## Common Scenarios + +### Scenario 1: Web App + Web API in Same Project + +Your application serves both web pages (using cookies/OpenID Connect) and API endpoints (using JWT Bearer tokens). + +```csharp +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.Identity.Web; + +var builder = WebApplication.CreateBuilder(args); + +// Add OpenID Connect for web app (browser sign-in) +builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd")) + .EnableTokenAcquisitionToCallDownstreamApi() + .AddInMemoryTokenCaches(); + +// Add JWT Bearer for API endpoints +builder.Services.AddAuthentication() + .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"), + JwtBearerDefaults.AuthenticationScheme); + +builder.Services.AddControllersWithViews(); + +var app = builder.Build(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapControllers(); +app.Run(); +``` + +### Scenario 2: Multiple Identity Providers + +Accept tokens from both Azure AD and Azure AD B2C: + +```csharp +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + // Primary scheme: Azure AD + .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"), + JwtBearerDefaults.AuthenticationScheme) + // Secondary scheme: Azure AD B2C + .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAdB2C"), + "AzureAdB2C"); +``` + +**appsettings.json:** + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-api-client-id" + }, + "AzureAdB2C": { + "Instance": "https://your-tenant.b2clogin.com/", + "Domain": "your-tenant.onmicrosoft.com", + "TenantId": "your-b2c-tenant-id", + "ClientId": "your-b2c-api-client-id", + "SignUpSignInPolicyId": "B2C_1_SignUpSignIn" + } +} +``` + +--- + +## Configuration + +### Registering Multiple Schemes + +```csharp +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.Identity.Web; + +var builder = WebApplication.CreateBuilder(args); + +// Set the default authentication scheme (can also do with AddAuthentication(scheme)) +builder.Services.AddAuthentication(options => +{ + options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; +}) +// Add JWT Bearer (primary) +.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"), + JwtBearerDefaults.AuthenticationScheme) +// Add OpenID Connect (secondary) +.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"), + OpenIdConnectDefaults.AuthenticationScheme); +``` + +### Named Schemes + +Use named schemes for clarity: + +```csharp +public static class AuthSchemes +{ + public const string AzureAd = "AzureAd"; + public const string AzureAdB2C = "AzureAdB2C"; +} + +builder.Services.AddAuthentication(AuthSchemes.AzureAd) + .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"), AuthSchemes.AzureAd) + .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAdB2C"), AuthSchemes.AzureAdB2C); +``` + +--- + +## Specifying Schemes in Controllers + +### Using [Authorize] Attribute + +Specify which authentication scheme(s) to use for a controller or action: + +```csharp +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +// Use default scheme +[Authorize] +[ApiController] +[Route("api/[controller]")] +public class DefaultController : ControllerBase +{ + [HttpGet] + public IActionResult Get() => Ok("Using default scheme"); +} + +// Use specific scheme +[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] +[ApiController] +[Route("api/[controller]")] +public class ApiController : ControllerBase +{ + [HttpGet] + public IActionResult Get() => Ok("Using JWT Bearer scheme"); +} + +// Accept multiple schemes (any one succeeds) +[Authorize(AuthenticationSchemes = "AzureAd,AzureAdB2C")] +[ApiController] +[Route("api/[controller]")] +public class MultiSchemeController : ControllerBase +{ + [HttpGet] + public IActionResult Get() => Ok($"Authenticated via: {User.Identity?.AuthenticationType}"); +} +``` + +### Per-Action Scheme Selection + +```csharp +[ApiController] +[Route("api/[controller]")] +public class MixedController : ControllerBase +{ + // This action uses JWT Bearer + [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] + [HttpGet("api-data")] + public IActionResult GetApiData() => Ok("API data"); + + // This action uses OpenID Connect (for browser-based calls) + [Authorize(AuthenticationSchemes = OpenIdConnectDefaults.AuthenticationScheme)] + [HttpGet("web-data")] + public IActionResult GetWebData() => Ok("Web data"); + + // This action accepts either scheme + [Authorize(AuthenticationSchemes = $"{JwtBearerDefaults.AuthenticationScheme},{OpenIdConnectDefaults.AuthenticationScheme}")] + [HttpGet("any-data")] + public IActionResult GetAnyData() => Ok("Data for any authenticated user"); +} +``` + +--- + +## Specifying Schemes When Calling APIs + +When your application uses multiple authentication schemes and calls downstream APIs, you need to specify which scheme to use for token acquisition. + +### With Microsoft Graph + +```csharp +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Graph; + +[Authorize] +public class GraphController : ControllerBase +{ + private readonly GraphServiceClient _graphClient; + + public GraphController(GraphServiceClient graphClient) + { + _graphClient = graphClient; + } + + [HttpGet("profile")] + public async Task GetProfile() + { + // Specify which authentication scheme to use for token acquisition + var user = await _graphClient.Me + .GetAsync(r => r.Options + .WithAuthenticationScheme(JwtBearerDefaults.AuthenticationScheme)); + + return Ok(user); + } + + [HttpGet("mail")] + public async Task GetMail() + { + // More detailed options including scopes and scheme + var messages = await _graphClient.Me.Messages + .GetAsync(r => + { + r.Options.WithAuthenticationOptions(options => + { + // Specify authentication scheme + options.AcquireTokenOptions.AuthenticationOptionsName = + JwtBearerDefaults.AuthenticationScheme; + + // Specify additional scopes if needed + options.Scopes = new[] { "Mail.Read" }; + }); + }); + + return Ok(messages); + } +} +``` + +### With IDownstreamApi + +```csharp +using Microsoft.Identity.Abstractions; + +[Authorize] +public class DownstreamController : ControllerBase +{ + private readonly IDownstreamApi _downstreamApi; + + public DownstreamController(IDownstreamApi downstreamApi) + { + _downstreamApi = downstreamApi; + } + + [HttpGet("data")] + public async Task GetData() + { + var result = await _downstreamApi.CallApiForUserAsync( + "MyApi", + options => + { + options.AcquireTokenOptions.AuthenticationOptionsName = + JwtBearerDefaults.AuthenticationScheme; + }); + + return Ok(result); + } +} +``` + +--- + +## Troubleshooting + +### Problem: Wrong Scheme Selected + +**Symptoms:** +- 401 Unauthorized errors +- Token acquired with wrong permissions +- User claims missing or incorrect + +**Solution:** Explicitly specify the authentication scheme: + +```csharp +// In controller +[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] +public class MyApiController : ControllerBase { } + +// When calling APIs +var user = await _graphClient.Me + .GetAsync(r => r.Options.WithAuthenticationScheme(JwtBearerDefaults.AuthenticationScheme)); +``` + +### Problem: Multiple Schemes Conflict + +**Symptoms:** +- Authentication works for one scheme but not another +- Unexpected redirects or challenges + +**Solution:** Set default schemes explicitly: + +```csharp +builder.Services.AddAuthentication(options => +{ + // Default for API calls + options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + + // Default for unauthenticated challenges (redirects) + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; +}); +``` + +### Problem: Token Cache Conflicts + +**Symptoms:** +- Tokens cached for wrong scheme +- Incorrect user context + +**Solution:** Use scheme-aware token acquisition: + + +### Problem: Authorization Policies Don't Work + +**Symptoms:** +- Policy requirements not enforced +- Claims not found + +**Solution:** Ensure policy uses correct scheme: + +```csharp +builder.Services.AddAuthorization(options => +{ + options.AddPolicy("ApiPolicy", policy => + { + policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme); + policy.RequireAuthenticatedUser(); + policy.RequireClaim("scope", "access_as_user"); + }); +}); +``` + +--- + +## Best Practices + +### 1. Use Constants for Scheme Names + +Usually SchemeDefaults.AuthenticationScheme, but a static class like this can work too: +```csharp +public static class AuthSchemes +{ + public const string Primary = JwtBearerDefaults.AuthenticationScheme; + public const string B2C = "AzureAdB2C"; + public const string Internal = "InternalApi"; +} + +// Usage +[Authorize(AuthenticationSchemes = AuthSchemes.Primary)] +public class MyController : ControllerBase { } +``` + +### 2. Document Your Scheme Configuration + +```csharp +/// +/// Configures authentication for the application. +/// +/// Schemes configured: +/// - JwtBearer (default): For API clients using Azure AD tokens +/// - AzureAdB2C: For consumer-facing API clients using B2C tokens +/// - OpenIdConnect: For browser-based authentication (web app) +/// +public static IServiceCollection AddApplicationAuthentication( + this IServiceCollection services, + IConfiguration configuration) +{ + // Implementation... +} +``` + +### 3. Test Each Scheme Independently + +Create integration tests that verify each scheme works correctly: + +```csharp +[Fact] +public async Task Api_WithJwtBearerToken_ReturnsSuccess() +{ + var token = await GetJwtBearerTokenAsync(); + _client.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", token); + + var response = await _client.GetAsync("/api/data"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); +} + +[Fact] +public async Task Api_WithB2CToken_ReturnsSuccess() +{ + var token = await GetB2CTokenAsync(); + _client.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", token); + + var response = await _client.GetAsync("/api/data"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); +} +``` + +--- + +## Related Documentation + +- [Authorization in Web APIs](../authentication/authorization.md) - Scope and role validation +- [Calling Microsoft Graph](../calling-downstream-apis/microsoft-graph.md) - Graph SDK integration +- [Calling Downstream APIs](../calling-downstream-apis/calling-downstream-apis-README.md) - API call patterns +- [Deploying Behind Gateways](./api-gateways.md) - Gateway authentication scenarios +- [Logging and Diagnostics](./logging.md) - Troubleshooting authentication issues diff --git a/docs/advanced/web-apps-behind-proxies.md b/docs/advanced/web-apps-behind-proxies.md new file mode 100644 index 000000000..3fa52499c --- /dev/null +++ b/docs/advanced/web-apps-behind-proxies.md @@ -0,0 +1,971 @@ +# Deploying Web Apps Behind Proxies and Gateways + +This guide explains how to deploy ASP.NET Core web applications using Microsoft.Identity.Web behind reverse proxies, load balancers, and Azure gateways, with special focus on **redirect URI** handling for authentication callbacks. + +## Overview + +When web apps are behind proxies or gateways, authentication redirect URIs become complex because: + +- **Azure AD redirects users** to the configured redirect URI after sign-in +- **Proxies change the request context** - scheme (HTTP/HTTPS), host, port, path +- **Redirect URI must match exactly** what's registered in Azure AD +- **CallbackPath** must work through the proxy + +## Common Proxy Scenarios + +### Azure Application Gateway + +**Use case:** Regional load balancing, WAF, SSL termination + +**Impact on redirect URI:** +- Gateway URL: `https://gateway.contoso.com/myapp` +- Backend URL: `http://backend.internal/` +- Azure AD redirect: `https://gateway.contoso.com/myapp/signin-oidc` + +### Azure Front Door + +**Use case:** Global distribution, CDN, multiple regions + +**Impact on redirect URI:** +- Front Door URL: `https://myapp.azurefd.net` +- Backend URLs: `https://app-eastus.azurewebsites.net`, `https://app-westus.azurewebsites.net` +- Azure AD redirect: `https://myapp.azurefd.net/signin-oidc` + +### On-Premises Reverse Proxy + +**Use case:** Corporate network, existing infrastructure + +**Impact on redirect URI:** +- Proxy URL: `https://apps.corp.com/myapp` +- Backend URL: `http://appserver:5000/` +- Azure AD redirect: `https://apps.corp.com/myapp/signin-oidc` + +### Kubernetes Ingress + +**Use case:** Container orchestration, microservices + +**Impact on redirect URI:** +- Ingress URL: `https://apps.k8s.com/webapp` +- Service URL: `http://webapp-service.default.svc.cluster.local` +- Azure AD redirect: `https://apps.k8s.com/webapp/signin-oidc` + +--- + +## Critical Configuration: Forwarded Headers + +### Why Forwarded Headers Matter for Web Apps + +Web apps **need correct request context** to: +1. β Build absolute redirect URIs for Azure AD +2. β Validate the incoming authentication response +3. β Generate correct sign-out URIs +4. β Handle HTTPS requirement enforcement + +**Without forwarded headers middleware:** +``` +User visits: https://gateway.contoso.com/myapp +Backend sees: http://localhost:5000/ +Redirect URI built: http://localhost:5000/signin-oidc β Wrong! +Azure AD redirects to: https://gateway.contoso.com/myapp/signin-oidc +Backend doesn't recognize it: Error! +``` + +**With forwarded headers middleware:** +``` +User visits: https://gateway.contoso.com/myapp +Backend sees forwarded headers: X-Forwarded-Proto: https, X-Forwarded-Host: gateway.contoso.com +Redirect URI built: https://gateway.contoso.com/myapp/signin-oidc β Correct! +Azure AD redirects to: https://gateway.contoso.com/myapp/signin-oidc +Backend recognizes it: Success! +``` + +### Basic Forwarded Headers Configuration + +**Program.cs:** + +```csharp +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.Identity.Web; + +var builder = WebApplication.CreateBuilder(args); + +// CRITICAL: Configure forwarded headers BEFORE authentication +builder.Services.Configure(options => +{ + options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | + ForwardedHeaders.XForwardedProto | + ForwardedHeaders.XForwardedHost; + + // Accept headers from any source (proxy/gateway) + options.KnownNetworks.Clear(); + options.KnownProxies.Clear(); + + // Standard header names + options.ForwardedForHeaderName = "X-Forwarded-For"; + options.ForwardedProtoHeaderName = "X-Forwarded-Proto"; + options.ForwardedHostHeaderName = "X-Forwarded-Host"; +}); + +// Authentication +builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd")); + +builder.Services.AddRazorPages(); + +var app = builder.Build(); + +// CRITICAL: Use forwarded headers BEFORE authentication +app.UseForwardedHeaders(); + +// Only enforce HTTPS if you're sure the proxy forwards the scheme correctly +if (!app.Environment.IsDevelopment()) +{ + app.UseHttpsRedirection(); +} + +app.UseStaticFiles(); +app.UseRouting(); +app.UseAuthentication(); +app.UseAuthorization(); +app.MapRazorPages(); + +app.Run(); +``` + +**appsettings.json:** + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + "ClientSecret": "your-client-secret", + "CallbackPath": "/signin-oidc" + } +} +``` + +**Azure AD App Registration - Redirect URIs:** +``` +https://gateway.contoso.com/myapp/signin-oidc +``` + +--- + +## Path-Based Routing Scenarios + +### Problem: Proxy Adds Path Prefix + +**Scenario:** +- Proxy URL: `https://apps.contoso.com/webapp1` +- Backend URL: `http://backend:5000/` +- Backend only knows about `/`, not `/webapp1` + +### Solution 1: Use PathBase (Recommended) + +```csharp +var app = builder.Build(); + +// Tell the app it's hosted at a path prefix +app.UsePathBase("/webapp1"); + +app.UseForwardedHeaders(); +app.UseStaticFiles(); +app.UseRouting(); +app.UseAuthentication(); +app.UseAuthorization(); +app.MapRazorPages(); + +app.Run(); +``` + +**How it works:** +- `HttpContext.Request.Path` removes the `/webapp1` prefix for routing +- `HttpContext.Request.PathBase` contains `/webapp1` +- Link generation automatically includes the path base +- Redirect URIs automatically include the path base + +**Azure AD Registration:** +``` +https://apps.contoso.com/webapp1/signin-oidc +``` + +### Solution 2: Proxy Rewrites Path + +Some proxies can rewrite paths before forwarding: + +**NGINX configuration:** +```nginx +location /webapp1/ { + proxy_pass http://backend:5000/; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Original-URL $request_uri; +} +``` + +**Application configuration:** +```csharp +// No PathBase needed if proxy strips the prefix +app.UseForwardedHeaders(); +``` + +**Azure AD Registration:** +``` +https://apps.contoso.com/webapp1/signin-oidc +``` + +### Solution 3: Custom Middleware for Dynamic PathBase + +When path base varies by environment: + +```csharp +// Read path base from configuration or headers +var pathBase = builder.Configuration["PathBase"]; +if (!string.IsNullOrEmpty(pathBase)) +{ + app.UsePathBase(pathBase); +} + +// Or detect from X-Forwarded-Prefix header +app.Use((context, next) => +{ + var forwardedPrefix = context.Request.Headers["X-Forwarded-Prefix"].ToString(); + if (!string.IsNullOrEmpty(forwardedPrefix)) + { + context.Request.PathBase = forwardedPrefix; + } + return next(); +}); + +app.UseForwardedHeaders(); +``` + +--- + +## SSL/TLS Termination + +### Problem: Proxy Terminates HTTPS + +**Scenario:** +- User connects to proxy via HTTPS +- Proxy connects to backend via HTTP +- Backend builds HTTP redirect URIs (wrong!) + +### Solution: X-Forwarded-Proto Header + +**Proxy configuration (NGINX):** +```nginx +location / { + proxy_pass http://backend:5000; + proxy_set_header X-Forwarded-Proto $scheme; # Critical! + proxy_set_header X-Forwarded-Host $host; +} +``` + +**Application configuration:** +```csharp +builder.Services.Configure(options => +{ + options.ForwardedHeaders = ForwardedHeaders.XForwardedProto | + ForwardedHeaders.XForwardedHost; + options.KnownNetworks.Clear(); + options.KnownProxies.Clear(); +}); + +var app = builder.Build(); + +app.UseForwardedHeaders(); // Reads X-Forwarded-Proto and sets Request.Scheme = "https" + +// HTTPS redirection becomes safe +app.UseHttpsRedirection(); // Won't create infinite redirect loop + +app.UseAuthentication(); +``` + +### Common Mistake: HTTPS Redirection Loop + +**Problem:** +```csharp +// Without UseForwardedHeaders() +app.UseHttpsRedirection(); // Sees Request.Scheme = "http", redirects to HTTPS +// User gets infinite redirect loop! +``` + +**Solution:** +```csharp +// WITH UseForwardedHeaders() +app.UseForwardedHeaders(); // Sets Request.Scheme = "https" from X-Forwarded-Proto +app.UseHttpsRedirection(); // Sees HTTPS, no redirect needed β +``` + +--- + +## Custom Domain Configuration + +### Scenario: Custom Domain Through Azure Front Door + +**Architecture:** +- Custom domain: `https://myapp.contoso.com` +- Front Door: `https://myapp.azurefd.net` (backend origin) +- Azure Web App: `https://myapp-backend.azurewebsites.net` + +**Front Door Configuration:** +1. Add custom domain `myapp.contoso.com` to Front Door +2. Configure SSL certificate (Front Door managed or custom) +3. Set backend pool to `myapp-backend.azurewebsites.net` +4. Enable HTTPS only + +**Application Configuration:** + +```csharp +// No special configuration needed if headers are forwarded correctly +builder.Services.Configure(options => +{ + options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | + ForwardedHeaders.XForwardedProto | + ForwardedHeaders.XForwardedHost; + options.KnownNetworks.Clear(); + options.KnownProxies.Clear(); +}); +``` + +**Azure AD Registration:** +``` +https://myapp.contoso.com/signin-oidc +https://myapp.contoso.com/signout-callback-oidc +``` + +**Test redirect URI generation:** + +```csharp +// In a controller or page +public IActionResult TestRedirectUri() +{ + var request = HttpContext.Request; + var scheme = request.Scheme; // Should be "https" + var host = request.Host.Value; // Should be "myapp.contoso.com" + var pathBase = request.PathBase.Value; // Should be "" or your path base + var path = "/signin-oidc"; + + var redirectUri = $"{scheme}://{host}{pathBase}{path}"; + // Expected: https://myapp.contoso.com/signin-oidc + + return Content($"Redirect URI would be: {redirectUri}"); +} +``` + +--- + +## Multiple Redirect URIs for Different Environments + +### Problem: Same App, Multiple Gateways + +**Scenario:** +- Production: `https://app.contoso.com` (Front Door) +- Staging: `https://app-staging.azurewebsites.net` (Direct) +- Development: `https://localhost:5001` (Local) + +### Solution: Register All Redirect URIs + +**Azure AD App Registration - Redirect URIs:** +``` +https://app.contoso.com/signin-oidc +https://app-staging.azurewebsites.net/signin-oidc +https://localhost:5001/signin-oidc +``` + +**Application configuration (works for all):** + +```csharp +var app = builder.Build(); + +app.UseForwardedHeaders(); // Handles proxy scenarios +app.UseAuthentication(); // Builds correct redirect URI based on request context +``` + +**How it works:** +- Application dynamically builds redirect URI based on incoming request +- `HttpContext.Request.Scheme`, `Host`, and `PathBase` determine the URI +- As long as it's registered in Azure AD, authentication succeeds + +--- + +## Azure Application Gateway Configuration + +### Complete Example with Path-Based Routing + +**Application Gateway Settings:** + +**Backend Pool:** +- Target: `backend.azurewebsites.net` or IP address + +**HTTP Settings:** +- Protocol: HTTPS (recommended) or HTTP +- Port: 443 or 80 +- Override backend path: No +- Custom probe: Yes + +**Health Probe:** +- Protocol: HTTPS or HTTP +- Host: Leave blank (uses backend pool hostname) +- Path: `/health` (must be anonymous endpoint) +- Interval: 30 seconds + +**Routing Rule:** +- Name: `webapp-rule` +- Listener: HTTPS listener on port 443 +- Backend pool: Your backend pool +- HTTP settings: Your HTTP settings + +**Application Code:** + +```csharp +using Microsoft.AspNetCore.HttpOverrides; + +var builder = WebApplication.CreateBuilder(args); + +// Forwarded headers for Application Gateway +builder.Services.Configure(options => +{ + options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | + ForwardedHeaders.XForwardedProto | + ForwardedHeaders.XForwardedHost; + options.KnownNetworks.Clear(); + options.KnownProxies.Clear(); +}); + +// Authentication +builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd")); + +// Health checks (for Application Gateway probe) +builder.Services.AddHealthChecks(); + +builder.Services.AddRazorPages(); + +var app = builder.Build(); + +// Health endpoint BEFORE authentication (critical for gateway probes) +app.MapHealthChecks("/health").AllowAnonymous(); + +// Middleware order +app.UseForwardedHeaders(); +app.UseHttpsRedirection(); +app.UseStaticFiles(); +app.UseRouting(); +app.UseAuthentication(); +app.UseAuthorization(); +app.MapRazorPages(); + +app.Run(); +``` + +**appsettings.json:** + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + "ClientSecret": "your-client-secret", + "CallbackPath": "/signin-oidc" + } +} +``` + +**Azure AD Registration:** +``` +https://gateway.contoso.com/signin-oidc +https://gateway.contoso.com/signout-callback-oidc +``` + +--- + +## Azure Front Door Configuration + +### Multi-Region Web App Deployment + +**Scenario:** +- Front Door: `https://app.azurefd.net` (global endpoint) +- East US: `https://app-eastus.azurewebsites.net` +- West US: `https://app-westus.azurewebsites.net` +- Users routed to nearest region + +**Front Door Configuration:** + +**Origin Group:** +- Name: `webapp-origins` +- Health probe: `/health` +- Load balancing: Latency-based + +**Origins:** +1. `app-eastus.azurewebsites.net` (priority 1) +2. `app-westus.azurewebsites.net` (priority 1) + +**Route:** +- Path: `/*` +- Forwarding protocol: HTTPS only +- Origin group: `webapp-origins` + +**Application Code (Both Regions):** + +```csharp +using Microsoft.AspNetCore.HttpOverrides; + +var builder = WebApplication.CreateBuilder(args); + +// Forwarded headers for Front Door +builder.Services.Configure(options => +{ + options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | + ForwardedHeaders.XForwardedProto | + ForwardedHeaders.XForwardedHost; + options.KnownNetworks.Clear(); + options.KnownProxies.Clear(); +}); + +builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd")); + +builder.Services.AddHealthChecks(); +builder.Services.AddRazorPages(); + +var app = builder.Build(); + +// Health check for Front Door probe +app.MapHealthChecks("/health").AllowAnonymous(); + +app.UseForwardedHeaders(); +app.UseHttpsRedirection(); +app.UseStaticFiles(); +app.UseRouting(); +app.UseAuthentication(); +app.UseAuthorization(); +app.MapRazorPages(); + +app.Run(); +``` + +**Important:** Both regions use the **same Azure AD app registration** with the **same redirect URI**: + +**Azure AD Registration:** +``` +https://app.azurefd.net/signin-oidc +https://app.azurefd.net/signout-callback-oidc +``` + +**Why it works:** +- Front Door URL is consistent across regions +- Forwarded headers ensure backend builds correct redirect URI +- Token acquisition happens at regional backend +- Distributed token cache (Redis) shares tokens across regions + +--- + +## Troubleshooting + +### Problem: "Redirect URI mismatch" Error + +**Symptoms:** +``` +AADSTS50011: The redirect URI 'http://localhost:5000/signin-oidc' +specified in the request does not match the redirect URIs configured +for the application 'your-app-id'. +``` + +**Possible causes:** + +1. **Missing forwarded headers middleware** + ```csharp + // Fix: Add BEFORE authentication + app.UseForwardedHeaders(); + app.UseAuthentication(); + ``` + +2. **Wrong redirect URI registered in Azure AD** + - Check registered URIs in Azure portal + - Ensure HTTPS (not HTTP) for production + - Ensure host matches (including port if non-standard) + - Ensure path includes PathBase if applicable + +3. **Proxy not forwarding headers** + - Check proxy configuration + - Verify `X-Forwarded-Proto`, `X-Forwarded-Host` are set + - Test with curl: `curl -H "X-Forwarded-Proto: https" -H "X-Forwarded-Host: gateway.com" http://backend:5000/` + +4. **PathBase not configured** + ```csharp + // If proxy adds /myapp prefix, add this: + app.UsePathBase("/myapp"); + ``` + +**Debug redirect URI generation:** + +```csharp +// Add this middleware to log the redirect URI being built +app.Use(async (context, next) => +{ + var logger = context.RequestServices.GetRequiredService>(); + logger.LogInformation( + "Request: Scheme={Scheme}, Host={Host}, PathBase={PathBase}, Path={Path}", + context.Request.Scheme, + context.Request.Host, + context.Request.PathBase, + context.Request.Path); + + await next(); +}); +``` + +### Problem: Authentication Works Locally But Not Behind Proxy + +**Symptoms:** +- Sign-in works on `localhost:5001` +- Sign-in fails on `gateway.contoso.com` +- Error: Redirect URI mismatch or correlation failed + +**Solution checklist:** + +1. β **Forwarded headers configured and used first** + ```csharp + app.UseForwardedHeaders(); // Must be first! + ``` + +2. β **Proxy forwards required headers** + - `X-Forwarded-Proto: https` + - `X-Forwarded-Host: gateway.contoso.com` + - Optional: `X-Forwarded-Prefix` for path base + +3. β **Redirect URI registered in Azure AD** + - `https://gateway.contoso.com/signin-oidc` + +4. β **PathBase configured if needed** + ```csharp + app.UsePathBase("/myapp"); // If proxy adds prefix + ``` + +5. β **HTTPS enforced correctly** + ```csharp + app.UseForwardedHeaders(); // Reads X-Forwarded-Proto first + app.UseHttpsRedirection(); // Then enforces HTTPS + ``` + +### Problem: Sign-Out Fails or Redirects to Wrong URL + +**Symptoms:** +- Sign-in works +- Sign-out redirects to wrong URL (localhost, http://, wrong host) + +**Solution:** + +```csharp +// Ensure PostLogoutRedirectUri uses correct base URL +builder.Services.Configure( + OpenIdConnectDefaults.AuthenticationScheme, + options => + { + options.Events.OnRedirectToIdentityProviderForSignOut = context => + { + // Build correct post-logout redirect URI + var request = context.HttpContext.Request; + var postLogoutUri = $"{request.Scheme}://{request.Host}{request.PathBase}/signout-callback-oidc"; + + context.ProtocolMessage.PostLogoutRedirectUri = postLogoutUri; + return Task.CompletedTask; + }; + }); +``` + +**Ensure registered in Azure AD:** +``` +https://gateway.contoso.com/signout-callback-oidc +``` + +### Problem: Infinite Redirect Loop + +**Symptoms:** +- Browser keeps redirecting between app and Azure AD +- Login never completes + +**Possible causes:** + +1. **HTTPS redirection before forwarded headers** + ```csharp + // WRONG ORDER: + app.UseHttpsRedirection(); // Sees HTTP, redirects to HTTPS + app.UseForwardedHeaders(); // Too late! + + // CORRECT ORDER: + app.UseForwardedHeaders(); // Sets scheme to HTTPS + app.UseHttpsRedirection(); // Sees HTTPS, no redirect + ``` + +2. **Cookie settings not compatible with proxy** + ```csharp + builder.Services.Configure(options => + { + options.MinimumSameSitePolicy = SameSiteMode.None; // For cross-site scenarios + options.Secure = CookieSecurePolicy.Always; // Requires HTTPS + }); + ``` + +3. **Cookie domain mismatch** + ```csharp + // If subdomain issues, may need to set cookie domain + builder.Services.ConfigureApplicationCookie(options => + { + options.Cookie.Domain = ".contoso.com"; // Allows cookies across subdomains + }); + ``` + +--- + +## Best Practices + +### 1. Always Use Forwarded Headers Middleware + +```csharp +// For ANY deployment behind proxy/gateway/load balancer +builder.Services.Configure(options => +{ + options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | + ForwardedHeaders.XForwardedProto | + ForwardedHeaders.XForwardedHost; + options.KnownNetworks.Clear(); + options.KnownProxies.Clear(); +}); + +var app = builder.Build(); +app.UseForwardedHeaders(); // FIRST middleware! +``` + +### 2. Register All Redirect URIs + +``` +Production: https://app.contoso.com/signin-oidc +Staging: https://app-staging.azurewebsites.net/signin-oidc +Development: https://localhost:5001/signin-oidc +``` + +### 3. Test Redirect URI Generation + +```csharp +// Add diagnostics endpoint (development only!) +if (app.Environment.IsDevelopment()) +{ + app.MapGet("/debug/redirect-uri", (HttpContext context) => + { + var redirectUri = $"{context.Request.Scheme}://{context.Request.Host}{context.Request.PathBase}/signin-oidc"; + return Results.Ok(new { redirectUri }); + }).AllowAnonymous(); +} +``` + +### 4. Health Endpoint for Gateway Probes + +```csharp +// Must be BEFORE authentication middleware +app.MapHealthChecks("/health").AllowAnonymous(); + +app.UseAuthentication(); // Health endpoint bypasses this +``` + +### 5. Distributed Token Cache for Multi-Region + +```csharp +// Use Redis for token cache across regions +builder.Services.AddStackExchangeRedisCache(options => +{ + options.Configuration = builder.Configuration["Redis:ConnectionString"]; + options.InstanceName = "TokenCache_"; +}); + +builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd")) + .EnableTokenAcquisitionToCallDownstreamApi() + .AddDistributedTokenCaches(); +``` + +### 6. Configure Logging for Troubleshooting + +```csharp +builder.Logging.AddConfiguration(builder.Configuration.GetSection("Logging")); + +// In appsettings.json +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.Identity.Web": "Debug", + "Microsoft.AspNetCore.HttpOverrides": "Debug" + } + } +} +``` + +--- + +## Complete Example: Web App Behind Application Gateway + +### Application Code + +**Program.cs:** + +```csharp +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.Identity.Web; +using Microsoft.Identity.Web.UI; + +var builder = WebApplication.CreateBuilder(args); + +// Forwarded headers for Application Gateway +builder.Services.Configure(options => +{ + options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | + ForwardedHeaders.XForwardedProto | + ForwardedHeaders.XForwardedHost; + options.KnownNetworks.Clear(); + options.KnownProxies.Clear(); +}); + +// Authentication +builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd")) + .EnableTokenAcquisitionToCallDownstreamApi() + .AddMicrosoftGraph() + .AddInMemoryTokenCaches(); + +// Health checks +builder.Services.AddHealthChecks(); + +// Add Microsoft Identity UI for sign-in/sign-out +builder.Services.AddRazorPages() + .AddMicrosoftIdentityUI(); + +var app = builder.Build(); + +// Health endpoint (before authentication) +app.MapHealthChecks("/health").AllowAnonymous(); + +// Middleware order is critical +app.UseForwardedHeaders(); + +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler("/Error"); + app.UseHsts(); +} + +app.UseHttpsRedirection(); +app.UseStaticFiles(); +app.UseRouting(); +app.UseAuthentication(); +app.UseAuthorization(); +app.MapRazorPages(); +app.MapControllers(); + +app.Run(); +``` + +**appsettings.json:** + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + "ClientSecret": "your-client-secret", + "CallbackPath": "/signin-oidc", + "SignedOutCallbackPath": "/signout-callback-oidc" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.Identity.Web": "Information", + "Microsoft.AspNetCore.HttpOverrides": "Debug" + } + } +} +``` + +**Azure AD App Registration:** + +**Redirect URIs:** +``` +https://gateway.contoso.com/signin-oidc +https://gateway.contoso.com/signout-callback-oidc +``` + +**Front-channel logout URL:** +``` +https://gateway.contoso.com/signout-oidc +``` + +### Application Gateway Configuration + +**Backend Pool:** +- Name: `webapp-backend` +- Target: `webapp.azurewebsites.net` or IP addresses + +**HTTP Settings:** +- Name: `webapp-https-settings` +- Protocol: HTTPS +- Port: 443 +- Override backend path: No +- Pick host name from backend target: Yes +- Custom probe: Yes β `webapp-health-probe` + +**Health Probe:** +- Name: `webapp-health-probe` +- Protocol: HTTPS +- Pick host name from backend HTTP settings: Yes +- Path: `/health` +- Interval: 30 seconds +- Unhealthy threshold: 3 + +**Listener:** +- Name: `webapp-listener` +- Frontend IP: Public +- Protocol: HTTPS +- Port: 443 +- SSL certificate: Your certificate + +**Routing Rule:** +- Name: `webapp-rule` +- Rule type: Basic +- Listener: `webapp-listener` +- Backend target: `webapp-backend` +- HTTP settings: `webapp-https-settings` + +--- + +## See Also + +- **[Quickstart: Web App](../getting-started/quickstart-webapp.md)** - Basic web app authentication +- **[APIs Behind Gateways](api-gateways.md)** - Web API gateway patterns +- **[Token Cache Configuration](../authentication/token-cache/token-cache-README.md)** - Distributed caching for multi-region +- **[Customization Guide](customization.md)** - OpenIdConnect event handlers +- **[Logging & Diagnostics](logging.md)** - Troubleshooting authentication + +--- + +## Additional Resources + +- [ASP.NET Core Forwarded Headers Middleware](https://learn.microsoft.com/aspnet/core/host-and-deploy/proxy-load-balancer) +- [Azure Application Gateway Documentation](https://learn.microsoft.com/azure/application-gateway/) +- [Azure Front Door Documentation](https://learn.microsoft.com/azure/frontdoor/) +- [Configure ASP.NET Core to work with proxy servers](https://learn.microsoft.com/aspnet/core/host-and-deploy/proxy-load-balancer) + +--- + +**Microsoft.Identity.Web Version:** 3.14.1+ +**Last Updated:** October 28, 2025 diff --git a/docs/authentication/authorization.md b/docs/authentication/authorization.md new file mode 100644 index 000000000..e1a240a33 --- /dev/null +++ b/docs/authentication/authorization.md @@ -0,0 +1,680 @@ +# Authorization in Web APIs with Microsoft.Identity.Web + +This guide explains how to implement authorization in ASP.NET Core web APIs using Microsoft.Identity.Web. Authorization ensures that authenticated callers have the necessary **scopes** (delegated permissions) or **app permissions** (application permissions) to access protected resources. + +--- + +## π Table of Contents + +- [Overview](#overview) +- [Authorization Concepts](#authorization-concepts) +- [Scope Validation with RequiredScope](#scope-validation-with-requiredscope) +- [App Permissions with RequiredScopeOrAppPermission](#app-permissions-with-requiredscopeorapppermission) +- [Authorization Policies](#authorization-policies) +- [Tenant Filtering](#tenant-filtering) +- [Best Practices](#best-practices) + +--- + +## Overview + +### Authentication vs Authorization + +| Concept | Purpose | Result | +|---------|---------|--------| +| **Authentication** | Verify identity | 401 Unauthorized if fails | +| **Authorization** | Verify permissions | 403 Forbidden if insufficient | + +### What Gets Validated? + +When a web API receives an access token, Microsoft.Identity.Web validates: + +1. **Token signature** - Is it from a trusted authority? +2. **Token audience** - Is it intended for this API? +3. **Token expiration** - Is it still valid? +4. **Scopes/Roles** - Doe the client app and the subject (user) have the right permissions? + +This guide focuses on **#4 - validating scopes and app permissions**. + +--- + +## Authorization Concepts + +### Scopes (Delegated Permissions) + +**Used when:** A user delegates permission to an app to act on their behalf. + +**Token claim:** `scp` or `scope` for the client app +**Example values:** `"access_as_user"`, `"User.Read"`, `"Files.ReadWrite"` + +**Token claim:** `roles` +**Example values:** `"admin"`, `"SimpleUser"` for the user. + + +**Scenario:** Web API on behalf of signed-in user. + +### App Permissions (Application Permissions) + +**Used when:** Web API called by an app acting as itself (no user context), like a daemon/background service. + +**Token claim:** `roles` +**Example values:** `"Mail.Read.All"`, `"User.Read.All"` + +**Scenario:** Daemon app calls web API using client credentials. + +--- + +## Scope Validation with RequiredScope + +The `RequiredScope` attribute validates that the access token contains at least one of the specified scopes. + +### Quick Start + +**1. Enable authorization in your API:** + +```csharp +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Identity.Web; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")); + +builder.Services.AddAuthorization(); // Required for authorization + +var app = builder.Build(); + +app.UseAuthentication(); +app.UseAuthorization(); // Must be after UseAuthentication +app.MapControllers(); + +app.Run(); +``` + +**2. Protect controllers or actions:** + +```csharp +using Microsoft.AspNetCore.Authorization; +using Microsoft.Identity.Web.Resource; + +[Authorize] +[RequiredScope("access_as_user")] +public class TodoListController : ControllerBase +{ + [HttpGet] + public IActionResult GetTodos() + { + // Only accessible if token has "access_as_user" scope + return Ok(new[] { "Todo 1", "Todo 2" }); + } +} +``` + +### Usage Patterns + +#### Pattern 1: Hardcoded Scopes + +**Use when:** Scopes are fixed and known at development time. + +```csharp +[Authorize] +[RequiredScope("access_as_user")] +public class TodoListController : ControllerBase +{ + // All actions require "access_as_user" scope +} +``` + +**Multiple scopes (any one matches):** + +```csharp +[Authorize] +[RequiredScope("read", "write", "admin")] +public class TodoListController : ControllerBase +{ + // Token must have "read" OR "write" OR "admin" +} +``` + +#### Pattern 2: Scopes from Configuration + +**Use when:** Scopes should be configurable per environment. + +**appsettings.json:** +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-api-client-id", + "Scopes": "access_as_user read write" + } +} +``` + +**Controller:** +```csharp +[Authorize] +[RequiredScope(RequiredScopesConfigurationKey = "AzureAd:Scopes")] +public class TodoListController : ControllerBase +{ + // Scopes read from configuration +} +``` + +**β Advantage:** Change scopes without recompiling. + +#### Pattern 3: Action-Level Scopes + +**Use when:** Different actions require different permissions. + +```csharp +[Authorize] +public class TodoListController : ControllerBase +{ + [HttpGet] + [RequiredScope("read")] + public IActionResult GetTodos() + { + return Ok(todos); + } + + [HttpPost] + [RequiredScope("write")] + public IActionResult CreateTodo([FromBody] Todo todo) + { + // Only tokens with "write" scope can create + return CreatedAtAction(nameof(GetTodos), todo); + } + + [HttpDelete("{id}")] + [RequiredScope("admin")] + public IActionResult DeleteTodo(int id) + { + // Only tokens with "admin" scope can delete + return NoContent(); + } +} +``` + +### How It Works + +When a request arrives: + +1. ASP.NET Core authentication middleware validates the token +2. `RequiredScope` attribute checks for the `scp` or `scope` claim +3. If token contains at least one matching scope β β Request proceeds +4. If no matching scope found β β 403 Forbidden response + +**Error response example:** +```json +{ + "error": "insufficient_scope", + "error_description": "The token does not have the required scope 'access_as_user'." +} +``` + +--- + +## App Permissions with RequiredScopeOrAppPermission + +The `RequiredScopeOrAppPermission` attribute validates either **scopes** (delegated) OR **app permissions** (application). + +### When to Use + +**β Use `RequiredScopeOrAppPermission` when:** +- Your API serves both user-delegated apps AND daemon/service apps +- Same endpoint should accept tokens from web apps (scopes) or background services (app permissions) + +**β Use `RequiredScope` when:** +- Your API only serves user-delegated requests + +### Quick Start + +```csharp +using Microsoft.Identity.Web.Resource; + +[Authorize] +[RequiredScopeOrAppPermission( + AcceptedScope = new[] { "access_as_user" }, + AcceptedAppPermission = new[] { "TodoList.ReadWrite.All" } +)] +public class TodoListController : ControllerBase +{ + [HttpGet] + public IActionResult GetTodos() + { + // Accessible with EITHER: + // - User-delegated token with "access_as_user" scope, OR + // - App-only token with "TodoList.ReadWrite.All" app permission + return Ok(todos); + } +} +``` + +### Configuration-Based App Permissions + +**appsettings.json:** +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-api-client-id", + "Scopes": "access_as_user", + "AppPermissions": "TodoList.ReadWrite.All TodoList.Admin" + } +} +``` + +**Controller:** +```csharp +[Authorize] +[RequiredScopeOrAppPermission( + RequiredScopesConfigurationKey = "AzureAd:Scopes", + RequiredAppPermissionsConfigurationKey = "AzureAd:AppPermissions" +)] +public class TodoListController : ControllerBase +{ + // Scopes and app permissions from configuration +} +``` + +### Token Claim Differences + +| Token Type | Claim | Example Value | +|------------|-------|---------------| +| **User-delegated** | `scp` or `scope` | `"access_as_user User.Read"` | +| **App-only** | `roles` | `["TodoList.ReadWrite.All"]` | + +**Example: User-delegated token:** +```json +{ + "aud": "api://your-api-client-id", + "iss": "https://login.microsoftonline.com/.../v2.0", + "scp": "access_as_user", + "sub": "user-object-id", + ... +} +``` + +**Example: App-only token:** +```json +{ + "aud": "api://your-api-client-id", + "iss": "https://login.microsoftonline.com/.../v2.0", + "roles": ["TodoList.ReadWrite.All"], + "sub": "app-object-id", + ... +} +``` + +--- + +## Authorization Policies + +For more complex authorization scenarios, use ASP.NET Core authorization policies. + +### Why Use Policies? + +- **Centralized logic** - Define authorization rules once, reuse everywhere +- **Composable** - Combine multiple requirements (scopes + claims + custom logic) +- **Testable** - Easier to unit test authorization logic +- **Flexible** - Custom requirements beyond scope validation + +### Pattern 1: Policy with RequireScope + +```csharp +using Microsoft.Identity.Web; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")); + +builder.Services.AddAuthorization(options => +{ + options.AddPolicy("TodoReadPolicy", policyBuilder => + { + policyBuilder.RequireScope("read", "access_as_user"); + }); + + options.AddPolicy("TodoWritePolicy", policyBuilder => + { + policyBuilder.RequireScope("write", "admin"); + }); +}); + +var app = builder.Build(); +``` + +**Controller:** +```csharp +[Authorize] +public class TodoListController : ControllerBase +{ + [HttpGet] + [Authorize(Policy = "TodoReadPolicy")] + public IActionResult GetTodos() + { + return Ok(todos); + } + + [HttpPost] + [Authorize(Policy = "TodoWritePolicy")] + public IActionResult CreateTodo([FromBody] Todo todo) + { + return CreatedAtAction(nameof(GetTodos), todo); + } +} +``` + +### Pattern 2: Policy with ScopeAuthorizationRequirement + +```csharp +using Microsoft.Identity.Web; +using Microsoft.Identity.Web.Resource; + +builder.Services.AddAuthorization(options => +{ + options.AddPolicy("CustomPolicy", policyBuilder => + { + policyBuilder.AddRequirements( + new ScopeAuthorizationRequirement(new[] { "access_as_user" }) + ); + }); +}); +``` + +### Pattern 3: Default Policy (Applies to All [Authorize]) + +```csharp +builder.Services.AddAuthorization(options => +{ + var defaultPolicy = new AuthorizationPolicyBuilder() + .RequireScope("access_as_user") + .Build(); + + options.DefaultPolicy = defaultPolicy; +}); +``` + +Now every `[Authorize]` attribute automatically requires the "access_as_user" scope: + +```csharp +[Authorize] // Automatically requires "access_as_user" scope +public class TodoListController : ControllerBase +{ + // All actions protected by default policy +} +``` + +### Pattern 4: Combining Multiple Requirements + +```csharp +builder.Services.AddAuthorization(options => +{ + options.AddPolicy("AdminPolicy", policyBuilder => + { + policyBuilder.RequireScope("admin"); + policyBuilder.RequireRole("Admin"); // Also check role claim + policyBuilder.RequireAuthenticatedUser(); + }); +}); +``` + +### Pattern 5: Configuration-Based Policy + +```csharp +var requiredScopes = builder.Configuration["AzureAd:Scopes"]?.Split(' '); + +builder.Services.AddAuthorization(options => +{ + options.AddPolicy("ApiAccessPolicy", policyBuilder => + { + if (requiredScopes != null) + { + policyBuilder.RequireScope(requiredScopes); + } + }); +}); +``` + +--- + +## Tenant Filtering + +Restrict API access to users from specific tenants only. + +### Use Case + +**Scenario:** Your multi-tenant API should only accept tokens from approved customer tenants. + +### Implementation + +```csharp +builder.Services.AddAuthorization(options => +{ + string[] allowedTenants = + { + "14c2f153-90a7-4689-9db7-9543bf084dad", // Contoso tenant + "af8cc1a0-d2aa-4ca7-b829-00d361edb652", // Fabrikam tenant + "979f4440-75dc-4664-b2e1-2cafa0ac67d1" // Northwind tenant + }; + + options.AddPolicy("AllowedTenantsOnly", policyBuilder => + { + policyBuilder.RequireClaim( + "http://schemas.microsoft.com/identity/claims/tenantid", + allowedTenants + ); + }); + + // Apply to all endpoints by default + options.DefaultPolicy = options.GetPolicy("AllowedTenantsOnly"); +}); +``` + +### Configuration-Based Tenant Filtering + +**appsettings.json:** +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "ClientId": "your-api-client-id", + "AllowedTenants": [ + "14c2f153-90a7-4689-9db7-9543bf084dad", + "af8cc1a0-d2aa-4ca7-b829-00d361edb652" + ] + } +} +``` + +**Startup:** +```csharp +var allowedTenants = builder.Configuration.GetSection("AzureAd:AllowedTenants") + .Get(); + +builder.Services.AddAuthorization(options => +{ + options.AddPolicy("AllowedTenantsOnly", policyBuilder => + { + policyBuilder.RequireClaim( + "http://schemas.microsoft.com/identity/claims/tenantid", + allowedTenants ?? Array.Empty() + ); + }); +}); +``` + +### Combined: Scopes + Tenant Filtering + +```csharp +builder.Services.AddAuthorization(options => +{ + options.AddPolicy("SecureApiAccess", policyBuilder => + { + // Require specific scope + policyBuilder.RequireScope("access_as_user"); + + // AND require specific tenant + policyBuilder.RequireClaim( + "http://schemas.microsoft.com/identity/claims/tenantid", + allowedTenants + ); + }); +}); +``` + +--- + +## Best Practices + +### β Do's + +**1. In Web APIs, always use `[Authorize]` with scope validation:** +```csharp +[Authorize] // Authentication +[RequiredScope("access_as_user")] // Authorization +public class MyController : ControllerBase { } +``` + +**2. Use configuration for environment-specific scopes:** +```csharp +[RequiredScope(RequiredScopesConfigurationKey = "AzureAd:Scopes")] +``` + +**3. Apply least privilege:** +```csharp +[HttpGet] +[RequiredScope("read")] // Only read permission needed + +[HttpPost] +[RequiredScope("write")] // Write permission for modifications +``` + +**4. Use policies for complex authorization:** +```csharp +builder.Services.AddAuthorization(options => +{ + options.AddPolicy("AdminOnly", policy => + { + policy.RequireScope("admin"); + policy.RequireClaim("department", "IT"); + }); +}); +``` + +**5. Enable detailed error responses in development:** +```csharp +if (builder.Environment.IsDevelopment()) +{ + Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true; +} +``` + +### β Don'ts + +**1. Don't skip `[Authorize]` when using `RequiredScope`:** +```csharp +// β Wrong - RequiredScope won't work without [Authorize] +[RequiredScope("access_as_user")] +public class MyController : ControllerBase { } + +// β Correct +[Authorize] +[RequiredScope("access_as_user")] +public class MyController : ControllerBase { } +``` + +**2. Don't hardcode tenant IDs in production:** +```csharp +// β Wrong +policyBuilder.RequireClaim("tid", "14c2f153-90a7-4689-9db7-9543bf084dad"); + +// β Better - use configuration +var tenants = Configuration.GetSection("AllowedTenants").Get(); +policyBuilder.RequireClaim("tid", tenants); +``` + +**3. Don't confuse scopes with roles:** +```csharp +// β Wrong - This checks roles claim, not scopes +[RequiredScope("Admin")] // "Admin" is typically a role, not a scope + +// β Correct +[RequiredScope("access_as_user")] // Scope +[Authorize(Roles = "Admin")] // Role +``` + +**4. Don't expose sensitive scope information in error messages (production):** + +Configure appropriate logging levels and error handling for production environments. + +--- + +## Troubleshooting + +### 403 Forbidden - Missing Scope + +**Error:** API returns 403 even with valid token. + +**Diagnosis:** +1. Decode token at [jwt.ms](https://jwt.ms) +2. Check `scp` or `scope` claim +3. Verify it matches your `RequiredScope` attribute + +**Solution:** +- Ensure client app requests the correct scope when acquiring token +- Verify scope is exposed in API app registration +- Grant admin consent if required + +### RequiredScope Not Working + +**Symptom:** Attribute seems to be ignored. + +**Check:** +1. Did you add `[Authorize]` attribute? +2. Is `app.UseAuthorization()` called after `app.UseAuthentication()`? +3. Is `services.AddAuthorization()` registered? + +### Configuration Key Not Found + +**Error:** Scope validation fails silently. + +**Check:** +```json +{ + "AzureAd": { + "Scopes": "access_as_user" // Matches RequiredScopesConfigurationKey + } +} +``` + +Ensure configuration path matches exactly. + +--- + +## See Also + +- **[Customization Guide](../advanced/customization.md)** - Configure authentication options and event handlers +- **[Logging & Diagnostics](../advanced/logging.md)** - Troubleshoot authentication and authorization issues with detailed logging +- **[Quickstart: Web API](../getting-started/quickstart-webapi.md)** - Get started with API protection +- **[Token Cache](token-cache/token-cache-README.md)** - Configure token caching for optimal performance + +--- + +## Additional Resources + +- [ASP.NET Core Authorization](https://learn.microsoft.com/aspnet/core/security/authorization/introduction) +- [Claims-based Authorization](https://learn.microsoft.com/aspnet/core/security/authorization/claims) +- [Policy-based Authorization](https://learn.microsoft.com/aspnet/core/security/authorization/policies) +- [Microsoft Identity Platform Scopes](https://learn.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent) +- [Protected Web API Overview](https://learn.microsoft.com/azure/active-directory/develop/scenario-protected-web-api-overview) + +--- + +**Last Updated:** October 27, 2025 +**Microsoft.Identity.Web Version:** 3.14.1+ diff --git a/docs/authentication/credentials/certificateless.md b/docs/authentication/credentials/certificateless.md new file mode 100644 index 000000000..ffb69591a --- /dev/null +++ b/docs/authentication/credentials/certificateless.md @@ -0,0 +1,697 @@ +# Certificateless Authentication (FIC + Managed Identity) + +Certificateless authentication eliminates the need to manage certificates by using Azure Managed Identity and Federated Identity Credentials (FIC). This is the **recommended approach for production applications** running on Azure. + +## Overview + +### What is Certificateless Authentication? + +Certificateless authentication uses **Federated Identity Credential (FIC)** combined with **Azure Managed Identity** to authenticate your application without requiring certificates or client secrets. Instead of proving your application's identity with a certificate, Azure issues signed assertions on behalf of your application. + +### Why Choose Certificateless? + +**Zero credential management:** +- β No certificates to create, store, or rotate +- β No secrets to manage or secure +- β No expiration dates to track +- β No Key Vault costs for credential storage + +**Automatic security:** +- β Credentials never leave Azure +- β Automatic rotation handled by Azure +- β Reduced attack surface (no credentials to leak) +- β Built-in Azure security best practices + +**Operational benefits:** +- β Simplified deployment +- β Lower maintenance overhead +- β Reduced security incidents +- β Cost-effective (no certificate costs) + +### How It Works + +```mermaid +sequenceDiagram + participant App as Your Application Code + participant MIW as Microsoft.Identity.Web + participant MSI as Azure Managed Identity + participant AAD as Microsoft Entra ID + participant API as Downstream API + + Note over App,MIW: π You write minimal code + App->>MIW: Call API (e.g., IDownstreamApi) + + Note over MIW,AAD: β¨ Microsoft.Identity.Web handles everything below + rect rgba(70, 130, 180, 0.2) + Note right of MIW: Automatic token acquisition + MIW->>MSI: Request signed assertion + MSI->>MSI: Generate assertion usingmanaged identity + MSI->>MIW: Return signed assertion + MIW->>AAD: Request access tokenwith assertion + AAD->>AAD: Validate assertionand FIC trust + AAD->>MIW: Return access token + MIW->>MIW: Cache token + end + + MIW->>API: Call API with token + API->>MIW: Return response + MIW->>App: Return data + + Note over App,MIW: π Your code gets the result +``` + +**Key components:** + +1. **Managed Identity** - Azure resource that represents your application's identity +2. **Federated Identity Credential (FIC)** - Trust relationship configured in your app registration +3. **Signed Assertion** - Token issued by Managed Identity proving your app's identity +4. **Access Token** - Token from Microsoft Entra ID used to call APIs + +--- + +## Prerequisites + +### Azure Resources Required + +1. **Azure Subscription** - Certificateless authentication requires Azure +2. **Managed Identity** - System-assigned or user-assigned +3. **App Registration** - In Microsoft Entra ID with FIC configured +4. **Azure Resource** - App Service, Container Apps, VM, AKS, etc. + +### Supported Azure Services + +Certificateless authentication works with any Azure service that supports Managed Identity: + +- β Azure App Service +- β Azure Functions +- β Azure Container Apps +- β Azure Kubernetes Service (AKS) +- β Azure Virtual Machines +- β Azure Container Instances +- β Azure Logic Apps +- β Azure Service Fabric + +--- + +## Configuration + +### Step 1: Enable Managed Identity + +#### System-Assigned Managed Identity (Recommended for Single App) + +**Azure Portal:** +1. Navigate to your Azure resource (e.g., App Service) +2. Select **Identity** from the left menu +3. Under **System assigned** tab, set **Status** to **On** +4. Click **Save** +5. Note the **Object (principal) ID** - you'll need this for FIC setup + +**Azure CLI:** +```bash +# Enable system-assigned managed identity +az webapp identity assign --name --resource-group + +# Get the principal ID +az webapp identity show --name --resource-group --query principalId -o tsv +``` + +**Benefits of system-assigned:** +- β Automatically created with the resource +- β Lifecycle tied to the resource (deleted when resource is deleted) +- β Simplest setup + +#### User-Assigned Managed Identity (Recommended for Multiple Apps) + +**Azure Portal:** +1. Search for **Managed Identities** in Azure Portal +2. Click **Create** +3. Enter name, subscription, resource group, location +4. Click **Review + Create**, then **Create** +5. After creation, note the **Client ID** and **Principal ID** +6. Assign the identity to your Azure resource(s) + +**Azure CLI:** +```bash +# Create user-assigned managed identity +az identity create --name --resource-group + +# Get the client ID and principal ID +az identity show --name --resource-group + +# Assign to your app +az webapp identity assign --name --resource-group --identities +``` + +**Benefits of user-assigned:** +- β Can be shared across multiple resources +- β Independent lifecycle from resources +- β Easier to manage permissions centrally + +--- + +### Step 2: Configure Federated Identity Credential + +The Federated Identity Credential (FIC) establishes trust between your app registration and the managed identity. + +#### Azure Portal + +1. Navigate to **Microsoft Entra ID** > **App registrations** +2. Select your application +3. Click **Certificates & secrets** +4. Select **Federated credentials** tab +5. Click **Add credential** +6. Select scenario: **Other issuer** +7. Configure the credential: + - **Issuer:** (depends on Azure service - see table below) + - **Subject identifier:** (depends on managed identity type - see below) + - **Name:** Descriptive name (e.g., "MyApp-Production-FIC") + - **Audience:** `api://AzureADTokenExchange` (default) +8. Click **Add** + +#### Azure CLI + +```bash +# Create federated identity credential +az ad app federated-credential create \ + --id \ + --parameters '{ + "name": "MyApp-Production-FIC", + "issuer": "", + "subject": "", + "audiences": ["api://AzureADTokenExchange"], + "description": "FIC for production environment" + }' +``` + +#### Issuer URLs by Azure Service + +| Azure Service | Issuer URL | +|---------------|------------| +| **App Service / Functions** | `https://login.microsoftonline.com//v2.0` | +| **Container Apps** | `https://login.microsoftonline.com//v2.0` | +| **AKS** | `https://oidc.prod-aks.azure.com/` | +| **Virtual Machines** | `https://login.microsoftonline.com//v2.0` | + +#### Subject Identifier by Managed Identity Type + +**System-Assigned Managed Identity:** +``` +microsoft:azure:::: +``` + +Example for App Service: +``` +microsoft:azure:websites:12345678-1234-1234-1234-123456789012:my-resource-group:my-app-name +``` + +**User-Assigned Managed Identity:** +``` +microsoft:azure:managed-identity::: +``` + +Example: +``` +microsoft:azure:managed-identity:12345678-1234-1234-1234-123456789012:my-resource-group:my-identity +``` + +--- + +### Step 3: Configure Your Application + +#### JSON Configuration (appsettings.json) + +**Using System-Assigned Managed Identity:** + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + "ClientCredentials": [ + { + "SourceType": "SignedAssertionFromManagedIdentity" + } + ] + } +} +``` + +**Using User-Assigned Managed Identity:** + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + "ClientCredentials": [ + { + "SourceType": "SignedAssertionFromManagedIdentity", + "ManagedIdentityClientId": "user-assigned-identity-client-id" + } + ] + } +} +``` + +#### Code Configuration + +**ASP.NET Core Web App:** + +```csharp +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.Identity.Web; +using Microsoft.Identity.Abstractions; + +var builder = WebApplication.CreateBuilder(args); + +// System-assigned managed identity +builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(options => + { + options.Instance = "https://login.microsoftonline.com/"; + options.TenantId = "your-tenant-id"; + options.ClientId = "your-client-id"; + options.ClientCredentials = new[] + { + new CredentialDescription + { + SourceType = CredentialSource.SignedAssertionFromManagedIdentity + } + }; + }); + +// User-assigned managed identity +builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(options => + { + options.Instance = "https://login.microsoftonline.com/"; + options.TenantId = "your-tenant-id"; + options.ClientId = "your-client-id"; + options.ClientCredentials = new[] + { + new CredentialDescription + { + SourceType = CredentialSource.SignedAssertionFromManagedIdentity, + ManagedIdentityClientId = "user-assigned-identity-client-id" + } + }; + }); +``` + +**ASP.NET Core Web API:** + +```csharp +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Identity.Web; +using Microsoft.Identity.Abstractions; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(options => + { + options.Instance = "https://login.microsoftonline.com/"; + options.TenantId = "your-tenant-id"; + options.ClientId = "your-client-id"; + options.ClientCredentials = new[] + { + new CredentialDescription + { + SourceType = CredentialSource.SignedAssertionFromManagedIdentity, + ManagedIdentityClientId = "optional-user-assigned-client-id" + } + }; + }) + .EnableTokenAcquisitionToCallDownstreamApi() + .AddInMemoryTokenCaches(); +``` + +**Daemon Application (Console/Worker Service):** + +```csharp +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Web; + +var tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance(); + +// System-assigned (no ManagedIdentityClientId needed) +// User-assigned (include ManagedIdentityClientId in appsettings.json) + +var sp = tokenAcquirerFactory.Build(); + +// Downstream API calls will automatically use certificateless authentication +var api = sp.GetRequiredService(); +var result = await api.GetForAppAsync("MyApi"); +``` + +--- + +## System-Assigned vs User-Assigned Managed Identity + +### When to Use System-Assigned + +**Use system-assigned when:** +- β You have a single application per Azure resource +- β You want the simplest setup +- β Identity lifecycle should match resource lifecycle +- β You don't need to share identity across resources + +**Example scenario:** +A production web app deployed to a dedicated App Service that calls Microsoft Graph. + +### When to Use User-Assigned + +**Use user-assigned when:** +- β Multiple applications need the same identity +- β You want to manage identity separately from resources +- β You need consistent identity across resource updates +- β You want to pre-configure permissions before deployment + +**Example scenario:** +A microservices architecture where multiple container instances need to call the same APIs with the same permissions. + +### Comparison Table + +| Feature | System-Assigned | User-Assigned | +|---------|----------------|---------------| +| **Lifecycle** | Tied to resource | Independent | +| **Sharing** | One resource only | Multiple resources | +| **Setup complexity** | Simpler | Slightly more complex | +| **Permission management** | Per resource | Centralized | +| **Use case** | Single-app scenarios | Multi-app scenarios | +| **Cost** | No additional cost | No additional cost | + +--- + +## Advanced Configuration + +### Multiple Managed Identities + +If you have multiple user-assigned managed identities, specify which one to use: + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + "ClientCredentials": [ + { + "SourceType": "SignedAssertionFromManagedIdentity", + "ManagedIdentityClientId": "identity-1-client-id" + } + ] + } +} +``` + +### Fallback Credentials + +You can configure fallback credentials for local development or migration scenarios: + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + "ClientCredentials": [ + { + "SourceType": "SignedAssertionFromManagedIdentity" + }, + { + "SourceType": "ClientSecret", + "ClientSecret": "dev-only-secret" + } + ] + } +} +``` + +Microsoft.Identity.Web tries credentials in order. In Azure, it uses managed identity. Locally (where managed identity isn't available), it falls back to client secret. + +### Environment-Specific Configuration + +Use different configurations per environment: + +**appsettings.Production.json:** +```json +{ + "AzureAd": { + "ClientCredentials": [ + { + "SourceType": "SignedAssertionFromManagedIdentity" + } + ] + } +} +``` + +**appsettings.Development.json:** +```json +{ + "AzureAd": { + "ClientCredentials": [ + { + "SourceType": "ClientSecret", + "ClientSecret": "dev-secret" + } + ] + } +} +``` + +--- + +## Migration from Certificates + +### Migration Strategy + +**Step 1: Add FIC alongside existing certificate** + +Configure both credentials: + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + "ClientCredentials": [ + { + "SourceType": "SignedAssertionFromManagedIdentity" + }, + { + "SourceType": "KeyVault", + "KeyVaultUrl": "https://your-keyvault.vault.azure.net", + "KeyVaultCertificateName": "YourCertificate" + } + ] + } +} +``` + +**Step 2: Test certificateless in non-production** + +Deploy to staging/test environment and verify: +- β Authentication works +- β API calls succeed +- β No certificate-related errors + +**Step 3: Deploy to production** + +Once validated, deploy to production with certificateless as primary. + +**Step 4: Remove certificate** + +After confirming stability: +1. Remove certificate from configuration +2. Delete FIC from app registration (if not needed) +3. Remove certificate from Key Vault (if not used elsewhere) + +### Migration Checklist + +- [ ] Enable managed identity on Azure resource +- [ ] Configure FIC in app registration +- [ ] Test with both credentials in staging +- [ ] Monitor authentication metrics +- [ ] Deploy to production +- [ ] Verify production authentication +- [ ] Remove certificate configuration +- [ ] Clean up unused certificates + +--- + +## Troubleshooting + +### Common Issues + +#### Problem: "Failed to get managed identity token" + +**Possible causes:** +- Managed identity not enabled on the resource +- Application not running on Azure +- Network connectivity issues to managed identity endpoint + +**Solutions:** +1. Verify managed identity is enabled: + ```bash + az webapp identity show --name --resource-group + ``` +2. Check that you're running on Azure (not locally without fallback) +3. Verify network connectivity: + ```bash + # From within the Azure resource + curl "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/" -H "Metadata: true" + ``` + +#### Problem: "The provided client credential is invalid" + +**Possible causes:** +- FIC not configured in app registration +- Subject identifier mismatch +- Issuer URL incorrect + +**Solutions:** +1. Verify FIC exists in app registration: + - Go to app registration > Certificates & secrets > Federated credentials + - Confirm credential is present +2. Double-check subject identifier format matches your resource type +3. Verify issuer URL matches your Azure service +4. Ensure audience is `api://AzureADTokenExchange` + +#### Problem: "User-assigned identity not found" + +**Possible causes:** +- Managed identity client ID incorrect +- Identity not assigned to the resource +- Typo in configuration + +**Solutions:** +1. Verify user-assigned identity is attached to resource: + ```bash + az webapp identity show --name --resource-group + ``` +2. Check the client ID matches exactly: + ```bash + az identity show --name --resource-group --query clientId + ``` +3. Verify `ManagedIdentityClientId` in configuration matches + +#### Problem: Works locally but fails in Azure + +**Possible causes:** +- Fallback credential (client secret) used locally +- FIC not configured for Azure environment +- Environment-specific configuration missing + +**Solutions:** +1. Check if fallback credentials are configured +2. Verify FIC is configured for Azure environment +3. Review environment-specific configuration files +4. Check Azure App Service configuration settings + +--- + +## Security Best Practices + +### Principle of Least Privilege + +Grant managed identity only the permissions it needs: + +```bash +# Example: Grant managed identity read access to Key Vault +az keyvault set-policy \ + --name \ + --object-id \ + --secret-permissions get \ + --certificate-permissions get +``` + +### Monitor Access + +- β Enable diagnostic logging for the app registration +- β Monitor sign-in logs for the managed identity +- β Set up alerts for authentication failures +- β Review permissions regularly + +### Rotate FIC Credentials + +While FIC doesn't require manual rotation, you should: + +- β Review FIC configurations annually +- β Remove unused FICs +- β Update FICs when resources change +- β Document FIC purpose and owner + +### Network Security + +- β Use private endpoints where possible +- β Restrict network access to Azure resources +- β Use Azure Private Link for Key Vault access +- β Enable Azure DDoS Protection + +--- + +## Performance Considerations + +### Token Caching + +Certificateless authentication benefits from token caching: + +```csharp +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(options => { /* ... */ }) + .EnableTokenAcquisitionToCallDownstreamApi() + .AddDistributedTokenCaches(); // Use distributed cache for scale +``` + +### Managed Identity Performance + +- β **System-assigned:** Slightly faster (no client ID lookup) +- β **User-assigned:** Minimal overhead with proper configuration +- β **Assertion caching:** Managed automatically by Azure +- β **Token caching:** Configure appropriately for your scenario + +--- + +## Cost Considerations + +### Cost Benefits of Certificateless + +**Eliminated costs:** +- β Certificate purchase/renewal +- β Key Vault storage for certificates (if only used for this) +- β Certificate management tools/services +- β Engineering time for certificate rotation + +**Remaining costs:** +- β Azure resource costs (you'd have these anyway) +- β Managed identity (no additional cost) +- β Token requests (included in Azure service costs) + +**Typical savings:** 80-90% reduction in authentication-related costs + +--- + +## Additional Resources + +- **[Azure Managed Identities Overview](https://learn.microsoft.com/azure/active-directory/managed-identities-azure-resources/overview)** - Complete managed identity documentation +- **[Federated Identity Credentials](https://learn.microsoft.com/entra/workload-id/workload-identity-federation)** - FIC deep dive +- **[Workload Identity Federation](https://learn.microsoft.com/azure/active-directory/develop/workload-identity-federation)** - Conceptual overview +- **[Microsoft.Identity.Web Samples](https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2)** - Working examples + +--- + +## Next Steps + +- **[Back to Credentials Overview](./credentials-README.md)** - Compare all credential types +- **[Certificates Guide](./certificates.md)** - Alternative authentication methods +- **[Calling Downstream APIs](../../calling-downstream-apis/calling-downstream-apis-README.md)** - Use certificateless auth to call APIs + +--- + +**Need help?** [Open an issue](https://github.com/AzureAD/microsoft-identity-web/issues) or check [troubleshooting guides](../../calling-downstream-apis/from-web-apps.md#troubleshooting). \ No newline at end of file diff --git a/docs/authentication/credentials/certificates.md b/docs/authentication/credentials/certificates.md new file mode 100644 index 000000000..d97518794 --- /dev/null +++ b/docs/authentication/credentials/certificates.md @@ -0,0 +1,962 @@ +# Certificate-Based Authentication + +Certificates provide strong cryptographic proof of your application's identity when authenticating to Microsoft Entra ID (formerly Azure AD). Microsoft.Identity.Web supports multiple ways to load and use certificates, from production-ready Key Vault integration to development-friendly file-based approaches. + +## Overview + +### What is Certificate-Based Authentication? + +Certificate-based authentication uses public-key cryptography to prove your application's identity. Your application signs a JSON Web Token (JWT) with its private key, and Microsoft Entra ID verifies the signature using the corresponding public key from your app registration. + +### Why Use Certificates? + +**Strong security:** +- β Stronger than client secrets (asymmetric vs symmetric keys) +- β Private key never transmitted over the network +- β Cryptographic proof of identity +- β Meets compliance requirements (FIPS, etc.) + +**Production-ready:** +- β Supported by security teams and IT operations +- β Integrates with enterprise PKI infrastructure +- β Hardware Security Module (HSM) support +- β Industry-standard credential type + +### Certificate vs Certificateless + +| Aspect | Certificates | Certificateless (FIC+MSI) | +|--------|-------------|---------------------------| +| **Management** | Manual or automated | Fully automatic | +| **Rotation** | Required (manual or with tools) | Automatic | +| **Azure dependency** | No (works anywhere) | Yes (Azure only) | +| **Cost** | Certificate costs | No certificate costs | +| **Compliance** | Often required | May not meet all requirements | +| **Setup complexity** | Moderate to high | Low to moderate | + +**When to use certificates:** +- β Compliance requires certificate-based authentication +- β Running outside Azure (on-premises, other clouds) +- β Existing PKI infrastructure +- β Organization policy mandates certificates + +**When to use certificateless:** +- β Running on Azure +- β Want to minimize management overhead +- β No specific certificate requirements + +See [Certificateless Authentication](./certificateless.md) for the alternative approach. + +--- + +## Certificate Types Supported + +Microsoft.Identity.Web supports four ways to load certificates: + +1. **[Azure Key Vault](#azure-key-vault)** β - Recommended for production +2. **[Certificate Store](#certificate-store)** - Windows production environments +3. **[File Path](#file-path)** - Development and simple deployments +4. **[Base64 Encoded](#base64-encoded)** - Configuration-embedded certificates + +--- + +## Azure Key Vault + +**Recommended for:** Production applications requiring centralized certificate management + +### Why Key Vault? + +**Centralized management:** +- β Single source of truth for certificates +- β Centralized access control and auditing +- β Automatic certificate renewal support +- β Versioning and rollback capabilities + +**Security benefits:** +- β Certificates never stored on disk +- β Access controlled by Azure RBAC or access policies +- β Activity logging and monitoring +- β Integration with managed identities + +**Operational benefits:** +- β Works across platforms (Windows, Linux, containers) +- β Share certificates across multiple applications +- β No certificate management on app servers +- β Simplified rotation and renewal + +### Prerequisites + +1. **Azure Key Vault** with a certificate +2. **Access permissions** for your application to read the certificate +3. **Network connectivity** from your application to Key Vault + +### Configuration + +#### JSON Configuration (appsettings.json) + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + "ClientCredentials": [ + { + "SourceType": "KeyVault", + "KeyVaultUrl": "https://your-keyvault.vault.azure.net", + "KeyVaultCertificateName": "YourCertificateName" + } + ] + } +} +``` + +#### C# Code Configuration + +```csharp +using Microsoft.Identity.Abstractions; + +// Using property initialization +var credentialDescription = new CredentialDescription +{ + SourceType = CredentialSource.KeyVault, + KeyVaultUrl = "https://your-keyvault.vault.azure.net", + KeyVaultCertificateName = "YourCertificateName" +}; + +// Using helper method +var credentialDescription = CredentialDescription.FromKeyVault( + "https://your-keyvault.vault.azure.net", + "YourCertificateName"); +``` + +#### ASP.NET Core Integration + +```csharp +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.Identity.Web; +using Microsoft.Identity.Abstractions; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(options => + { + options.Instance = "https://login.microsoftonline.com/"; + options.TenantId = "your-tenant-id"; + options.ClientId = "your-client-id"; + options.ClientCredentials = new[] + { + CredentialDescription.FromKeyVault( + "https://your-keyvault.vault.azure.net", + "YourCertificateName") + }; + }); +``` + +### Setup Guide + +#### Step 1: Create or Import Certificate in Key Vault + +**Using Azure Portal:** +1. Navigate to your Key Vault +2. Select **Certificates** from the left menu +3. Click **Generate/Import** +4. Choose **Generate** (for new) or **Import** (for existing) +5. Configure certificate properties: + - **Name:** Descriptive name (e.g., "MyApp-Prod-Cert") + - **Type:** Self-signed or CA-issued + - **Subject:** CN=YourAppName + - **Validity period:** 12-24 months + - **Content type:** PFX +6. Click **Create** + +**Using Azure CLI:** + +```bash +# Generate self-signed certificate in Key Vault +az keyvault certificate create \ + --vault-name \ + --name \ + --policy "$(az keyvault certificate get-default-policy)" + +# Import existing certificate +az keyvault certificate import \ + --vault-name \ + --name \ + --file /path/to/certificate.pfx \ + --password +``` + +#### Step 2: Grant Access to Your Application + +**Option A: Using Managed Identity (Recommended)** + +```bash +# Get your app's managed identity principal ID +PRINCIPAL_ID=$(az webapp identity show \ + --name \ + --resource-group \ + --query principalId -o tsv) + +# Grant access to certificates +az keyvault set-policy \ + --name \ + --object-id $PRINCIPAL_ID \ + --certificate-permissions get \ + --secret-permissions get +``` + +**Option B: Using Service Principal** + +```bash +# Grant access using service principal +az keyvault set-policy \ + --name \ + --spn \ + --certificate-permissions get \ + --secret-permissions get +``` + +**Option C: Using Azure RBAC** + +```bash +# Get your app's managed identity principal ID +PRINCIPAL_ID=$(az webapp identity show \ + --name \ + --resource-group \ + --query principalId -o tsv) + +# Assign Key Vault Secrets User role +az role assignment create \ + --role "Key Vault Secrets User" \ + --assignee $PRINCIPAL_ID \ + --scope /subscriptions//resourceGroups//providers/Microsoft.KeyVault/vaults/ +``` + +#### Step 3: Upload Public Key to App Registration + +1. Navigate to **Microsoft Entra ID** > **App registrations** +2. Select your application +3. Click **Certificates & secrets** +4. Under **Certificates** tab, click **Upload certificate** +5. Download the public key (.cer) from Key Vault: + ```bash + az keyvault certificate download \ + --vault-name \ + --name \ + --file certificate.cer \ + --encoding DER + ``` +6. Upload the .cer file +7. Add a description and click **Add** + +### Automatic Certificate Renewal + +**Key Vault supports automatic certificate renewal:** + +1. Configure renewal policy in Key Vault: + ```bash + az keyvault certificate set-attributes \ + --vault-name \ + --name \ + --policy @policy.json + ``` + +2. Example policy.json: + ```json + { + "lifetimeActions": [ + { + "trigger": { + "daysBeforeExpiry": 30 + }, + "action": { + "actionType": "AutoRenew" + } + } + ], + "issuerParameters": { + "name": "Self" + } + } + ``` + +3. Update app registration with new public key when renewed +4. Microsoft.Identity.Web automatically picks up the latest version from Key Vault + +--- + +## Certificate Store + +**Recommended for:** Production Windows applications using enterprise certificate management + +### Why Certificate Store? + +**Windows integration:** +- β Native Windows certificate management +- β IT-managed certificate lifecycle +- β Hardware Security Module (HSM) support +- β Group Policy deployment + +**Enterprise scenarios:** +- β Existing PKI infrastructure +- β Centralized certificate management +- β Compliance requirements +- β On-premises deployments + +### Certificate Store Locations + +| Store Path | Description | Use When | +|------------|-------------|----------| +| **CurrentUser/My** | Current user's personal certificates | Service runs as user account | +| **LocalMachine/My** | Computer's personal certificates | Service runs as system account or service identity | +| **CurrentUser/Root** | Trusted root CAs (user) | Validating certificate chains | +| **LocalMachine/Root** | Trusted root CAs (computer) | System-level certificate trust | + +### Configuration: Using Thumbprint + +**Best for:** Static certificate deployment + +#### JSON Configuration + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + "ClientCredentials": [ + { + "SourceType": "StoreWithThumbprint", + "CertificateStorePath": "CurrentUser/My", + "CertificateThumbprint": "A1B2C3D4E5F6789012345678901234567890ABCD" + } + ] + } +} +``` + +#### C# Code Configuration + +```csharp +using Microsoft.Identity.Abstractions; + +var credentialDescription = CredentialDescription.FromCertificateStore( + "CurrentUser/My", + thumbprint: "A1B2C3D4E5F6789012345678901234567890ABCD"); +``` + +**Note:** Thumbprint changes when certificate is renewed, requiring configuration updates. + +### Configuration: Using Distinguished Name + +**Best for:** Automatic certificate rotation + +#### JSON Configuration + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + "ClientCredentials": [ + { + "SourceType": "StoreWithDistinguishedName", + "CertificateStorePath": "CurrentUser/My", + "CertificateDistinguishedName": "CN=MyAppCertificate" + } + ] + } +} +``` + +#### C# Code Configuration + +```csharp +using Microsoft.Identity.Abstractions; + +var credentialDescription = CredentialDescription.FromCertificateStore( + "CurrentUser/My", + distinguishedName: "CN=MyAppCertificate"); +``` + +**Benefit:** When certificate is renewed with the same distinguished name, Microsoft.Identity.Web automatically uses the newest certificate without configuration changes. + +### Setup Guide + +#### Step 1: Generate or Import Certificate + +**Option A: Generate Self-Signed Certificate (Development)** + +```powershell +# PowerShell: Generate self-signed certificate +$cert = New-SelfSignedCertificate ` + -Subject "CN=MyAppCertificate" ` + -CertStoreLocation "Cert:\CurrentUser\My" ` + -KeyExportPolicy Exportable ` + -KeySpec Signature ` + -KeyLength 2048 ` + -KeyAlgorithm RSA ` + -HashAlgorithm SHA256 ` + -NotAfter (Get-Date).AddYears(2) + +# Export public key for app registration +Export-Certificate -Cert $cert -FilePath "MyAppCertificate.cer" + +# View thumbprint +$cert.Thumbprint +``` + +**Option B: Import Existing Certificate** + +```powershell +# PowerShell: Import PFX certificate +$pfxPath = "C:\path\to\certificate.pfx" +$pfxPassword = ConvertTo-SecureString -String "your-password" -Force -AsPlainText + +Import-PfxCertificate ` + -FilePath $pfxPath ` + -CertStoreLocation "Cert:\CurrentUser\My" ` + -Password $pfxPassword +``` + +**Option C: Enterprise PKI Deployment** + +Use Group Policy or SCCM to deploy certificates to target machines. + +#### Step 2: Grant Application Access to Private Key + +```powershell +# PowerShell: Grant IIS App Pool identity access to private key +$cert = Get-ChildItem -Path "Cert:\LocalMachine\My" | Where-Object {$_.Thumbprint -eq "YOUR_THUMBPRINT"} + +$rsaCert = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert) +$fileName = $rsaCert.Key.UniqueName + +$path = "$env:ALLUSERSPROFILE\Microsoft\Crypto\RSA\MachineKeys\$fileName" + +# Grant Read permission to IIS App Pool identity +icacls $path /grant "IIS APPPOOL\YourAppPoolName:R" +``` + +#### Step 3: Upload Public Key to App Registration + +1. Navigate to **Microsoft Entra ID** > **App registrations** +2. Select your application +3. Click **Certificates & secrets** +4. Under **Certificates** tab, click **Upload certificate** +5. Upload the .cer file exported in Step 1 +6. Add a description and click **Add** + +### Certificate Rotation + +**Using Distinguished Name (Recommended):** + +1. Deploy new certificate with same CN to certificate store +2. Ensure new certificate is valid and not expired +3. Microsoft.Identity.Web automatically selects newest valid certificate +4. Remove old certificate after grace period + +**Using Thumbprint:** + +1. Deploy new certificate to certificate store +2. Update configuration with new thumbprint +3. Restart application +4. Remove old certificate + +--- + +## File Path + +**Recommended for:** Development, testing, and simple deployments + +### Why File Path? + +**Simple setup:** +- β Easy to deploy certificate with application +- β No external dependencies +- β Works on any platform +- β Container-friendly + +**Development scenarios:** +- β Local development +- β Automated testing +- β CI/CD pipelines (with secure file handling) +- β Simple container deployments + +**β οΈ Security Warning:** Not recommended for production. Use Key Vault or Certificate Store for production workloads. + +### Configuration + +#### JSON Configuration + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + "ClientCredentials": [ + { + "SourceType": "Path", + "CertificateDiskPath": "/app/certificates/mycert.pfx", + "CertificatePassword": "certificate-password" + } + ] + } +} +``` + +#### C# Code Configuration + +```csharp +using Microsoft.Identity.Abstractions; + +var credentialDescription = CredentialDescription.FromCertificatePath( + "/app/certificates/mycert.pfx", + "certificate-password"); +``` + +### Setup Guide + +#### Step 1: Generate or Export Certificate + +**Generate Self-Signed (Development):** + +```bash +# Linux/macOS: Generate self-signed certificate with OpenSSL +openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes -subj "/CN=MyAppCertificate" + +# Create PFX from PEM files +openssl pkcs12 -export -out mycert.pfx -inkey key.pem -in cert.pem -passout pass:your-password + +# Extract public key for app registration +openssl pkcs12 -in mycert.pfx -clcerts -nokeys -out public-cert.cer -passin pass:your-password +``` + +**Windows PowerShell:** + +```powershell +# Generate self-signed and export to PFX +$cert = New-SelfSignedCertificate ` + -Subject "CN=MyAppCertificate" ` + -CertStoreLocation "Cert:\CurrentUser\My" ` + -KeyExportPolicy Exportable ` + -KeySpec Signature + +$pfxPassword = ConvertTo-SecureString -String "your-password" -Force -AsPlainText + +Export-PfxCertificate -Cert $cert -FilePath "mycert.pfx" -Password $pfxPassword +Export-Certificate -Cert $cert -FilePath "public-cert.cer" +``` + +#### Step 2: Secure the Certificate File + +**File permissions:** + +```bash +# Linux: Restrict access to certificate file +chmod 600 /app/certificates/mycert.pfx +chown app-user:app-group /app/certificates/mycert.pfx +``` + +**Container secrets (Docker):** + +```dockerfile +# Dockerfile: Copy certificate securely +COPY --chown=app:app certificates/mycert.pfx /app/certificates/ +RUN chmod 600 /app/certificates/mycert.pfx +``` + +**Environment-specific paths:** + +```json +{ + "AzureAd": { + "ClientCredentials": [ + { + "SourceType": "Path", + "CertificateDiskPath": "/app/certificates/${ENVIRONMENT}-cert.pfx", + "CertificatePassword": "${CERT_PASSWORD}" + } + ] + } +} +``` + +#### Step 3: Upload Public Key to App Registration + +(Same as Certificate Store Step 3) + +### Security Best Practices for File-Based Certificates + +**DO:** +- β Use restrictive file permissions (600 on Linux, ACLs on Windows) +- β Store password separately (Key Vault, environment variable, secrets manager) +- β Encrypt file system where certificate is stored +- β Use container secrets for containerized apps +- β Rotate certificates regularly + +**DON'T:** +- β Commit certificates to source control +- β Store passwords in plaintext configuration +- β Use world-readable file permissions +- β Leave certificates on disk after deployment (if possible to load into memory) +- β Use in production (prefer Key Vault or Certificate Store) + +--- + +## Base64 Encoded + +**Recommended for:** Development, testing, and configuration-embedded certificates + +### Why Base64 Encoded? + +**Configuration simplicity:** +- β Certificate embedded in configuration +- β No file system dependency +- β Easy to pass via environment variables +- β Works in serverless environments + +**Container scenarios:** +- β Kubernetes secrets +- β Docker environment variables +- β Configuration management tools + +**β οΈ Security Warning:** Not recommended for production. Secrets exposed in configuration files. + +### Configuration + +#### JSON Configuration + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + "ClientCredentials": [ + { + "SourceType": "Base64Encoded", + "Base64EncodedValue": "MIIKcQIBAzCCCi0GCSqGSIb3DQEHAaCCCh4EggoaMIIKFjCCBg8GCSqGSIb3... (truncated)" + } + ] + } +} +``` + +#### C# Code Configuration + +```csharp +using Microsoft.Identity.Abstractions; + +var credentialDescription = CredentialDescription.FromBase64String( + "MIIKcQIBAzCCCi0GCSqGSIb3DQEHAaCCCh4EggoaMIIKFjCCBg8GCSqGSIb3... (truncated)"); +``` + +#### Environment Variable Pattern + +```bash +# Linux/macOS: Set certificate as environment variable +export CERT_BASE64=$(cat mycert.pfx | base64) + +# Windows PowerShell +$certBytes = [System.IO.File]::ReadAllBytes("mycert.pfx") +$certBase64 = [System.Convert]::ToBase64String($certBytes) +[System.Environment]::SetEnvironmentVariable("CERT_BASE64", $certBase64, "User") +``` + +```csharp +// Read from environment variable in code +var certBase64 = Environment.GetEnvironmentVariable("CERT_BASE64"); +var credentialDescription = CredentialDescription.FromBase64String(certBase64); +``` + +### Setup Guide + +#### Step 1: Convert Certificate to Base64 + +**Linux/macOS:** + +```bash +# Convert PFX to base64 +base64 -i mycert.pfx -o mycert-base64.txt + +# Or inline +CERT_BASE64=$(cat mycert.pfx | base64 | tr -d '\n') +echo $CERT_BASE64 +``` + +**Windows PowerShell:** + +```powershell +# Convert PFX to base64 +$certBytes = [System.IO.File]::ReadAllBytes("mycert.pfx") +$certBase64 = [System.Convert]::ToBase64String($certBytes) +$certBase64 | Out-File -FilePath "mycert-base64.txt" +``` + +#### Step 2: Store Base64 String Securely + +**Kubernetes Secret:** + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: app-certificate +type: Opaque +data: + certificate: +``` + +**Azure App Service Configuration:** + +```bash +az webapp config appsettings set \ + --name \ + --resource-group \ + --settings CERT_BASE64="" +``` + +**Docker Compose:** + +```yaml +services: + app: + environment: + - CERT_BASE64=${CERT_BASE64} +``` + +#### Step 3: Reference in Configuration + +```json +{ + "AzureAd": { + "ClientCredentials": [ + { + "SourceType": "Base64Encoded", + "Base64EncodedValue": "${CERT_BASE64}" + } + ] + } +} +``` + +--- + +## Certificate Requirements + +### Technical Requirements + +**Supported algorithms:** +- β RSA (2048-bit or higher recommended) +- β ECDSA (P-256, P-384, P-521) + +**Supported formats:** +- β PFX/PKCS#12 (.pfx, .p12) +- β PEM (for Key Vault and some scenarios) + +**Certificate must include:** +- β Private key +- β Key usage: Digital Signature +- β Extended key usage: Client Authentication (optional but recommended) + +### App Registration Requirements + +1. **Public key uploaded** to app registration (Certificates & secrets) +2. **Matching thumbprint** between uploaded public key and certificate used +3. **Valid certificate** (not expired, trusted chain) + +--- + +## Multiple Certificates + +You can configure multiple certificates for fallback or rotation scenarios: + +```json +{ + "AzureAd": { + "ClientCredentials": [ + { + "SourceType": "KeyVault", + "KeyVaultUrl": "https://keyvault.vault.azure.net", + "KeyVaultCertificateName": "NewCertificate" + }, + { + "SourceType": "KeyVault", + "KeyVaultUrl": "https://keyvault.vault.azure.net", + "KeyVaultCertificateName": "OldCertificate" + } + ] + } +} +``` + +Microsoft.Identity.Web tries certificates in order until one succeeds. + +--- + +## Certificate Rotation Strategies + +### Zero-Downtime Rotation + +**Step 1:** Add new certificate + +- Upload new certificate to Key Vault/Certificate Store +- Add new public key to app registration +- Keep old certificate active + +**Step 2:** Deploy configuration with both certificates + +```json +{ + "ClientCredentials": [ + { "SourceType": "KeyVault", "KeyVaultCertificateName": "NewCert" }, + { "SourceType": "KeyVault", "KeyVaultCertificateName": "OldCert" } + ] +} +``` + +**Step 3:** Wait for all instances to update + +- Verify new certificate works +- Monitor authentication success + +**Step 4:** Remove old certificate + +- Remove old certificate from configuration +- Remove old public key from app registration +- Delete old certificate from Key Vault/store + +### Automated Rotation with Key Vault + +1. Enable Key Vault auto-renewal +2. Use Distinguished Name in Certificate Store +3. Microsoft.Identity.Web automatically picks up new certificate +4. Update app registration with new public key (can be automated) + +--- + +## Troubleshooting + +### Problem: "Certificate not found" + +**Possible causes:** +- Certificate doesn't exist at specified location +- Incorrect path, thumbprint, or distinguished name +- Permission issues accessing certificate + +**Solutions:** +```bash +# Verify Key Vault certificate exists +az keyvault certificate show --vault-name --name + +# Verify Certificate Store (PowerShell) +Get-ChildItem -Path "Cert:\CurrentUser\My" | Where-Object {$_.Thumbprint -eq "YOUR_THUMBPRINT"} + +# Verify file exists +ls -la /path/to/certificate.pfx +``` + +### Problem: "The provided client credential is not valid" + +**Possible causes:** +- Private key not accessible +- Certificate expired +- Wrong certificate used (thumbprint mismatch) +- Public key not uploaded to app registration + +**Solutions:** +1. Verify certificate is valid: + ```bash + # Check expiration + openssl pkcs12 -in mycert.pfx -nokeys -passin pass:password | openssl x509 -noout -dates + ``` +2. Verify thumbprint matches app registration +3. Check private key permissions +4. Ensure public key is uploaded to app registration + +### Problem: "Access to Key Vault was denied" + +**Possible causes:** +- Managed identity doesn't have permissions +- Access policy not configured +- Network connectivity issues + +**Solutions:** +```bash +# Verify access policy +az keyvault show --name --query properties.accessPolicies + +# Grant access +az keyvault set-policy --name --object-id --certificate-permissions get --secret-permissions get +``` + +### Problem: Certificate works locally but fails in production + +**Possible causes:** +- Different certificate stores (CurrentUser vs LocalMachine) +- File path differences between environments +- Permission differences + +**Solutions:** +1. Use environment-specific configuration +2. Verify certificate location in production +3. Check application identity permissions +4. Use Key Vault for consistent behavior across environments + +--- + +## Security Best Practices + +### Certificate Storage + +- β **Production:** Use Azure Key Vault or Hardware Security Module (HSM) +- β **Windows:** Use LocalMachine store with proper ACLs +- β οΈ **Development:** File-based with restricted permissions +- β **Never:** Commit certificates to source control + +### Key Protection + +- β Use strong private key encryption +- β Limit private key access to necessary identities +- β Enable audit logging for key access +- β Consider HSM for highly sensitive scenarios + +### Certificate Lifecycle + +- β Rotate certificates before expiration +- β Use certificates with appropriate validity periods (12-24 months) +- β Automate renewal where possible (Key Vault) +- β Monitor expiration dates +- β Test rotation procedures regularly + +### Access Control + +- β Grant least-privilege permissions +- β Use managed identities instead of service principals when possible +- β Audit certificate access +- β Review permissions regularly + +--- + +## Additional Resources + +- **[Azure Key Vault Certificates](https://learn.microsoft.com/azure/key-vault/certificates/about-certificates)** - Key Vault certificate documentation +- **[Certificate Management Best Practices](https://learn.microsoft.com/azure/active-directory/develop/howto-create-service-principal-portal#option-1-upload-a-certificate)** - Microsoft Entra ID guidance +- **[X.509 Certificates](https://learn.microsoft.com/azure/active-directory/develop/active-directory-certificate-credentials)** - Certificate credentials overview + +--- + +## Next Steps + +- **[Back to Credentials Overview](./credentials-README.md)** - Compare all credential types +- **[Certificateless Authentication](./certificateless.md)** - Alternative to certificates +- **[Client Secrets](./client-secrets.md)** - Simple authentication for development +- **[Calling Downstream APIs](../../calling-downstream-apis/calling-downstream-apis-README.md)** - Use certificates to call APIs + +--- + +**Need help?** [Open an issue](https://github.com/AzureAD/microsoft-identity-web/issues) or check [troubleshooting guides](../../calling-downstream-apis/from-web-apps.md#troubleshooting). \ No newline at end of file diff --git a/docs/authentication/credentials/client-secrets.md b/docs/authentication/credentials/client-secrets.md new file mode 100644 index 000000000..12c7e08c7 --- /dev/null +++ b/docs/authentication/credentials/client-secrets.md @@ -0,0 +1,604 @@ +# Client Secrets + +Client secrets are simple string-based credentials used to authenticate your application to Microsoft Entra ID (formerly Azure AD). While easy to configure, they provide lower security than certificates or certificateless methods and are **recommended only for development and testing**. + +## Overview + +### What Are Client Secrets? + +A client secret is a password-like string that your application sends to Microsoft Entra ID to prove its identity. Think of it as an API key or shared secret between your application and the identity provider. + +### Why Use Client Secrets? + +**Development convenience:** +- β Simple to create and configure +- β No certificate management required +- β Quick setup for testing +- β Easy to rotate for development + +**When appropriate:** +- β Local development environments +- β Proof-of-concept projects +- β Testing and staging (with caution) +- β Short-lived demo applications + +### Why NOT Use Client Secrets in Production + +**Security concerns:** +- β Symmetric key (both sides know the secret) +- β Lower security than asymmetric cryptography +- β Risk of exposure in configuration files +- β No cryptographic proof of identity +- β Many organizations prohibit client secrets entirely + +**Operational limitations:** +- β Manual rotation required +- β No automatic renewal +- β Difficult to manage at scale +- β Limited audit capabilities + +**Compliance issues:** +- β May not meet regulatory requirements (FIPS, PCI-DSS, etc.) +- β Not accepted by some security teams +- β Fails many security assessments + +--- + +## Client Secrets vs Alternatives + +| Feature | Client Secrets | Certificates | Certificateless (FIC+MSI) | +|---------|---------------|--------------|---------------------------| +| **Security** | Low | High | Very High | +| **Setup complexity** | Very simple | Moderate | Moderate | +| **Production ready** | No | Yes | Yes | +| **Rotation** | Manual | Manual or automated | Automatic | +| **Compliance** | Often fails | Usually passes | Usually passes | +| **Cost** | Free | Certificate costs | Free | +| **Azure dependency** | No | No | Yes (Azure only) | +| **Recommended for** | Dev/test only | Production | Production on Azure | + +**Production alternatives:** +- **Running on Azure?** β [Certificateless Authentication](./certificateless.md) (recommended) +- **Certificate required?** β [Certificate-Based Authentication](./certificates.md) +- **Need strong security?** β Avoid client secrets + +--- + +## Configuration + +### JSON Configuration (appsettings.json) + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + "ClientCredentials": [ + { + "SourceType": "ClientSecret", + "ClientSecret": "your-client-secret" + } + ] + } +} +``` + +### C# Code Configuration + +```csharp +using Microsoft.Identity.Abstractions; + +var credentialDescription = new CredentialDescription +{ + SourceType = CredentialSource.ClientSecret, + ClientSecret = "your-client-secret" +}; +``` + +### ASP.NET Core Web App + +```csharp +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.Identity.Web; +using Microsoft.Identity.Abstractions; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(options => + { + options.Instance = "https://login.microsoftonline.com/"; + options.TenantId = "your-tenant-id"; + options.ClientId = "your-client-id"; + options.ClientCredentials = new[] + { + new CredentialDescription + { + SourceType = CredentialSource.ClientSecret, + ClientSecret = builder.Configuration["AzureAd:ClientSecret"] + } + }; + }); +``` + +### ASP.NET Core Web API + +```csharp +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Identity.Web; +using Microsoft.Identity.Abstractions; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(options => + { + options.Instance = "https://login.microsoftonline.com/"; + options.TenantId = "your-tenant-id"; + options.ClientId = "your-client-id"; + options.ClientCredentials = new[] + { + new CredentialDescription + { + SourceType = CredentialSource.ClientSecret, + ClientSecret = builder.Configuration["AzureAd:ClientSecret"] + } + }; + }) + .EnableTokenAcquisitionToCallDownstreamApi() + .AddInMemoryTokenCaches(); +``` + +### Daemon Application (Console/Worker Service) + +```csharp +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Web; + +// Client secret loaded from appsettings.json automatically +var tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance(); +var sp = tokenAcquirerFactory.Build(); + +var api = sp.GetRequiredService(); +var result = await api.GetForAppAsync("MyApi"); +``` + +--- + +## Setup Guide + +### Step 1: Create Client Secret in Azure Portal + +1. Navigate to **Microsoft Entra ID** > **App registrations** +2. Select your application +3. Click **Certificates & secrets** +4. Under **Client secrets** tab, click **New client secret** +5. Configure the secret: + - **Description:** Descriptive name (e.g., "Development Secret" or "Testing Secret") + - **Expires:** Select expiration period + - 6 months (recommended for development) + - 12 months + - 24 months + - Custom +6. Click **Add** +7. **IMPORTANT:** Copy the secret value **immediately** - you won't be able to see it again + +### Step 2: Store the Secret Securely + +**β οΈ CRITICAL: Never commit secrets to source control** + +**Option A: User Secrets (Local Development - Recommended)** + +```bash +# .NET User Secrets (local development only) +cd YourProject +dotnet user-secrets init +dotnet user-secrets set "AzureAd:ClientSecret" "your-client-secret" +``` + +User secrets are stored outside your project directory and never committed to source control. + +**Option B: Environment Variables** + +```bash +# Linux/macOS +export AzureAd__ClientSecret="your-client-secret" + +# Windows PowerShell +$env:AzureAd__ClientSecret="your-client-secret" +``` + +**Option C: Azure Key Vault (Best for Shared Environments)** + +```bash +# Store secret in Key Vault +az keyvault secret set \ + --vault-name \ + --name "AzureAd--ClientSecret" \ + --value "your-client-secret" +``` + +Configure your application to read from Key Vault: + +```csharp +var builder = WebApplication.CreateBuilder(args); + +// Add Key Vault configuration +var keyVaultUrl = builder.Configuration["KeyVaultUrl"]; +builder.Configuration.AddAzureKeyVault( + new Uri(keyVaultUrl), + new DefaultAzureCredential()); +``` + +**Option D: Azure App Service Configuration** + +```bash +# Set app setting in Azure App Service +az webapp config appsettings set \ + --name \ + --resource-group \ + --settings AzureAd__ClientSecret="your-client-secret" +``` + +### Step 3: Configure Your Application + +Add to `appsettings.json` (without the secret value): + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + "ClientCredentials": [ + { + "SourceType": "ClientSecret" + } + ] + } +} +``` + +The actual secret value comes from user secrets, environment variable, or Key Vault. + +--- + +## Securing Client Secrets + +### Development Best Practices + +**DO:** +- β Use .NET User Secrets for local development +- β Store secrets in Key Vault for shared dev/test environments +- β Use separate secrets for each environment (dev, test, staging) +- β Rotate secrets regularly (every 3-6 months) +- β Use short expiration periods (6-12 months) +- β Document where secrets are stored +- β Remove secrets from configuration files before committing + +**DON'T:** +- β Commit secrets to source control +- β Share secrets via email, Slack, or Teams +- β Store secrets in plaintext files +- β Use production secrets in development +- β Leave expired secrets in app registration +- β Use the same secret across multiple environments + +### .gitignore Configuration + +Ensure your `.gitignore` includes: + +```gitignore +# User secrets +secrets.json + +# Environment files +.env +.env.local +.env.*.local + +# Configuration files with secrets +appsettings.Development.json +appsettings.Local.json +**/appsettings.*.json + +# VS user-specific files +*.user +*.suo +``` + +### Configuration Hierarchy + +Microsoft.Identity.Web resolves client secrets in this order: + +1. **Code configuration** (least recommended) +2. **Environment variables** (good for containers) +3. **User secrets** (best for local development) +4. **Azure Key Vault** (best for shared environments) +5. **appsettings.json** (never store secrets here) + +--- + +## Secret Rotation + +### Rotation Strategy + +**Step 1: Create new secret** + +1. Create a new client secret in Azure Portal +2. Note the new secret value +3. Keep old secret active + +**Step 2: Update configuration with new secret** + +```bash +# Update user secrets +dotnet user-secrets set "AzureAd:ClientSecret" "new-secret-value" + +# Or update Key Vault +az keyvault secret set \ + --vault-name \ + --name "AzureAd--ClientSecret" \ + --value "new-secret-value" +``` + +**Step 3: Deploy and verify** + +- Deploy updated configuration +- Verify authentication works with new secret +- Monitor for errors + +**Step 4: Remove old secret** + +- After grace period (e.g., 24-48 hours) +- Delete old client secret from app registration +- Verify no applications are using old secret + +### Automated Rotation Reminders + +**Azure Portal:** +- Set calendar reminders 30 days before expiration + +**Automation:** +```bash +# Script to check secret expiration +az ad app credential list \ + --id \ + --query "[?type=='Password'].{Description:customKeyIdentifier, Expires:endDateTime}" \ + -o table +``` + +--- + +## Migration to Production Credentials + +### From Client Secrets to Certificates + +**Step 1: Create and configure certificate** + +See [Certificate-Based Authentication](./certificates.md) for detailed instructions. + +**Step 2: Add certificate alongside secret** + +```json +{ + "AzureAd": { + "ClientCredentials": [ + { + "SourceType": "KeyVault", + "KeyVaultUrl": "https://keyvault.vault.azure.net", + "KeyVaultCertificateName": "ProductionCert" + }, + { + "SourceType": "ClientSecret", + "ClientSecret": "fallback-secret" + } + ] + } +} +``` + +**Step 3: Test with certificate in non-production** + +**Step 4: Deploy to production with certificate only** + +**Step 5: Remove client secret** + +### From Client Secrets to Certificateless (FIC+MSI) + +**Step 1: Enable managed identity and configure FIC** + +See [Certificateless Authentication](./certificateless.md) for detailed instructions. + +**Step 2: Add certificateless alongside secret** + +```json +{ + "AzureAd": { + "ClientCredentials": [ + { + "SourceType": "SignedAssertionFromManagedIdentity" + }, + { + "SourceType": "ClientSecret", + "ClientSecret": "fallback-secret" + } + ] + } +} +``` + +**Step 3: Test in Azure** + +Managed identity only works in Azure, so test in an Azure environment. + +**Step 4: Deploy to production** + +**Step 5: Remove client secret** + +--- + +## Troubleshooting + +### Problem: "Invalid client secret provided" + +**Possible causes:** +- Secret expired +- Wrong secret value +- Secret not configured in app registration +- Whitespace or encoding issues + +**Solutions:** +1. Verify secret exists and is not expired: + ```bash + az ad app credential list --id + ``` +2. Create new secret if expired +3. Check for leading/trailing whitespace in configuration +4. Verify secret matches app registration + +### Problem: Secret not loading from configuration + +**Possible causes:** +- Configuration key name mismatch +- Environment variable not set +- User secrets not initialized +- Key Vault access denied + +**Solutions:** +```csharp +// Debug: Log where secret is coming from +var secret = builder.Configuration["AzureAd:ClientSecret"]; +Console.WriteLine($"Secret loaded: {secret != null}"); +Console.WriteLine($"Secret length: {secret?.Length ?? 0}"); +``` + +### Problem: Works locally but fails in Azure + +**Possible causes:** +- Different configuration sources (user secrets vs app settings) +- Secret not deployed to Azure App Service +- Key Vault permissions not configured + +**Solutions:** +1. Check Azure App Service configuration settings +2. Verify Key Vault access from App Service +3. Use same secret storage mechanism across environments + +--- + +## Security Warnings + +### β οΈ Common Pitfalls + +**Exposed secrets:** +- β Secrets committed to Git repositories +- β Secrets in public Docker images +- β Secrets logged in application logs +- β Secrets in error messages or exceptions + +**Detection and remediation:** +```bash +# Scan Git history for secrets (using git-secrets or similar) +git secrets --scan-history + +# If secret exposed in Git history: +# 1. Immediately revoke the secret in Azure Portal +# 2. Create new secret +# 3. Update all configurations +# 4. Consider rewriting Git history (complex) +``` + +### π Defense in Depth + +Even when using client secrets for development: + +1. **Separate secrets per environment** - Never reuse production secrets +2. **Short expiration** - 6 months or less for development +3. **Regular rotation** - Rotate every 3-6 months +4. **Access auditing** - Monitor secret usage +5. **Least privilege** - Grant minimum required permissions + +--- + +## When Client Secrets Are Acceptable + +### Acceptable Use Cases + +**Development:** +- β Local developer workstations (with user secrets) +- β Personal development Azure subscriptions +- β Proof-of-concept projects + +**Testing:** +- β Automated testing (CI/CD with secure secret storage) +- β Integration test environments (isolated, non-production) +- β Staging environments (with enhanced monitoring) + +**Special scenarios:** +- β Short-lived demo applications +- β Internal tools with limited scope +- β Temporary solutions during migration + +### Unacceptable Use Cases + +**Never use client secrets for:** +- β Production applications +- β Customer-facing services +- β Applications handling sensitive data +- β Long-running services +- β Publicly accessible applications +- β Compliance-regulated workloads + +--- + +## Migration Path to Production + +```mermaid +flowchart LR + Dev[DevelopmentClient Secrets] --> Test[TestingClient Secrets+ Key Vault] + Test --> Staging[StagingCertificates orCertificateless] + Staging --> Prod[ProductionCertificates orCertificatelessNO SECRETS] + + style Dev fill:#ffc107,stroke:#d39e00,color:#000 + style Test fill:#ffc107,stroke:#d39e00,color:#000 + style Staging fill:#17a2b8,stroke:#117a8b,color:#fff + style Prod fill:#28a745,stroke:#1e7e34,color:#fff +``` + +**Recommended progression:** +1. **Development:** Client secrets with user secrets +2. **Testing:** Client secrets in Key Vault (short-lived) +3. **Staging:** Certificates or certificateless (production-like) +4. **Production:** Certificates or certificateless (no client secrets) + +--- + +## Additional Resources + +- **[Azure Key Vault for Secrets](https://learn.microsoft.com/azure/key-vault/secrets/about-secrets)** - Secure secret storage +- **[.NET User Secrets](https://learn.microsoft.com/aspnet/core/security/app-secrets)** - Local development secrets +- **[Client Credentials Flow](https://learn.microsoft.com/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow)** - OAuth 2.0 client credentials +- **[Secret Scanning Tools](https://github.com/awslabs/git-secrets)** - Detect exposed secrets + +--- + +## Next Steps + +### Migrate to Production Credentials + +- **[Certificateless Authentication](./certificateless.md)** - Best for Azure (recommended) +- **[Certificate-Based Authentication](./certificates.md)** - Universal production solution + +### Learn More + +- **[Back to Credentials Overview](./credentials-README.md)** - Compare all credential types +- **[Calling Downstream APIs](../../calling-downstream-apis/calling-downstream-apis-README.md)** - Use credentials to call APIs + +--- + +**β οΈ Remember:** Client secrets are for development only. Always use certificates or certificateless authentication in production. + +**Need help?** [Open an issue](https://github.com/AzureAD/microsoft-identity-web/issues) or check [troubleshooting guides](../../calling-downstream-apis/from-web-apps.md#troubleshooting). \ No newline at end of file diff --git a/docs/authentication/credentials/credentials-README.md b/docs/authentication/credentials/credentials-README.md new file mode 100644 index 000000000..fe760ccb5 --- /dev/null +++ b/docs/authentication/credentials/credentials-README.md @@ -0,0 +1,736 @@ +# Application Credentials in Microsoft.Identity.Web + +Client credentials are used to prove the identity of your application when acquiring tokens from Microsoft Entra ID (formerly Azure AD). Microsoft.Identity.Web supports multiple credential types to meet different security requirements, deployment environments, and operational needs. + +## Overview + +### What Are Client Credentials? + +Client credentials authenticate your application to the identity provider. They serve two primary purposes: + +1. **Client Credentials** - Prove the identity of your application when acquiring tokens +2. **Token Decryption Credentials** - Decrypt encrypted tokens sent to your application + +### Why Authentication Matters + +Proper credential management is critical for: + +- **Security** - Protecting your application and user data +- **Compliance** - Meeting regulatory and organizational requirements +- **Operations** - Minimizing management overhead and security incidents +- **Reliability** - Ensuring your application can authenticate consistently + +### Microsoft.Identity.Web's Approach + +Microsoft.Identity.Web provides a unified `ClientCredentials` configuration model that supports both traditional certificate-based authentication and modern certificateless approaches. This flexibility allows you to choose the right credential type for your scenario while maintaining a consistent configuration pattern. + +--- + +## When to Use Which Credential Type + +### Decision Flow + +```mermaid +flowchart LR + Start[Choose Credential Type] --> Q1{Avoid certificatemanagement?} + + Q1 -->|Yes| Q2{Running on Azure?} + Q1 -->|No| Q5{Productionenvironment?} + + Q2 -->|Yes| FIC[β FIC + Managed IdentityCertificateless] + Q2 -->|No| Q3{Can use othercertificateless?} + + Q3 -->|Yes| OtherCertless[β Other CertificatelessMethods] + Q3 -->|No| Q5 + + Q5 -->|Yes| Q6{Need credentialrotation?} + Q5 -->|No| DevCreds[βΉοΈ Development CredentialsSecrets or File Certs] + + Q6 -->|Yes| Q7{On Windows?} + Q6 -->|No| KeyVault[β Key Vaultwith Managed Certs] + + Q7 -->|Yes| CertStore[β Certificate Storewith Distinguished Name] + Q7 -->|No| KeyVault + + FIC --> Recommended[β Recommended for Production] + KeyVault --> Recommended + CertStore --> GoodChoice[β Good for Production] + + style FIC fill:#28a745,stroke:#1e7e34,stroke-width:2px,color:#fff + style KeyVault fill:#28a745,stroke:#1e7e34,stroke-width:2px,color:#fff + style CertStore fill:#17a2b8,stroke:#117a8b,stroke-width:2px,color:#fff + style DevCreds fill:#ffc107,stroke:#d39e00,stroke-width:2px,color:#fff + style Recommended fill:#fd7e14,stroke:#dc6502,stroke-width:2px,color:#fff + style OtherCertless fill:#28a745,stroke:#1e7e34,stroke-width:2px,color:#fff + style GoodChoice fill:#17a2b8,stroke:#117a8b,stroke-width:2px,color:#fff +``` + +### Comparison Table + +| Credential Type | What Is It | When to Use | Advantages | Considerations | +|----------------|------------|-------------|------------|----------------| +| **Federated Identity Credential with Managed Identity (FIC+MSI)** (`SignedAssertionFromManagedIdentity`) | Azure Managed Identity generates signed assertions | β’ Production on Azureβ’ Zero certificate managementβ’ Cloud-native apps | β’ No secrets to manageβ’ Automatic rotationβ’ No certificate lifecycleβ’ Highly secureβ’ Cost-effective | β’ Azure-onlyβ’ Requires managed identity setup | +| **Key Vault** (`SourceType = KeyVault`) | Certificate stored in Azure Key Vault | β’ Production environmentsβ’ Centralized managementβ’ Automatic rotationβ’ Shared credentials | β’ Centralized controlβ’ Audit loggingβ’ Access policiesβ’ Automatic renewalβ’ Cross-platform | β’ Requires Azure subscriptionβ’ Additional costβ’ Network dependency | +| **Certificate Store** (`StoreWithThumbprint` or `StoreWithDistinguishedName`) | Certificate in Windows Certificate Store | β’ Production on Windowsβ’ Using Windows cert managementβ’ On-premises environments | β’ Integrated with Windowsβ’ IT-managed certificatesβ’ Hardware security modules (HSM)β’ Distinguished Name enables rotation | β’ Windows-onlyβ’ Manual renewal (with thumbprint)β’ Requires certificate management | +| **File Path** (`SourceType = Path`) | PFX/P12 file on disk | β’ Development/testingβ’ Simple deploymentβ’ Container environments | β’ Simple setupβ’ Easy deploymentβ’ No external dependencies | **Not for production**β’ File system security riskβ’ Manual rotationβ’ Secret exposure risk | +| **Base64 Encoded** (`SourceType = Base64Encoded`) | Certificate as base64 string | β’ Development/testingβ’ Configuration-embedded certificatesβ’ Environment variables | β’ Simple configurationβ’ No file system dependencyβ’ Works in containers | **Not for production**β’ Configuration exposureβ’ Manual rotationβ’ Difficult to secure | +| **Client Secret** (`SourceType = ClientSecret`) | Simple shared secret string | β’ Development/testingβ’ Proof of conceptβ’ Basic scenarios | β’ Simple to useβ’ Easy to configureβ’ Quick setup | **Not for production**β’ Lower securityβ’ Manual rotationβ’ Exposure risk | +| **Auto Decrypt Keys** (`SourceType = AutoDecryptKeys`) | Automatic key retrieval for token decryption | β’ Encrypted token scenariosβ’ Automatic token decryption | β’ Automatic key managementβ’ Key rotation supportβ’ Transparent decryption | β’ Specific use caseβ’ Requires client credentialsβ’ Additional configuration | + +--- + +## Quick Configuration Examples + +All credential types are configured in the `ClientCredentials` array in your application configuration. Both JSON and code-based configuration are supported. + +### Certificateless Authentication (FIC + Managed Identity) β Recommended + +**Best for:** Production applications running on Azure + +#### JSON Configuration + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + "ClientCredentials": [ + { + "SourceType": "SignedAssertionFromManagedIdentity", + "ManagedIdentityClientId": "optional-for-user-assigned-msi" + } + ] + } +} +``` + +#### C# Code Configuration + +```csharp +using Microsoft.Identity.Abstractions; + +var credentialDescription = new CredentialDescription +{ + SourceType = CredentialSource.SignedAssertionFromManagedIdentity, + ManagedIdentityClientId = "optional-for-user-assigned-msi" +}; +``` + +**Benefits:** +- β Zero certificate management overhead +- β Automatic credential rotation +- β No secrets in configuration +- β Reduced security risk + +**[Learn more about certificateless authentication β](./certificateless.md)** + +--- + +### Certificates from Key Vault + +**Best for:** Production applications requiring certificate-based authentication with centralized management + +#### JSON Configuration + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + "ClientCredentials": [ + { + "SourceType": "KeyVault", + "KeyVaultUrl": "https://your-keyvault.vault.azure.net", + "KeyVaultCertificateName": "YourCertificateName" + } + ] + } +} +``` + +#### C# Code Configuration + +```csharp +using Microsoft.Identity.Abstractions; + +// Using property initialization +var credentialDescription = new CredentialDescription +{ + SourceType = CredentialSource.KeyVault, + KeyVaultUrl = "https://your-keyvault.vault.azure.net", + KeyVaultCertificateName = "YourCertificateName" +}; + +// Using helper method +var credentialDescription = CredentialDescription.FromKeyVault( + "https://your-keyvault.vault.azure.net", + "YourCertificateName"); +``` + +**Benefits:** +- β Centralized certificate management +- β Automatic renewal support +- β Audit logging and access control +- β Works across platforms + +**[Learn more about Key Vault certificates β](./certificates.md#key-vault)** + +--- + +### Certificates from Certificate Store + +**Best for:** Production Windows applications using enterprise certificate management + +#### Using Thumbprint + +**JSON Configuration:** + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + "ClientCredentials": [ + { + "SourceType": "StoreWithThumbprint", + "CertificateStorePath": "CurrentUser/My", + "CertificateThumbprint": "A1B2C3D4E5F6..." + } + ] + } +} +``` + +**C# Code Configuration:** + +```csharp +using Microsoft.Identity.Abstractions; + +var credentialDescription = CredentialDescription.FromCertificateStore( + "CurrentUser/My", + thumbprint: "A1B2C3D4E5F6..."); +``` + +#### Using Distinguished Name (Recommended for Rotation) + +**JSON Configuration:** + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + "ClientCredentials": [ + { + "SourceType": "StoreWithDistinguishedName", + "CertificateStorePath": "CurrentUser/My", + "CertificateDistinguishedName": "CN=YourAppCertificate" + } + ] + } +} +``` + +**C# Code Configuration:** + +```csharp +using Microsoft.Identity.Abstractions; + +var credentialDescription = CredentialDescription.FromCertificateStore( + "CurrentUser/My", + distinguishedName: "CN=YourAppCertificate"); +``` + +**Certificate Store Paths:** +- `CurrentUser/My` - User's personal certificate store +- `LocalMachine/My` - Computer's certificate store + +**Benefits:** +- β Integrated with Windows certificate management +- β Hardware security module (HSM) support +- β IT-managed certificate lifecycle +- β Distinguished Name enables automatic rotation + +**[Learn more about certificate store β](./certificates.md#certificate-store)** + +--- + +### Certificates from File Path + +**Best for:** Development, testing, and simple deployments + +#### JSON Configuration + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + "ClientCredentials": [ + { + "SourceType": "Path", + "CertificateDiskPath": "/app/certificates/mycert.pfx", + "CertificatePassword": "certificate-password" + } + ] + } +} +``` + +#### C# Code Configuration + +```csharp +using Microsoft.Identity.Abstractions; + +var credentialDescription = CredentialDescription.FromCertificatePath( + "/app/certificates/mycert.pfx", + "certificate-password"); +``` + +**β οΈ Security Warning:** Not recommended for production. Use Key Vault or Certificate Store instead. + +**[Learn more about file-based certificates β](./certificates.md#file-path)** + +--- + +### Base64 Encoded Certificates + +**Best for:** Development and testing with configuration-embedded certificates + +#### JSON Configuration + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + "ClientCredentials": [ + { + "SourceType": "Base64Encoded", + "Base64EncodedValue": "MIID... (base64 encoded certificate)" + } + ] + } +} +``` + +#### C# Code Configuration + +```csharp +using Microsoft.Identity.Abstractions; + +var credentialDescription = CredentialDescription.FromBase64String( + "MIID... (base64 encoded certificate)"); +``` + +**β οΈ Security Warning:** Not recommended for production. Secrets exposed in configuration files. + +**[Learn more about base64 certificates β](./certificates.md#base64-encoded)** + +--- + +### Client Secrets (Development/Testing Only) + +**Best for:** Development, testing, and proof-of-concept scenarios + +#### JSON Configuration + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + "ClientCredentials": [ + { + "SourceType": "ClientSecret", + "ClientSecret": "your-client-secret" + } + ] + } +} +``` + +#### C# Code Configuration + +```csharp +using Microsoft.Identity.Abstractions; + +var credentialDescription = new CredentialDescription +{ + SourceType = CredentialSource.ClientSecret, + ClientSecret = "your-client-secret" +}; +``` + +**β οΈ Security Warning:** +- **Not for production use** +- Lower security than certificates or certificateless methods +- Some organizations prohibit client secrets entirely +- Manual rotation required + +**[Learn more about client secrets β](./client-secrets.md)** + +--- + +### Token Decryption Credentials + +**Best for:** Applications that receive encrypted tokens + +#### JSON Configuration + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + "TokenDecryptionCredentials": [ + { + "SourceType": "AutoDecryptKeys", + "DecryptKeysAuthenticationOptions": { + "ProtocolScheme": "Bearer", + "AcquireTokenOptions": { + "Tenant": "your-tenant.onmicrosoft.com" + } + } + } + ] + } +} +``` + +#### C# Code Configuration + +```csharp +using Microsoft.Identity.Abstractions; + +var credentialDescription = new CredentialDescription +{ + SourceType = CredentialSource.AutoDecryptKeys, + DecryptKeysAuthenticationOptions = new AuthorizationHeaderProviderOptions + { + ProtocolScheme = "Bearer", + AcquireTokenOptions = new AcquireTokenOptions + { + Tenant = "your-tenant.onmicrosoft.com" + } + } +}; +``` + +**Note:** Token decryption credentials require client credentials to acquire decryption keys. + +**[Learn more about token decryption β](./token-decryption.md)** + +--- + +## Security Best Practices + +### For Production Environments + +**Recommended (Priority Order):** + +1. **Certificateless Authentication (if possible)** + - β Federated Identity Credential with Managed Identity (FIC+MSI) + - β Other certificateless methods + - **Why:** Zero credential management, automatic rotation, lowest risk + +2. **Certificate-Based Authentication (if required)** + - β Azure Key Vault with managed certificates + - β Certificate Store with Distinguished Name (Windows) + - **Why:** Strong cryptographic proof, suitable for compliance requirements + +**Never in Production:** +- β Client Secrets +- β File-based certificates (except in secure container environments) +- β Base64 encoded certificates + +### For Development and Testing + +**Acceptable shortcuts:** +- β Client secrets (for quick setup) +- β File-based certificates (for local development) +- β Base64 encoded certificates (for isolated testing) + +**Important:** Keep development credentials separate from production and rotate them regularly. + +### Common Security Pitfalls + +1. **Hardcoding Secrets** - Never commit credentials to source control +2. **Using Development Credentials in Production** - Always use production-grade credentials for production +3. **Ignoring Rotation** - Implement credential rotation strategies +4. **Overprivileged Service Principals** - Grant only necessary permissions +5. **Inadequate Monitoring** - Log and monitor credential usage + +--- + +## Configuration Approaches + +### Configuration by File (appsettings.json) + +All scenarios support configuration through `appsettings.json`: + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + "ClientCredentials": [ + { + "SourceType": "SignedAssertionFromManagedIdentity" + } + ] + } +} +``` + +**For daemon apps and console applications:** +Ensure `appsettings.json` is copied to the output directory. Add this to your `.csproj`: + +```xml + + + PreserveNewest + + +``` + +### Configuration by Code + +You can configure credentials programmatically: + +#### ASP.NET Core Web App + +```csharp +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.Identity.Web; +using Microsoft.Identity.Abstractions; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(options => + { + options.Instance = "https://login.microsoftonline.com/"; + options.TenantId = "your-tenant-id"; + options.ClientId = "your-client-id"; + options.ClientCredentials = new[] + { + new CredentialDescription + { + SourceType = CredentialSource.SignedAssertionFromManagedIdentity + } + }; + }); +``` + +#### ASP.NET Core Web API + +```csharp +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Identity.Web; +using Microsoft.Identity.Abstractions; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(options => + { + options.Instance = "https://login.microsoftonline.com/"; + options.TenantId = "your-tenant-id"; + options.ClientId = "your-client-id"; + options.ClientCredentials = new[] + { + new CredentialDescription + { + SourceType = CredentialSource.KeyVault, + KeyVaultUrl = "https://your-keyvault.vault.azure.net", + KeyVaultCertificateName = "YourCertificateName" + } + }; + }); +``` + +#### Daemon Application + +```csharp +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Web; + +var tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance(); + +// Credentials are loaded from appsettings.json automatically +// Or configure programmatically: +tokenAcquirerFactory.Services.Configure(options => +{ + options.Instance = "https://login.microsoftonline.com/"; + options.TenantId = "your-tenant-id"; + options.ClientId = "your-client-id"; + options.ClientCredentials = new[] + { + new CredentialDescription + { + SourceType = CredentialSource.SignedAssertionFromManagedIdentity + } + }; +}); +``` + +### Hybrid Approach + +You can mix file and code configuration: + +```csharp +// Load base configuration from appsettings.json +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd")); + +// Override or supplement with code-based configuration +builder.Services.Configure(options => +{ + // Add additional credential sources + options.ClientCredentials = options.ClientCredentials.Concat(new[] + { + new CredentialDescription + { + SourceType = CredentialSource.KeyVault, + KeyVaultUrl = Environment.GetEnvironmentVariable("KEY_VAULT_URL"), + KeyVaultCertificateName = Environment.GetEnvironmentVariable("CERT_NAME") + } + }).ToArray(); +}); +``` + +--- + +## Important Notes + +### Credential Types and Usage + +1. **Certificate** - Can be used for both client credentials and token decryption +2. **Client Secret** - Only for client credentials (not for token decryption) +3. **Signed Assertion** - Only for client credentials (not for token decryption) +4. **Decrypt Keys** - Only for token decryption (not for client credentials) + +### Multiple Credentials + +You can configure multiple credential sources in the `ClientCredentials` array. Microsoft.Identity.Web will attempt to use them in order: + +```json +{ + "ClientCredentials": [ + { + "SourceType": "SignedAssertionFromManagedIdentity" + }, + { + "SourceType": "KeyVault", + "KeyVaultUrl": "https://your-keyvault.vault.azure.net", + "KeyVaultCertificateName": "FallbackCertificate" + } + ] +} +``` + +This provides fallback options and supports migration scenarios. + +### Custom Credential Providers + +For advanced scenarios, you can implement custom signed assertion providers: + +```json +{ + "ClientCredentials": [ + { + "SourceType": "CustomSignedAssertion", + "CustomSignedAssertionProviderName": "MyCustomProvider", + "CustomSignedAssertionProviderData": { + "Key1": "Value1", + "Key2": "Value2" + } + } + ] +} +``` + +--- + +## Next Steps + +### Choose Your Credential Type + +Based on the decision flow and comparison table above, select the credential type that best fits your scenario: + +- **[Certificateless Authentication β](./certificateless.md)** - FIC+MSI and modern approaches (recommended) +- **[Certificates β](./certificates.md)** - Key Vault, Certificate Store, File, Base64 +- **[Client Secrets β](./client-secrets.md)** - Development and testing +- **[Token Decryption β](./token-decryption.md)** - Encrypted token scenarios + +### Explore Scenarios + +Learn how credentials are used in different application scenarios: + +- **[Web Applications](../../getting-started/quickstart-webapp.md)** - Sign-in users with web apps +- **[Web APIs](../../getting-started/quickstart-webapi.md)** - Protect and call APIs +- **[Daemon Applications](../../getting-started/daemon-app.md)** - Background services and console apps +- **[Agent Identities](../../calling-downstream-apis/AgentIdentities-Readme.md)** - Call APIs on behalf of agents + +### Related Topics + +- **[Calling Downstream APIs](../../calling-downstream-apis/calling-downstream-apis-README.md)** - Use credentials to call protected APIs +- **[Token Cache](../token-cache/token-cache-README.md)** - Configure token caching strategies + +--- + +## Troubleshooting + +### Common Issues + +**Problem:** "The provided client credential is not valid" + +**Solutions:** +- Verify the credential type matches your app registration +- Check that certificates are not expired +- Ensure managed identity is properly configured +- Validate Key Vault access permissions + +**Problem:** "Cannot find certificate with thumbprint" + +**Solutions:** +- Verify the certificate is installed in the correct store +- Check the thumbprint matches exactly (no spaces or extra characters) +- Consider using Distinguished Name for rotation support +- Ensure the application has permission to access the certificate store + +**Problem:** "Access to Key Vault was denied" + +**Solutions:** +- Verify managed identity has "Get" permission for secrets and certificates +- Check Key Vault access policies or RBAC assignments +- Ensure network connectivity to Key Vault +- Validate the Key Vault URL and certificate name are correct + +**More troubleshooting:** See scenario-specific troubleshooting guides in [Web Apps](../../calling-downstream-apis/from-web-apps.md#troubleshooting), [Web APIs](../../calling-downstream-apis/from-web-apis.md#troubleshooting), and [Daemon Apps](../../getting-started/daemon-app.md). + +--- + +## Additional Resources + +- **[Microsoft.Identity.Abstractions CredentialDescription](https://github.com/AzureAD/microsoft-identity-abstractions-for-dotnet/blob/main/docs/credentialdescription.md)** - Underlying credential model +- **[Azure Managed Identities](https://learn.microsoft.com/azure/active-directory/managed-identities-azure-resources/overview)** - Learn about managed identities +- **[Azure Key Vault](https://learn.microsoft.com/azure/key-vault/general/overview)** - Key Vault documentation +- **[Certificate Management](https://learn.microsoft.com/azure/active-directory/develop/howto-create-service-principal-portal)** - Managing app credentials + +--- + +**Need help?** Visit our [troubleshooting guide](../../calling-downstream-apis/from-web-apps.md#troubleshooting) or [open an issue](https://github.com/AzureAD/microsoft-identity-web/issues). diff --git a/docs/authentication/credentials/token-decryption.md b/docs/authentication/credentials/token-decryption.md new file mode 100644 index 000000000..b15df1f7e --- /dev/null +++ b/docs/authentication/credentials/token-decryption.md @@ -0,0 +1,557 @@ +# Token Decryption Credentials + +Token decryption credentials are used when your application receives encrypted tokens from Microsoft Entra ID (formerly Azure AD). This is a specialized scenario where Microsoft Entra ID encrypts tokens using your application's public key, and your application needs credentials to decrypt them. + +## Overview + +### What is Token Decryption? + +Token decryption (also called token encryption) is a security feature where: + +1. **Microsoft Entra ID encrypts** the token using your application's public key +2. **Your application decrypts** the token using its private key (decryption credential) +3. **Token contents** are protected in transit and at rest + +This provides an additional layer of security beyond HTTPS, protecting token contents from unauthorized access even if network traffic is intercepted. + +### When is Token Decryption Used? + +**Common scenarios:** +- β High-security applications requiring defense in depth +- β Compliance requirements mandating token encryption +- β Applications handling extremely sensitive data +- β Zero-trust architecture implementations +- β Applications subject to regulatory requirements (HIPAA, PCI-DSS, etc.) + +**Not needed for most applications:** +- β οΈ Most applications use HTTPS, which already encrypts tokens in transit +- β οΈ Token encryption adds complexity +- β οΈ Only implement if you have specific security or compliance requirements + +### Token Decryption vs Client Credentials + +**Different purposes:** + +| Feature | Client Credentials | Token Decryption Credentials | +|---------|-------------------|------------------------------| +| **Purpose** | Prove app identity | Decrypt encrypted tokens | +| **Used when** | Acquiring tokens | Receiving encrypted tokens | +| **Direction** | Outbound (to Entra ID) | Inbound (from Entra ID) | +| **Required** | Always | Only if token encryption enabled | +| **Can use secrets** | Yes (not recommended) | No (certificates only) | + +**Important:** Token decryption credentials are in addition to client credentials, not instead of. + +--- + +## How Token Decryption Works + +```mermaid +sequenceDiagram + participant App as Your Application + participant MIW as Microsoft.Identity.Web + participant AAD as Microsoft Entra ID + + Note over App,AAD: Setup: Upload public key for encryption + App->>AAD: Configure token encryptionwith public key + + Note over App,AAD: Runtime: Acquire and decrypt token + MIW->>AAD: Request token(using client credentials) + AAD->>AAD: Encrypt token withapp's public key + AAD->>MIW: Return encrypted token + + rect rgba(70, 130, 180, 0.2) + Note right of MIW: Microsoft.Identity.Web handles decryption + MIW->>MIW: Load decryption certificate + MIW->>MIW: Decrypt token withprivate key + MIW->>MIW: Validate decrypted token + end + + MIW->>App: Return decrypted token +``` + +--- + +## Configuration + +Token decryption uses certificates to decrypt encrypted tokens. You can use any of the certificate types supported by Microsoft.Identity.Web. + +### Using Certificates for Token Decryption + +#### JSON Configuration + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + "ClientCredentials": [ + { + "SourceType": "SignedAssertionFromManagedIdentity" + } + ], + "TokenDecryptionCredentials": [ + { + "SourceType": "KeyVault", + "KeyVaultUrl": "https://your-keyvault.vault.azure.net", + "KeyVaultCertificateName": "TokenDecryptionCert" + } + ] + } +} +``` + +#### C# Code Configuration + +```csharp +using Microsoft.Identity.Abstractions; + +var decryptionCredential = new CredentialDescription +{ + SourceType = CredentialSource.KeyVault, + KeyVaultUrl = "https://your-keyvault.vault.azure.net", + KeyVaultCertificateName = "TokenDecryptionCert" +}; +``` + +#### ASP.NET Core Integration + +```csharp +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Identity.Web; +using Microsoft.Identity.Abstractions; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(options => + { + // Client credentials for acquiring tokens + options.ClientCredentials = new[] + { + new CredentialDescription + { + SourceType = CredentialSource.SignedAssertionFromManagedIdentity + } + }; + + // Token decryption credentials + options.TokenDecryptionCredentials = new[] + { + new CredentialDescription + { + SourceType = CredentialSource.KeyVault, + KeyVaultUrl = "https://your-keyvault.vault.azure.net", + KeyVaultCertificateName = "TokenDecryptionCert" + } + }; + }) + .EnableTokenAcquisitionToCallDownstreamApi() + .AddInMemoryTokenCaches(); +``` + +### Certificate Types for Token Decryption + +You can use any certificate type supported by Microsoft.Identity.Web: + +**Azure Key Vault (Recommended):** +```json +{ + "TokenDecryptionCredentials": [ + { + "SourceType": "KeyVault", + "KeyVaultUrl": "https://your-keyvault.vault.azure.net", + "KeyVaultCertificateName": "DecryptionCert" + } + ] +} +``` + +**Certificate Store (Windows):** +```json +{ + "TokenDecryptionCredentials": [ + { + "SourceType": "StoreWithDistinguishedName", + "CertificateStorePath": "CurrentUser/My", + "CertificateDistinguishedName": "CN=TokenDecryptionCert" + } + ] +} +``` + +**File Path (Development):** +```json +{ + "TokenDecryptionCredentials": [ + { + "SourceType": "Path", + "CertificateDiskPath": "/app/certificates/decryption-cert.pfx", + "CertificatePassword": "cert-password" + } + ] +} +``` + +**Base64 Encoded:** +```json +{ + "TokenDecryptionCredentials": [ + { + "SourceType": "Base64Encoded", + "Base64EncodedValue": "MIID... (base64 encoded certificate)" + } + ] +} +``` + +See [Certificates Guide](./certificates.md) for detailed information on each certificate type. + +### Using Same Certificate for Both Purposes + +You can use the same certificate for both client credentials and token decryption: + +```json +{ + "AzureAd": { + "ClientCredentials": [ + { + "SourceType": "KeyVault", + "KeyVaultUrl": "https://your-keyvault.vault.azure.net", + "KeyVaultCertificateName": "SharedCert" + } + ], + "TokenDecryptionCredentials": [ + { + "SourceType": "KeyVault", + "KeyVaultUrl": "https://your-keyvault.vault.azure.net", + "KeyVaultCertificateName": "SharedCert" + } + ] + } +} +``` + +--- + +## Setup Guide + +### Step 1: Generate or Obtain Certificate + +**Option A: Generate in Azure Key Vault** + +```bash +# Generate certificate for token decryption +az keyvault certificate create \ + --vault-name \ + --name token-decryption-cert \ + --policy "$(az keyvault certificate get-default-policy)" \ + --validity 24 +``` + +**Option B: Generate with PowerShell** + +```powershell +# Generate self-signed certificate +$cert = New-SelfSignedCertificate ` + -Subject "CN=TokenDecryptionCert" ` + -CertStoreLocation "Cert:\CurrentUser\My" ` + -KeyExportPolicy Exportable ` + -KeySpec KeyExchange ` + -KeyLength 2048 ` + -KeyAlgorithm RSA ` + -HashAlgorithm SHA256 ` + -NotAfter (Get-Date).AddYears(2) + +# Export public key +Export-Certificate -Cert $cert -FilePath "decryption-cert.cer" +``` + +**Important:** For token decryption, the certificate must have: +- β Key usage: Key Encipherment +- β Key spec: KeyExchange (not Signature) + +### Step 2: Configure Token Encryption in App Registration + +1. Navigate to **Microsoft Entra ID** > **App registrations** +2. Select your application +3. Click **Token encryption** (in the left menu under "Manage") +4. Click **Upload certificate** +5. Upload the public key certificate (.cer file) +6. Select the uploaded certificate as the **encryption certificate** +7. Click **Save** + +**Azure CLI:** + +```bash +# Upload certificate for token encryption +az ad app credential reset \ + --id \ + --cert @decryption-cert.cer \ + --append + +# Note: Setting as encryption certificate requires Graph API call +az rest \ + --method PATCH \ + --uri "https://graph.microsoft.com/v1.0/applications/" \ + --body '{ + "keyCredentials": [ + { + "type": "AsymmetricX509Cert", + "usage": "Encrypt", + "key": "" + } + ] + }' +``` + +### Step 3: Configure Decryption Credentials in Your Application + +Use the configuration examples above to set up `TokenDecryptionCredentials` in your application. + +### Step 4: Test Token Decryption + +```csharp +// Token decryption happens automatically +// When your app receives an encrypted token, Microsoft.Identity.Web decrypts it + +// You can verify decryption is working by inspecting token claims +var claims = User.Claims; +foreach (var claim in claims) +{ + Console.WriteLine($"{claim.Type}: {claim.Value}"); +} +``` + +--- + +## Multiple Decryption Credentials + +You can configure multiple token decryption credentials for rotation scenarios: + +```json +{ + "AzureAd": { + "TokenDecryptionCredentials": [ + { + "SourceType": "KeyVault", + "KeyVaultUrl": "https://keyvault.vault.azure.net", + "KeyVaultCertificateName": "NewDecryptionCert" + }, + { + "SourceType": "KeyVault", + "KeyVaultUrl": "https://keyvault.vault.azure.net", + "KeyVaultCertificateName": "OldDecryptionCert" + } + ] + } +} +``` + +Microsoft.Identity.Web tries each credential until one successfully decrypts the token. + +--- + +## Token Decryption Key Rotation + +### Rotation Strategy + +**Step 1: Generate new certificate** + +Create a new certificate for token decryption (see Step 1 in Setup Guide). + +**Step 2: Upload new certificate to app registration** + +Upload the new public key and configure it as an additional encryption certificate (don't remove the old one yet). + +**Step 3: Deploy configuration with both certificates** + +```json +{ + "TokenDecryptionCredentials": [ + { "SourceType": "KeyVault", "KeyVaultCertificateName": "NewDecryptionCert" }, + { "SourceType": "KeyVault", "KeyVaultCertificateName": "OldDecryptionCert" } + ] +} +``` + +**Step 4: Wait for token refresh** + +Tokens are encrypted with the certificate configured in app registration. After updating app registration, newly issued tokens use the new certificate. Wait for all existing tokens to expire (typically 1 hour). + +**Step 5: Remove old certificate** + +Once all tokens are using the new certificate: +- Remove old certificate from configuration +- Remove old certificate from app registration +- Delete old certificate from Key Vault/store + +--- + +## Troubleshooting + +### Problem: "Unable to decrypt token" + +**Possible causes:** +- Decryption certificate not configured +- Certificate doesn't match app registration +- Private key not accessible +- Wrong certificate used (signature cert instead of encryption cert) + +**Solutions:** + +1. Verify token encryption is enabled: + - Check app registration > Token encryption + - Ensure certificate is uploaded and selected + +2. Verify decryption credentials are configured: + ```csharp + // Add logging to verify credentials are loaded + var decryptCreds = options.TokenDecryptionCredentials; + Console.WriteLine($"Decryption credentials: {decryptCreds?.Length ?? 0}"); + ``` + +3. Check certificate key usage: + ```bash + # Verify certificate has KeyEncipherment usage + openssl x509 -in cert.cer -noout -text | grep "Key Usage" + ``` + +### Problem: "The provided client credential is not valid" + +**Possible causes:** +- Private key not accessible +- Certificate expired +- Wrong certificate used (thumbprint mismatch) + +**Solutions:** + +1. Verify certificate is valid: + ```bash + # Check expiration + openssl pkcs12 -in mycert.pfx -nokeys -passin pass:password | openssl x509 -noout -dates + ``` + +2. Verify certificate is accessible from your application +3. Check private key permissions +4. Ensure certificate matches the one uploaded to app registration + +### Problem: Token decryption works locally but fails in production + +**Possible causes:** +- Different certificates in different environments +- Production certificate not accessible +- Key Vault permissions not configured + +**Solutions:** + +1. Use environment-specific configuration: + ```json + // appsettings.Production.json + { + "AzureAd": { + "TokenDecryptionCredentials": [ + { + "SourceType": "KeyVault", + "KeyVaultUrl": "https://prod-keyvault.vault.azure.net", + "KeyVaultCertificateName": "ProdDecryptionCert" + } + ] + } + } + ``` + +2. Verify managed identity has Key Vault access in production + +3. Check app registration has correct certificate for production + +--- + +## Security Best Practices + +### Certificate Management + +- β Use separate certificates for client credentials and token decryption +- β Store decryption certificates in Azure Key Vault +- β Rotate certificates regularly (annually or per policy) +- β Use certificates with appropriate key usage (KeyEncipherment) +- β Monitor certificate expiration + +### Key Protection + +- β Never expose private keys +- β Use Hardware Security Modules (HSM) for high-security scenarios +- β Limit private key access to necessary identities +- β Enable audit logging for key access + +### Token Handling + +- β Decrypt tokens immediately upon receipt +- β Don't log decrypted token contents +- β Validate tokens after decryption +- β Use short token lifetimes + +--- + +## Performance Considerations + +### Token Decryption Overhead + +Token decryption adds computational overhead: + +- β±οΈ **Decryption time:** ~1-5ms per token +- β±οΈ **Certificate retrieval:** ~10-50ms (cached) +- β±οΈ **Overall impact:** Minimal for most applications + +### Optimization Strategies + +**Caching:** +- β Microsoft.Identity.Web caches certificates automatically +- β Tokens are decrypted once per request +- β Use distributed cache for scale-out scenarios + +**Certificate location:** +- β Key Vault: Network call to retrieve certificate (cached) +- β Certificate Store: Local access (faster) +- β File: Local disk access (fastest, but less secure) + +--- + +## When to Use Token Decryption + +### Use Token Decryption When: + +- β **Compliance requires it** - Regulatory mandates for token encryption +- β **High-security applications** - Defense in depth strategy +- β **Zero-trust architecture** - Never trust, always verify +- β **Sensitive data handling** - Extra protection for critical data +- β **Untrusted networks** - Additional layer beyond HTTPS + +### Don't Use Token Decryption When: + +- β οΈ **HTTPS is sufficient** - Most applications are fine with HTTPS alone +- β οΈ **Added complexity not justified** - Overhead not worth the benefit +- β οΈ **No compliance requirement** - Not mandated by regulations +- β οΈ **Performance critical** - Milliseconds matter + +**Default recommendation:** Most applications don't need token decryption. Use it only when you have specific security or compliance requirements. + +--- + +## Additional Resources + +- **[Token Encryption in Microsoft Entra ID](https://learn.microsoft.com/azure/active-directory/develop/active-directory-certificate-credentials#token-encryption)** - Official documentation +- **[Certificate-Based Authentication](./certificates.md)** - Detailed certificate guidance + +--- + +## Next Steps + +- **[Back to Credentials Overview](./credentials-README.md)** - Compare all credential types +- **[Certificates Guide](./certificates.md)** - Certificate management +- **[Calling Downstream APIs](../../calling-downstream-apis/calling-downstream-apis-README.md)** - Use credentials to call APIs + +--- + +**Need help?** [Open an issue](https://github.com/AzureAD/microsoft-identity-web/issues) or check [troubleshooting guides](../../calling-downstream-apis/from-web-apps.md#troubleshooting). \ No newline at end of file diff --git a/docs/authentication/token-cache/token-cache-README.md b/docs/authentication/token-cache/token-cache-README.md new file mode 100644 index 000000000..377ec66ec --- /dev/null +++ b/docs/authentication/token-cache/token-cache-README.md @@ -0,0 +1,524 @@ +# Token Cache in Microsoft.Identity.Web + +Token caching is fundamental to application performance, reliability, and user experience. Microsoft.Identity.Web provides flexible token caching strategies that balance performance, persistence, and operational reliability. + +--- + +## π Table of Contents + +- [Overview](#overview) +- [Quick Start](#quick-start) +- [Choosing a Cache Strategy](#choosing-a-cache-strategy) +- [Cache Implementations](#cache-implementations) +- [Advanced Configuration](#advanced-configuration) +- [Next Steps](#next-steps) + +--- + +## Overview + +### What Are Tokens Cached? + +Microsoft.Identity.Web caches several types of tokens: + +| Token Type | Size | Scope | Eviction | +|------------|------|-------|----------| +| **Access Tokens** | ~2 KB | Per (user/app, tenant, resource) | Automatic (lifetime-based) | +| **Refresh Tokens** | Variable | Per user account | Manual or policy-based | +| **ID Tokens** | ~2-7 KB | Per user | Automatic | + +**Where token caching applies:** +- **[Web apps calling APIs](../../calling-downstream-apis/from-web-apps.md)** - User tokens for delegated access +- **[Web APIs calling downstream APIs](../../calling-downstream-apis/from-web-apis.md)** - OBO tokens (requires careful eviction policies) +- **Daemon applications** - App-only tokens for service-to-service calls + +### Why Cache Tokens? + +**Performance Benefits:** +- Reduces round trips to Microsoft Entra ID +- Faster API calls (L1: <10ms vs L2: ~30ms vs network: >100ms) +- Lower latency for end users + +**Reliability Benefits:** +- Continues working during temporary Entra ID outages +- Resilient to network transients +- Graceful degradation when distributed cache fails + +**Cost Benefits:** +- Reduces authentication requests (throttling avoidance) +- Lower Azure costs for authentication operations + +--- + +## Quick Start + +### Development - In-Memory Cache + +For development and samples, use the in-memory cache: + +```csharp +using Microsoft.Identity.Web; + +builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration, "AzureAd") + .EnableTokenAcquisitionToCallDownstreamApi() + .AddInMemoryTokenCaches(); +``` + +**β Pros:** +- Simple setup +- Fast performance +- No external dependencies + +**β Cons:** +- Cache lost on app restart. In a web app, you'll be signed-in (with the cookie) but a re-sign-in will be needed to get an access token, and populate the token cache +- Not suitable for production multi-server deployments +- Not shared across application instances + +--- + +### Production - Distributed Cache + +For production applications, especially multi-server deployments: + +```csharp +using Microsoft.Identity.Web; + +builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration, "AzureAd") + .EnableTokenAcquisitionToCallDownstreamApi() + .AddDistributedTokenCaches(); + +// Choose your cache implementation +builder.Services.AddStackExchangeRedisCache(options => +{ + options.Configuration = builder.Configuration.GetConnectionString("Redis"); + options.InstanceName = "MyApp_"; +}); +``` + +**β Pros:** +- Survives app restarts +- Shared across all application instances +- Automatic L1+L2 caching + +**β Cons:** +- Requires external cache infrastructure +- Additional configuration complexity +- Network latency for cache operations + +--- + +## Choosing a Cache Strategy + +```mermaid +flowchart TD + Start([Token CachingDecision]) --> Q1{ProductionEnvironment?} + + Q1 -->|No - Dev/Test| DevChoice[In-Memory CacheAddInMemoryTokenCaches] + Q1 -->|Yes| Q2{Multiple ServerInstances?} + + Q2 -->|No - Single Server| Q3{App RestartsAcceptable?} + Q3 -->|Yes| DevChoice + Q3 -->|No| DistChoice + + Q2 -->|Yes| DistChoice[Distributed CacheAddDistributedTokenCaches] + + DistChoice --> Q4{CacheImplementation?} + + Q4 -->|High Performance| Redis[Redis CacheStackExchange.Redisβ Recommended] + Q4 -->|Azure Native| Azure[Azure Cache for Redisor Azure Cosmos DB] + Q4 -->|On-Premises| SQL[SQL Server CacheAddDistributedSqlServerCache] + Q4 -->|Testing| DistMem[Distributed Memoryβ Not for production] + + Redis --> L1L2[Automatic L1+L2Caching] + Azure --> L1L2 + SQL --> L1L2 + DistMem --> L1L2 + + L1L2 --> Config[Configure OptionsMsalDistributedTokenCacheAdapterOptions] + DevChoice --> MemConfig[Configure Memory OptionsMsalMemoryTokenCacheOptions] + + style Start fill:#e1f5ff + style DevChoice fill:#d4edda + style DistChoice fill:#fff3cd + style Redis fill:#d1ecf1 + style L1L2 fill:#f8d7da +``` + +### Decision Matrix + +| Scenario | Recommended Cache | Rationale | +|----------|------------------|-----------| +| **Local development** | In-Memory | Simplicity, no infrastructure needed | +| **Samples/demos** | In-Memory | Easy setup for demonstrations | +| **Single-server production (restarts OK)** | In-Memory | Acceptable if sessions can be re-established | +| **Multi-server production** | Redis | Shared cache, high performance, reliable | +| **Azure-hosted applications** | Azure Cache for Redis | Native Azure integration, managed service | +| **On-premises enterprise** | SQL Server | Leverages existing infrastructure | +| **High-security environments** | SQL Server + Encryption | Data residency, encryption at rest | +| **Testing distributed scenarios** | Distributed Memory | Tests L2 cache behavior without infrastructure | + +--- + +## Cache Implementations + +### 1. In-Memory Cache + +**When to use:** +- Development and testing +- Single-server deployments with acceptable restart behavior +- Samples and prototypes + +**Configuration:** + +```csharp +builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration, "AzureAd") + .EnableTokenAcquisitionToCallDownstreamApi() + .AddInMemoryTokenCaches(); +``` + +**With custom options:** + +```csharp +builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration, "AzureAd") + .EnableTokenAcquisitionToCallDownstreamApi() + .AddInMemoryTokenCaches(options => + { + // Token cache entry will expire after this duration + options.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1); + + // Limit cache size (default is unlimited) + options.SizeLimit = 500 * 1024 * 1024; // 500 MB + }); +``` + +[β Learn more about in-memory cache configuration](#1-in-memory-cache) + +--- + +### 2. Distributed Cache (L2) with Automatic L1 Support + +**When to use:** +- Production multi-server deployments +- Applications requiring cache persistence across restarts +- High-availability scenarios + +**Key Feature:** Since Microsoft.Identity.Web v1.8.0, distributed cache automatically includes an in-memory L1 cache for performance and reliability. + +#### Redis Cache (Recommended) + +**appsettings.json:** +```json +{ + "ConnectionStrings": { + "Redis": "localhost:6379" + } +} +``` + +**Program.cs:** +```csharp +using Microsoft.Identity.Web; +using Microsoft.Identity.Web.TokenCacheProviders.Distributed; + +builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration, "AzureAd") + .EnableTokenAcquisitionToCallDownstreamApi() + .AddDistributedTokenCaches(); + +// Redis cache implementation +builder.Services.AddStackExchangeRedisCache(options => +{ + options.Configuration = builder.Configuration.GetConnectionString("Redis"); + options.InstanceName = "MyApp_"; // Unique prefix per application +}); + +// Optional: Configure distributed cache behavior +builder.Services.Configure(options => +{ + // Control L1 cache size + options.L1CacheOptions.SizeLimit = 500 * 1024 * 1024; // 500 MB + + // Handle L2 cache failures gracefully + options.OnL2CacheFailure = (exception) => + { + if (exception is StackExchange.Redis.RedisConnectionException) + { + // Log the failure + // Optionally attempt reconnection + return true; // Retry the operation + } + return false; // Don't retry + }; +}); +``` + +#### Azure Cache for Redis + +```csharp +builder.Services.AddStackExchangeRedisCache(options => +{ + options.Configuration = builder.Configuration.GetConnectionString("AzureRedis"); + options.InstanceName = "MyApp_"; +}); +``` + +**Connection string format:** +``` +.redis.cache.windows.net:6380,password=,ssl=True,abortConnect=False +``` + +#### SQL Server Cache + +```csharp +builder.Services.AddDistributedSqlServerCache(options => +{ + options.ConnectionString = builder.Configuration.GetConnectionString("TokenCacheDb"); + options.SchemaName = "dbo"; + options.TableName = "TokenCache"; + + // Set expiration longer than access token lifetime (default 1 hour) + // This prevents cache entries from expiring before tokens + options.DefaultSlidingExpiration = TimeSpan.FromMinutes(90); +}); +``` + +#### Azure Cosmos DB Cache + +```csharp +builder.Services.AddCosmosCache((CosmosCacheOptions options) => +{ + options.ContainerName = builder.Configuration["CosmosCache:ContainerName"]; + options.DatabaseName = builder.Configuration["CosmosCache:DatabaseName"]; + options.ClientBuilder = new CosmosClientBuilder( + builder.Configuration["CosmosCache:ConnectionString"]); + options.CreateIfNotExists = true; +}); +``` + +[β Learn more about distributed cache configuration](#2-distributed-cache-l2-with-automatic-l1-support) + +--- + +### 3. Session Cache (Not Recommended) + +**β οΈ Use with caution** - Session-based caching has significant limitations: + +```csharp +using Microsoft.Identity.Web.TokenCacheProviders.Session; + +// In Program.cs +builder.Services.AddSession(); + +builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration, "AzureAd") + .EnableTokenAcquisitionToCallDownstreamApi() + .AddSessionTokenCaches(); + +// In middleware pipeline +app.UseSession(); // Must be before UseAuthentication() +app.UseAuthentication(); +app.UseAuthorization(); +``` + +**β Limitations:** +- **Cookie size issues** - Large ID tokens with many claims cause problems +- **Scope conflicts** - Cannot use with singleton `TokenAcquisition` (e.g., Microsoft Graph SDK) +- **Session affinity required** - Doesn't work well in load-balanced scenarios +- **Not recommended** - Use distributed cache instead + +--- + +## Advanced Configuration + +### L1 Cache Control + +The L1 (in-memory) cache improves performance when using distributed caches: + +```csharp +builder.Services.Configure(options => +{ + // Control L1 cache size (default: 500 MB) + options.L1CacheOptions.SizeLimit = 100 * 1024 * 1024; // 100 MB + + // Disable L1 cache if session affinity is not available + // (forces all requests to use L2 cache for consistency) + options.DisableL1Cache = false; +}); +``` + +**When to disable L1:** +- No session affinity in load balancer +- Users frequently prompted for MFA due to cache inconsistency +- Trade-off: L2 access is slower (~30ms vs ~10ms) + +--- + +### Cache Eviction Policies + +Control when cached tokens are removed: + +```csharp +builder.Services.Configure(options => +{ + // Absolute expiration (removed after this time, regardless of use) + options.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(72); + + // Sliding expiration (renewed on each access) + options.SlidingExpiration = TimeSpan.FromHours(2); +}); +``` + +**Or configure via appsettings.json:** + +```json +{ + "TokenCacheOptions": { + "AbsoluteExpirationRelativeToNow": "72:00:00", + "SlidingExpiration": "02:00:00" + } +} +``` + +```csharp +builder.Services.Configure( + builder.Configuration.GetSection("TokenCacheOptions")); +``` + +**Recommendations:** +- Set expiration **longer than token lifetime** (tokens typically expire in 1 hour) +- Default: 90 minutes sliding expiration +- Balance between memory usage and user experience +- Consider: 72 hours absolute + 2 hours sliding for good UX + +[β Learn more about cache eviction strategies](#cache-eviction-policies) + +--- + +### Encryption at Rest + +Protect sensitive token data in distributed caches: + +#### Single Machine + +```csharp +builder.Services.Configure(options => +{ + options.Encrypt = true; // Uses ASP.NET Core Data Protection +}); +``` + +#### Distributed Systems (Multiple Servers) + +**β οΈ Critical:** Distributed systems **do not** share encryption keys by default. You must configure key sharing: + +**Azure Key Vault (Recommended):** + +```csharp +using Microsoft.AspNetCore.DataProtection; + +builder.Services.AddDataProtection() + .PersistKeysToAzureBlobStorage(new Uri(builder.Configuration["DataProtection:BlobUri"])) + .ProtectKeysWithAzureKeyVault( + new Uri(builder.Configuration["DataProtection:KeyIdentifier"]), + new DefaultAzureCredential()); +``` + +**Certificate-Based:** + +```csharp +builder.Services.AddDataProtection() + .PersistKeysToFileSystem(new DirectoryInfo(@"\\server\share\keys")) + .ProtectKeysWithCertificate( + new X509Certificate2("current.pfx", builder.Configuration["CertPassword"])) + .UnprotectKeysWithAnyCertificate( + new X509Certificate2("current.pfx", builder.Configuration["CertPassword"]), + new X509Certificate2("previous.pfx", builder.Configuration["PrevCertPassword"])); +``` + +[β Learn more about encryption and data protection](#encryption-at-rest) + +--- + +## Cache Performance Considerations + +### Token Size Estimates + +| Token Type | Typical Size | Per | Notes | +|------------|--------------|-----|-------| +| App tokens | ~2 KB | Tenant Γ Resource | Auto-evicted | +| User tokens | ~7 KB | User Γ Tenant Γ Resource | Manual eviction needed | +| Refresh tokens | Variable | User | Long-lived | + +### Memory Planning + +For **500 concurrent users** calling **3 APIs**: +- User tokens: 500 Γ 3 Γ 7 KB = **10.5 MB** +- With overhead: **~15-20 MB** + +For **10,000 concurrent users**: +- User tokens: 10,000 Γ 3 Γ 7 KB = **210 MB** +- With overhead: **~300-350 MB** + +**Recommendation:** Set L1 cache size limit based on expected concurrent users. + +--- + +## Next Steps + +### Documentation + +- **[Distributed Cache Configuration](#2-distributed-cache-l2-with-automatic-l1-support)** - L1/L2 architecture, configuration +- **[Cache Eviction Policies](#cache-eviction-policies)** - Managing OBO tokens, sliding expiration +- **[Troubleshooting Guide](troubleshooting.md)** - Common issues and solutions +- **[Encryption at Rest](#encryption-at-rest)** - Data protection in distributed systems + +### Using Token Caching in Your Application + +- **[Calling Downstream APIs from Web Apps](../../calling-downstream-apis/from-web-apps.md)** - User token acquisition and caching +- **[Calling Downstream APIs from Web APIs](../../calling-downstream-apis/from-web-apis.md)** - OBO token caching considerations +- **[Web App Quickstart](../../getting-started/quickstart-webapp.md)** - Getting started with authentication + +### Common Scenarios + +- [Redis Cache Configuration](#redis-cache-recommended) +- [Handling L2 Cache Failures](troubleshooting.md#l2-cache-not-being-written) +- [L1 Cache Control](#l1-cache-control) +- [Azure Cache for Redis](#azure-cache-for-redis) + +--- + +## Best Practices + +β **Use distributed cache in production** - Essential for multi-server deployments + +β **Set appropriate cache size limits** - Prevent unbounded memory growth + +β **Configure eviction policies** - Balance UX and memory usage + +β **Enable encryption for sensitive data** - Protect tokens at rest + +β **Monitor cache health** - Track hit rates, failures, and performance + +β **Handle L2 cache failures gracefully** - L1 cache ensures resilience + +β **Test cache behavior** - Verify restart scenarios and failover + +β **Don't use distributed memory cache in production** - Not persistent or distributed + +β **Don't use session cache** - Has significant limitations + +β **Don't set expiration shorter than token lifetime** - Forces unnecessary re-authentication + +β **Don't forget encryption key sharing** - Distributed systems need shared keys + +--- + +## Reference + +- [Token cache serialization (ASP.NET Core)](https://aka.ms/ms-id-web/token-cache-serialization) +- [ASP.NET Core Distributed Caching](https://learn.microsoft.com/aspnet/core/performance/caching/distributed) +- [Data Protection in ASP.NET Core](https://learn.microsoft.com/aspnet/core/security/data-protection/introduction) +- [Azure Cache for Redis](https://learn.microsoft.com/azure/azure-cache-for-redis/) + +--- + +**Last Updated:** October 27, 2025 +**Microsoft.Identity.Web Version:** 3.14.1+ diff --git a/docs/authentication/token-cache/troubleshooting.md b/docs/authentication/token-cache/troubleshooting.md new file mode 100644 index 000000000..bebfa298c --- /dev/null +++ b/docs/authentication/token-cache/troubleshooting.md @@ -0,0 +1,709 @@ +# Token Cache Troubleshooting + +This guide helps you diagnose and resolve common token caching issues in Microsoft.Identity.Web applications. + +--- + +## π Quick Issue Index + +- [L2 Cache Not Being Written](#l2-cache-not-being-written) +- [Deserialization Errors with Encryption](#deserialization-errors-with-encryption) +- [Memory Cache Growing Too Large](#memory-cache-growing-too-large) +- [Frequent MFA Prompts](#frequent-mfa-prompts) +- [Cache Connection Failures](#cache-connection-failures) +- [Token Cache Empty After Restart](#token-cache-empty-after-restart) +- [Session Cache Cookie Too Large](#session-cache-cookie-too-large) + +--- + +## L2 Cache Not Being Written + +### Symptoms + +- Distributed cache (Redis, SQL, Cosmos DB) appears empty +- No entries visible in cache monitoring tools +- Application works but cache doesn't persist across restarts + +### + + Root Causes + +1. **L2 cache connection failure** - Most common cause +2. **Misconfigured cache options** +3. **Encryption key issues** +4. **Network connectivity problems** + +### Diagnosis + +Check application logs for errors similar to: + +```text +fail: Microsoft.Identity.Web.TokenCacheProviders.Distributed.MsalDistributedTokenCacheAdapter[0] + [MsIdWeb] DistributedCache: Connection issue. InRetry? False Error message: + It was not possible to connect to the redis server(s). + UnableToConnect on localhost:5002/Interactive, Initializing/NotStarted, + last: NONE, origin: BeginConnectAsync, outstanding: 0, last-read: 2s ago, + last-write: 2s ago, keep-alive: 60s, state: Connecting, mgr: 10 of 10 available, + last-heartbeat: never, global: 9s ago, v: 2.2.4.27433 +``` + +### Solution + +**1. Verify cache configuration:** + +```csharp +// Check connection string is correct +builder.Services.AddStackExchangeRedisCache(options => +{ + options.Configuration = builder.Configuration.GetConnectionString("Redis"); + options.InstanceName = "MyApp_"; +}); +``` + +**2. Test cache connectivity:** + +```csharp +// Add this temporary code to test cache +var cache = app.Services.GetRequiredService(); +try +{ + await cache.SetStringAsync("test-key", "test-value"); + var value = await cache.GetStringAsync("test-key"); + Console.WriteLine($"Cache test successful: {value}"); +} +catch (Exception ex) +{ + Console.WriteLine($"Cache test failed: {ex.Message}"); +} +``` + +**3. Handle L2 failures gracefully:** + +```csharp +builder.Services.Configure(options => +{ + options.OnL2CacheFailure = (exception) => + { + // Log the failure + Console.WriteLine($"L2 cache failed: {exception.Message}"); + + if (exception is StackExchange.Redis.RedisConnectionException) + { + // Attempt reconnection logic here if needed + return true; // Retry the operation + } + + return false; // Don't retry for other exceptions + }; +}); +``` + +### Important Notes + +**β L1 Cache Provides Resilience:** +- When L2 cache fails, Microsoft.Identity.Web automatically falls back to L1 (in-memory) cache +- Users can continue to sign in and call APIs without disruption +- L2 cache becomes eventually consistent when back online + +**Verification:** +1. Check that `AddDistributedTokenCaches()` is called +2. Verify `IDistributedCache` implementation is registered +3. Confirm connection string and credentials are correct +4. Test network connectivity to cache endpoint + +--- + +## Deserialization Errors with Encryption + +### Symptoms + +```text +ErrorCode: json_parse_failed +Microsoft.Identity.Client.MsalClientException: +MSAL V3 Deserialization failed to parse the cache contents. +``` + +Or: + +```text +ErrorCode: json_parse_failed +Microsoft.Identity.Client.MsalClientException: +IDW10802: Exception occurred while deserializing token cache. +See https://aka.ms/msal-net-token-cache-serialization +``` + +### Root Causes + +1. **Encryption keys not shared** across distributed system (most common) +2. **Certificate rotation** without proper key migration +3. **Mismatched encryption configuration** between servers + +### Solution + +**Critical:** Distributed systems do NOT share encryption keys by default! + +#### Azure-Based Key Sharing (Recommended) + +```csharp +using Microsoft.AspNetCore.DataProtection; +using Azure.Identity; + +builder.Services.AddDataProtection() + .PersistKeysToAzureBlobStorage( + new Uri(builder.Configuration["DataProtection:BlobStorageUri"])) + .ProtectKeysWithAzureKeyVault( + new Uri(builder.Configuration["DataProtection:KeyVaultKeyUri"]), + new DefaultAzureCredential()); + +// Enable encryption in token cache +builder.Services.Configure(options => +{ + options.Encrypt = true; +}); +``` + +**appsettings.json:** +```json +{ + "DataProtection": { + "BlobStorageUri": "https://.blob.core.windows.net//", + "KeyVaultKeyUri": "https://.vault.azure.net/keys/" + } +} +``` + +#### Certificate-Based Key Sharing + +```csharp +builder.Services.AddDataProtection() + .PersistKeysToFileSystem(new DirectoryInfo(@"\\server\share\keys")) + .ProtectKeysWithCertificate( + new X509Certificate2("current-cert.pfx", Configuration["CurrentCertPassword"])) + .UnprotectKeysWithAnyCertificate( + new X509Certificate2("current-cert.pfx", Configuration["CurrentCertPassword"]), + new X509Certificate2("previous-cert.pfx", Configuration["PreviousCertPassword"])); +``` + +**β Certificate Rotation Best Practice:** +- Always include both **current** and **previous** certificates in `UnprotectKeysWithAnyCertificate()` +- This allows decryption of data protected with the old certificate during rotation + +#### Testing Encryption Across Servers + +```csharp +// Test on Server 1: Encrypt data +var protectionProvider = app.Services.GetRequiredService(); +var protector = protectionProvider.CreateProtector("TestPurpose"); +string protectedData = protector.Protect("test-message"); +Console.WriteLine($"Protected: {protectedData}"); + +// Test on Server 2: Decrypt data +var protectionProvider2 = app.Services.GetRequiredService(); +var protector2 = protectionProvider2.CreateProtector("TestPurpose"); +string unprotectedData = protector2.Unprotect(protectedData); +Console.WriteLine($"Unprotected: {unprotectedData}"); +``` + +### Reference + +- [Data Protection in ASP.NET Core](https://learn.microsoft.com/aspnet/core/security/data-protection/introduction) +- [Key encryption at rest](https://learn.microsoft.com/aspnet/core/security/data-protection/implementation/key-encryption-at-rest) + +--- + +## Memory Cache Growing Too Large + +### Symptoms + +- Application memory usage continuously increases +- Out of memory exceptions +- Server performance degrades over time +- Cache grows to gigabytes + +### Root Causes + +**Token Accumulation - Different Scenarios:** + +#### Web Apps (user sign-in/sign-out): +- **User tokens:** ~7 KB each - Removed on sign-out via `RemoveAccountAsync()` β +- Memory growth is typically manageable since user tokens are cleaned up + +#### Web APIs (OBO flow): +- **OBO tokens:** ~7 KB each - **NOT automatically removed** β +- Web APIs don't have user sign-in/sign-outβthey receive tokens from client apps +- When web APIs call downstream APIs on behalf of users (OBO flow), OBO tokens accumulate + +**Why OBO tokens accumulate in web API caches:** +1. User signs in to web app β web app gets user token +2. Web app calls web API β web API acquires OBO token to call downstream API +3. User signs out of web app β web app removes its user token via `RemoveAccountAsync()` +4. **Problem:** The OBO token in the web API's cache is NOT removed +5. User signs in again β new OBO token created, old one remains +6. Without eviction policies, these accumulate indefinitely in the web API's cache + +**App tokens:** +- ~2 KB each - Short-lived, automatically managed β +- Minimal impact on memory + +### Token Size Calculation Examples + +**Scenario 1: Web API with OBO flow (most problematic):** +``` +10,000 users Γ 3 downstream APIs Γ 7 KB per OBO token = 210 MB (current active OBO tokens) +After 5 user sign-in/sign-out cycles in web app: 1,050 MB (orphaned OBO tokens in web API) +With overhead: ~1.2-1.5 GB in the web API's cache +``` + +**Why this happens:** +- Each user sign-in/sign-out cycle in the **web app** creates new OBO tokens in the **web API** +- The web API has no knowledge of user sign-out events from the web app +- Old OBO tokens remain in the web API's cache indefinitely + +**Scenario 2: Web app (without calling APIs with OBO):** +``` +10,000 concurrent users Γ 7 KB per user token = 70 MB +(User tokens cleaned up on sign-out via RemoveAccountAsync) +With overhead: ~100-150 MB +``` + +### Solution + +#### 1. Set L1 Cache Size Limit + +```csharp +builder.Services.Configure(options => +{ + // Limit L1 cache to 500 MB (default) + options.L1CacheOptions.SizeLimit = 500 * 1024 * 1024; + + // For smaller deployments, reduce further + options.L1CacheOptions.SizeLimit = 100 * 1024 * 1024; // 100 MB +}); +``` + +#### 2. Configure Eviction Policies (Critical for Web APIs with OBO) + +**These policies apply to ALL cache entries, including orphaned OBO tokens in web APIs:** + +```csharp +// In your Web API's Program.cs or Startup.cs +builder.Services.Configure(options => +{ + // Remove ALL entries (including OBO tokens) after 72 hours regardless of usage + options.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(72); + + // Remove ALL entries (including OBO tokens) not accessed in 2 hours + // RECOMMENDED for web APIs: Use SlidingExpiration + options.SlidingExpiration = TimeSpan.FromHours(2); +}); +``` + +**Why SlidingExpiration is recommended for web APIs with OBO:** +- Active users' OBO tokens remain cached (good performance for ongoing requests) +- Inactive users' orphaned OBO tokens are automatically removed after inactivity +- Default OBO token lifetime is 1 hour; set expiration to 2+ hours +- Balances cache hit rate with memory management + +**Or via configuration:** + +```json +{ + "TokenCacheOptions": { + "AbsoluteExpirationRelativeToNow": "72:00:00", + "SlidingExpiration": "02:00:00" + } +} +``` + +```csharp +builder.Services.Configure( + builder.Configuration.GetSection("TokenCacheOptions")); +``` + +#### 3. Disable L1 Cache (if needed) + +If you cannot control memory growth and have session affinity: + +```csharp +builder.Services.Configure(options => +{ + // Forces all cache operations to L2 (slower but no memory growth) + options.DisableL1Cache = true; +}); +``` + +**Trade-off:** +- L1 cache access: <10ms +- L2 cache access: ~30-50ms +- Network call to Entra ID: >100ms + +### Monitoring + +Add logging to track cache size: + +```csharp +var cache = app.Services.GetRequiredService(); +var cacheStats = cache.GetCurrentStatistics(); +Console.WriteLine($"Current entries: {cacheStats?.CurrentEntryCount}"); +Console.WriteLine($"Current size: {cacheStats?.CurrentEstimatedSize} bytes"); +``` + +--- + +## Frequent MFA Prompts + +### Symptoms + +- Users prompted for MFA on every request or frequently +- MFA completed successfully but prompt appears again +- Occurs in multi-server deployments + +### Root Cause + +**Session affinity not configured** in load balancer: + +1. Request β Server 1: MFA needed, user completes MFA, tokens cached in Server 1's L1 cache +2. Next request β Server 2: Reads its own L1 cache, finds old tokens (without MFA claims) +3. Result: User prompted for MFA again + +### Solution + +#### Option A: Enable Session Affinity (Recommended) + +Configure your load balancer to route requests from the same user to the same server: + +**Azure App Service:** +- Enable "ARR Affinity" (enabled by default) + +**Azure Application Gateway:** +```json +{ + "backendHttpSettings": { + "affinityCookieName": "ApplicationGatewayAffinity", + "cookieBasedAffinity": "Enabled" + } +} +``` + +**NGINX:** +```nginx +upstream backend { + ip_hash; # Routes same IP to same server + server server1.example.com; + server server2.example.com; +} +``` + +#### Option B: Disable L1 Cache + +If session affinity is not possible: + +```csharp +builder.Services.Configure(options => +{ + // All servers use L2 cache directly (consistent but slower) + options.DisableL1Cache = true; +}); +``` + +**Performance Impact:** +- L1: <10ms per cache operation +- L2: ~30-50ms per cache operation +- Trade-off for consistency across servers + +### Verification + +Test your load balancer configuration: + +```bash +# Send multiple requests, check which server responds +for i in {1..10}; do + curl -b cookies.txt -c cookies.txt https://your-app.com/api/test +done +``` + +Check for consistent `Server` or `X-Server-ID` headers. + +--- + +## Cache Connection Failures + +### Symptoms + +```text +fail: Microsoft.Identity.Web.TokenCacheProviders.Distributed.MsalDistributedTokenCacheAdapter[0] + [MsIdWeb] DistributedCache: Connection issue. + RedisConnectionException: No connection is available to service this operation +``` + +### Common Causes + +1. **Redis server not running** +2. **Incorrect connection string** +3. **Firewall blocking connection** +4. **SSL/TLS configuration mismatch** +5. **Connection pool exhausted** + +### Solutions + +#### 1. Verify Redis Connectivity + +```bash +# Test Redis connection +redis-cli -h -p -a ping +``` + +Expected: `PONG` + +#### 2. Check Connection String + +**Local Redis:** +```json +{ + "ConnectionStrings": { + "Redis": "localhost:6379" + } +} +``` + +**Azure Cache for Redis:** +```json +{ + "ConnectionStrings": { + "Redis": ".redis.cache.windows.net:6380,password=,ssl=True,abortConnect=False" + } +} +``` + +**Connection string parameters:** +- `ssl=True` - Required for Azure Redis +- `abortConnect=False` - Allows retry on connection failure +- `connectTimeout=5000` - Connection timeout in milliseconds +- `syncTimeout=5000` - Operation timeout in milliseconds + +#### 3. Handle Transient Failures + +```csharp +builder.Services.AddStackExchangeRedisCache(options => +{ + options.Configuration = builder.Configuration.GetConnectionString("Redis"); + options.InstanceName = "MyApp_"; + + // Configure connection options + options.ConfigurationOptions = new StackExchange.Redis.ConfigurationOptions + { + AbortOnConnectFail = false, + ConnectTimeout = 5000, + SyncTimeout = 5000, + EndPoints = { ":" }, + Password = "", + Ssl = true + }; +}); + +// Handle L2 cache failures +builder.Services.Configure(options => +{ + options.OnL2CacheFailure = (exception) => + { + // Log for monitoring + Console.WriteLine($"Redis cache failure: {exception.Message}"); + + // Retry for connection issues + if (exception is StackExchange.Redis.RedisConnectionException) + { + return true; // Retry + } + + return false; // Don't retry other exceptions + }; +}); +``` + +#### 4. Monitor Cache Health + +```csharp +// Add health check +builder.Services.AddHealthChecks() + .AddRedis(builder.Configuration.GetConnectionString("Redis")); +``` + +--- + +## Token Cache Empty After Restart + +### Symptoms + +- Users must re-authenticate after application restart +- Cache appears empty after server restart +- Happens in production despite distributed cache + +### Root Causes + +1. **Using in-memory cache** instead of distributed cache +2. **L2 cache not properly configured** +3. **Distributed memory cache** (not persistent) + +### Solution + +#### Verify Distributed Cache Configuration + +**β Wrong - Using in-memory:** +```csharp +// This cache is lost on restart +.AddInMemoryTokenCaches() +``` + +**β Wrong - Distributed memory (not persistent):** +```csharp +// This is NOT persistent across restarts +builder.Services.AddDistributedMemoryCache(); +.AddDistributedTokenCaches() +``` + +**β Correct - Persistent distributed cache:** +```csharp +// Redis - persists across restarts +builder.Services.AddStackExchangeRedisCache(options => +{ + options.Configuration = builder.Configuration.GetConnectionString("Redis"); + options.InstanceName = "MyApp_"; +}); +.AddDistributedTokenCaches() +``` + +Or: + +```csharp +// SQL Server - persists across restarts +builder.Services.AddDistributedSqlServerCache(options => +{ + options.ConnectionString = builder.Configuration.GetConnectionString("TokenCacheDb"); + options.SchemaName = "dbo"; + options.TableName = "TokenCache"; + options.DefaultSlidingExpiration = TimeSpan.FromMinutes(90); +}); +.AddDistributedTokenCaches() +``` + +--- + +## Session Cache Cookie Too Large + +### Symptoms + +```text +Error: Headers too large +HTTP 400 Bad Request +Cookie size exceeds maximum allowed +``` + +### Root Cause + +Session cache stores tokens in cookies. Large ID tokens (many claims) cause cookies to exceed browser limits (typically 4KB per cookie). + +### Solution + +**β Don't use session cache** - Use distributed cache instead: + +```csharp +// Replace this: +.AddSessionTokenCaches() + +// With this: +.AddDistributedTokenCaches() + +builder.Services.AddStackExchangeRedisCache(options => +{ + options.Configuration = builder.Configuration.GetConnectionString("Redis"); + options.InstanceName = "MyApp_"; +}); +``` + +### Why Session Cache Is Not Recommended + +1. **Cookie size limitations** - Easily exceeded with many claims +2. **Scope issues** - Cannot use with singleton services (e.g., Microsoft Graph SDK) +3. **Performance** - Cookies sent with every request +4. **Security** - Sensitive data in cookies +5. **Scale** - Doesn't work well in load-balanced scenarios + +--- + +## Advanced Debugging + +### Enable Detailed Logging + +**appsettings.json:** +```json +{ + "Logging": { + "LogLevel": { + "Default": "Warning", + "Microsoft.Identity.Web": "Debug", + "Microsoft.Identity.Web.TokenCacheProviders": "Trace" + } + } +} +``` + +### Inspect Cache Contents + +```csharp +// For distributed cache +var cache = app.Services.GetRequiredService(); +var keys = /* implementation-specific way to list keys */; + +foreach (var key in keys) +{ + var value = await cache.GetStringAsync(key); + Console.WriteLine($"Key: {key}, Size: {value?.Length} bytes"); +} +``` + +### Test Cache Serialization + +```csharp +// Get the token cache adapter +var adapter = app.Services.GetRequiredService(); + +// This will log serialization/deserialization details +// Look for errors in application logs +``` + +--- + +## Getting Help + +If you're still experiencing issues: + +1. **Check logs** - Enable Debug/Trace logging for Microsoft.Identity.Web +2. **Verify configuration** - Review all cache-related configuration +3. **Test connectivity** - Ensure cache infrastructure is accessible +4. **Monitor performance** - Use Application Insights or similar tools +5. **Review documentation** - [Token Cache Overview](./token-cache-README.md) + +### Reporting Issues + +When reporting token cache issues, include: + +- Microsoft.Identity.Web version +- Cache implementation (Redis, SQL Server, etc.) +- Configuration code +- Error messages and stack traces +- Application logs with Debug level +- Infrastructure details (Azure, on-premises, etc.) + +--- + +## Related Documentation + +- [Token Cache Overview](token-cache-README.md) +- [Distributed Cache Configuration](token-cache-README.md#2-distributed-cache-l2-with-automatic-l1-support) +- [Cache Eviction Policies](token-cache-README.md#cache-eviction-policies) +- [Data Protection & Encryption](https://learn.microsoft.com/aspnet/core/security/data-protection/) + +--- + +**Last Updated:** October 27, 2025 +**Microsoft.Identity.Web Version:** 3.14.1+ diff --git a/docs/blog-posts/downstreamwebapi-to-downstreamapi.md b/docs/blog-posts/downstreamwebapi-to-downstreamapi.md index f68e0b4c5..f2cde54f4 100644 --- a/docs/blog-posts/downstreamwebapi-to-downstreamapi.md +++ b/docs/blog-posts/downstreamwebapi-to-downstreamapi.md @@ -74,6 +74,6 @@ To migrate your existing code using **IDownstreamWebApi** to Microsoft.Identity. ### Example code -The following sample illustrates the usage of IDownstreamApi: [ASP.NET Core web app calling web API/TodoListController]([https://github.com/AzureAD/microsoft-identity-web/pull/2036/files](https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/blob/jmprieur/relv2/4-WebApp-your-API/4-1-MyOrg/Client/Controllers/TodoListController.cs)). +The following sample illustrates the usage of IDownstreamApi: [ASP.NET Core web app calling web API/TodoListController](https://github.com/AzureAD/microsoft-identity-web/pull/2036/files). ### Differences between IDownstreamWebApi and IDownstreamApi diff --git a/docs/calling-downstream-apis/AgentIdentities-Readme.md b/docs/calling-downstream-apis/AgentIdentities-Readme.md new file mode 100644 index 000000000..36005c9ef --- /dev/null +++ b/docs/calling-downstream-apis/AgentIdentities-Readme.md @@ -0,0 +1,524 @@ +# Microsoft.Identity.Web.AgentIdentities + +Not .NET? See [Entra SDK container sidecar](https://github.com/AzureAD/microsoft-identity-web/blob/feature/doc-modernization/docs/sidecar/agent-identities.md) for the Entra SDK container documentation allowing support of agent identies in any language and platform. + +## Overview + +The Microsoft.Identity.Web.AgentIdentities NuGet package provides support for Agent Identities in Microsoft Entra ID. It enables applications to securely authenticate and acquire tokens for agent applications, agent identities, and agent user identities, which is useful for autonomous agents, interactive agents acting on behalf of their user, and agents having their own user identity. + +This package is part of the [Microsoft.Identity.Web](https://github.com/AzureAD/microsoft-identity-web) suite of libraries and was introduced in version 3.10.0. + +## Key Concepts + +### Agent identity blueprint + +An agent identity blueprint has a special application registration in Microsoft Entra ID that has permissions to act on behalf of Agent identities or Agent User identities. It's represented by its application ID (Agent identity blueprint Client ID). The agent identity blueprint is configured with credentials (typically FIC+MSI or client certificates) and permissions to acquire tokens for itself to call graph. This is the app that you develop. It's a confidential client application, usually a web API. The only permissions it can have are maintain (create / delete) Agent Identities (using the Microsoft Graph) + +### Agent Identity + +An agent identity is a special service principal in Microsoft Entra ID. It represents an identity that the agent identity blueprint created and is authorized to impersonate. It doesn't have credentials on its own. The agent identity blueprint can acquire tokens on behalf of the agent identity provided the user or tenant admin consented for the agent identity to the corresponding scopes. Autonomous agents acquire app tokens on behalf of the agent identity. Interactive agents called with a user token acquire user tokens on behalf of the agent identity. + +### Agent User Identity + +An agent user identity is an Agent identity that can also act as a user (think of an agent identity that would have its own mailbox, or would report to you in the directory). An agent application can acquire a token on behalf of an agent user identity. + +### Federated Identity Credentials (FIC) + +FIC is a trust mechanism in Microsoft Entra ID that enables applications to trust each other using OpenID Connect (OIDC) tokens. In the context of agent identities, FICs are used to establish trust between the agent application and agent identities, and agent identities and agent user identities. + +### More information +For details about Entra ID agent identities see [Microsoft Entra Agent ID documentation](https://learn.microsoft.com/entra/agent-id/) + +## Installation + +```bash +dotnet add package Microsoft.Identity.Web.AgentIdentities +``` + +## Usage + +### 1. Configure Services + +First, register the required services in your application: + +```csharp +// Add the core Identity Web services +services.AddTokenAcquisition(); +services.AddInMemoryTokenCaches(); +services.AddHttpClient(); + +// Add Microsoft Graph integration if needed. +// Requires the Microsoft.Identity.Web.GraphServiceClient package +services.AddMicrosoftGraph(); + +// Add Agent Identities support +services.AddAgentIdentities(); +``` + +### 2. Configure the Agent identity blueprint + +Configure your agent identity blueprint application with the necessary credentials using appsettings.json: + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "agent-application-client-id", + + "ClientCredentials": [ + { + "SourceType": "StoreWithDistinguishedName", + "CertificateStorePath": "LocalMachine/My", + "CertificateDistinguishedName": "CN=YourCertificateName" + } + + // Or for Federation Identity Credential with Managed Identity: + // { + // "SourceType": "SignedAssertionFromManagedIdentity", + // "ManagedIdentityClientId": "managed-identity-client-id" // Omit for system-assigned + // } + ] + } +} +``` + +Or, if you prefer, configure programmatically: + +```csharp +// Configure the information about the agent application +services.Configure( + options => + { + options.Instance = "https://login.microsoftonline.com/"; + options.TenantId = "your-tenant-id"; + options.ClientId = "agent-application-client-id"; + options.ClientCredentials = [ + CertificateDescription.FromStoreWithDistinguishedName( + "CN=YourCertificateName", StoreLocation.LocalMachine, StoreName.My) + ]; + }); +``` + +See https://aka.ms/ms-id-web/credential-description for all the ways to express credentials. + +On ASP.NET Core, use the override of services.Configure taking an authentication scheeme. Youy can also +use Microsoft.Identity.Web.Owin if you have an ASP.NET Core application on OWIN (not recommended for new +apps), or even create a daemon application. + +### 3. Use Agent Identities + +#### Agent Identity + +##### Autonomous agent + +For your autonomous agent application to acquire **app-only** tokens for an agent identity: + +```csharp +// Get the required services from the DI container +IAuthorizationHeaderProvider authorizationHeaderProvider = + serviceProvider.GetRequiredService(); + +// Configure options for the agent identity +string agentIdentity = "agent-identity-guid"; +var options = new AuthorizationHeaderProviderOptions() + .WithAgentIdentity(agentIdentity); + +// Acquire an access token for the agent identity +string authHeader = await authorizationHeaderProvider + .CreateAuthorizationHeaderForAppAsync("https://resource/.default", options); + +// The authHeader contains "Bearer " + the access token (or another protocol +// depending on the options) +``` + +##### Interactive agent + +For your interactive agent application to acquire **user** tokens for an agent identity on behalf of the user calling the web API: + +```csharp +// Get the required services from the DI container +IAuthorizationHeaderProvider authorizationHeaderProvider = + serviceProvider.GetRequiredService(); + +// Configure options for the agent identity +string agentIdentity = "agent-identity-guid"; +var options = new AuthorizationHeaderProviderOptions() + .WithAgentIdentity(agentIdentity); + +// Acquire an access token for the agent identity +string authHeader = await authorizationHeaderProvider + .CreateAuthorizationHeaderForAppAsync(["https://resource/.default"], options); + +// The authHeader contains "Bearer " + the access token (or another protocol +// depending on the options) +``` + +#### Agent User Identity + +For your agent application to acquire tokens on behalf of a agent user identity, you can use either the user's UPN (User Principal Name) or OID (Object ID). + +##### Using UPN (User Principal Name) + +```csharp +// Get the required services +IAuthorizationHeaderProvider authorizationHeaderProvider = + serviceProvider.GetRequiredService(); + +// Configure options for the agent user identity using UPN +string agentIdentity = "agent-identity-client-id"; +string userUpn = "user@contoso.com"; +var options = new AuthorizationHeaderProviderOptions() + .WithAgentUserIdentity(agentIdentity, userUpn); + +// Create a ClaimsPrincipal to enable token caching +ClaimsPrincipal user = new ClaimsPrincipal(); + +// Acquire a user token +string authHeader = await authorizationHeaderProvider + .CreateAuthorizationHeaderForUserAsync( + scopes: ["https://graph.microsoft.com/.default"], + options: options, + user: user); + +// The user object now has claims including uid and utid. If you use it +// in another call it will use the cached token. +``` + +##### Using OID (Object ID) + +```csharp +// Get the required services +IAuthorizationHeaderProvider authorizationHeaderProvider = + serviceProvider.GetRequiredService(); + +// Configure options for the agent user identity using OID +string agentIdentity = "agent-identity-client-id"; +Guid userOid = Guid.Parse("e1f76997-1b35-4aa8-8a58-a5d8f1ac4636"); +var options = new AuthorizationHeaderProviderOptions() + .WithAgentUserIdentity(agentIdentity, userOid); + +// Create a ClaimsPrincipal to enable token caching +ClaimsPrincipal user = new ClaimsPrincipal(); + +// Acquire a user token +string authHeader = await authorizationHeaderProvider + .CreateAuthorizationHeaderForUserAsync( + scopes: ["https://graph.microsoft.com/.default"], + options: options, + user: user); + +// The user object now has claims including uid and utid. If you use it +// in another call it will use the cached token. +``` + +### 4. Microsoft Graph Integration + +Install the Microsoft.Identity.Web.GraphServiceClient which handles authentication for the Graph SDK + +```bash +dotnet add package Microsoft.Identity.Web.AgentIdentities +``` + +Add the support for Microsoft Graph in your service collection. + +```bash +services.AddMicrosoftGraph(); +``` + +You can now get a GraphServiceClient from the service provider + +#### Using Agent Identity with Microsoft Graph: + +```csharp +// Get the GraphServiceClient +GraphServiceClient graphServiceClient = serviceProvider.GetRequiredService(); + +// Call Microsoft Graph APIs with the agent identity +var applications = await graphServiceClient.Applications + .GetAsync(r => r.Options.WithAuthenticationOptions(options => + { + options.WithAgentIdentity(agentIdentity); + options.RequestAppToken = true; + })); +``` + +#### Using Agent User Identity with Microsoft Graph: + +You can use either UPN or OID with Microsoft Graph: + +```csharp +// Get the GraphServiceClient +GraphServiceClient graphServiceClient = serviceProvider.GetRequiredService(); + +// Call Microsoft Graph APIs with the agent user identity using UPN +var me = await graphServiceClient.Me + .GetAsync(r => r.Options.WithAuthenticationOptions(options => + options.WithAgentUserIdentity(agentIdentity, userUpn))); + +// Or using OID +var me = await graphServiceClient.Me + .GetAsync(r => r.Options.WithAuthenticationOptions(options => + options.WithAgentUserIdentity(agentIdentity, userOid))); +``` + +### 5. Downstream API Integration + +To call other APIs using the IDownstreamApi abstraction: + +1. Install the Microsoft.Identity.Web.GraphServiceClient which handles authentication for the Graph SDK + +```bash +dotnet add package Microsoft.Identity.Web.DownstreamApi +``` + +2. Add a "DownstreamApis" section in your configuration, expliciting the parameters for your downstream API: + +```json +"AzureAd":{ + // usual config +}, +"DownstreamApis":{ + "MyApi": + { + "BaseUrl": "https://myapi.domain.com", + "Scopes": [ "https://myapi.domain.com/read", "https://myapi.domain.com/write" ] + } +} +``` + +3. Add the support for Downstream apis in your service collection. + +```bash +services.AddDownstreamApis(Configuration.GetSection("DownstreamApis")); +``` + +You can now access an `IDownstreamApi` service in the service provider, and call the "MyApi" API using +any Http verb + + +```csharp +// Get the IDownstreamApi service +IDownstreamApi downstreamApi = serviceProvider.GetRequiredService(); + +// Call API with agent identity +var response = await downstreamApi.GetForAppAsync( + "MyApi", + options => options.WithAgentIdentity(agentIdentity)); + +// Call API with agent user identity using UPN +var userResponse = await downstreamApi.GetForUserAsync( + "MyApi", + options => options.WithAgentUserIdentity(agentIdentity, userUpn)); + +// Or using OID +var userResponseByOid = await downstreamApi.GetForUserAsync( + "MyApi", + options => options.WithAgentUserIdentity(agentIdentity, userOid)); +``` + + +### 6. Azure SDKs integration + +To call Azure SDKs, use the MicrosoftIdentityAzureCredential class from the Microsoft.Identity.Web.Azure NuGet package. + +Install the Microsoft.Identity.Web.Azure package: + +```bash +dotnet add package Microsoft.Identity.Web.Azure +``` + +Add the support for Azure token credential in your service collection: + +```bash +services.AddMicrosoftIdentityAzureTokenCredential(); +``` + +You can now get a `MicrosoftIdentityTokenCredential` from the service provider. This class has a member Options to which you can apply the +`.WithAgentIdentity()` or `.WithAgentUserIdentity()` methods. + +See [Azure SDKs integration](./azure-sdks.md) for more details. + +### 7. HttpClient with MicrosoftIdentityMessageHandler Integration + +For scenarios where you want to use HttpClient directly with flexible authentication options, you can use the `MicrosoftIdentityMessageHandler` from the Microsoft.Identity.Web.TokenAcquisition package. + +Note: The Microsoft.Identity.Web.TokenAcquisition package is already referenced by Microsoft.Identity.Web.AgentIdentities. + +#### Using Agent Identity with MicrosoftIdentityMessageHandler: + +```csharp +// Configure HttpClient with MicrosoftIdentityMessageHandler in DI +services.AddHttpClient("MyApiClient", client => +{ + client.BaseAddress = new Uri("https://myapi.domain.com"); +}) +.AddMicrosoftIdentityMessageHandler(options => +{ + options.Scopes= { "https://myapi.domain.com/.default" } +}); + +// Usage in your service or controller +public class MyService +{ + private readonly HttpClient _httpClient; + + public MyService(IHttpClientFactory httpClientFactory) + { + _httpClient = httpClientFactory.CreateClient("MyApiClient"); + } + + public async Task CallApiWithAgentIdentity(string agentIdentity) + { + // Create request with agent identity authentication + var request = new HttpRequestMessage(HttpMethod.Get, "/api/data") + .WithAuthenticationOptions(options => + { + options.WithAgentIdentity(agentIdentity); + options.RequestAppToken = true; + }); + + var response = await _httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStringAsync(); + } +} +``` + +#### Using Agent User Identity with MicrosoftIdentityMessageHandler: + +```csharp +public async Task CallApiWithAgentUserIdentity(string agentIdentity, string userUpn) +{ + // Create request with agent user identity authentication + var request = new HttpRequestMessage(HttpMethod.Get, "/api/userdata") + .WithAuthenticationOptions(options => + { + options.WithAgentUserIdentity(agentIdentity, userUpn); + options.Scopes.Add("https://myapi.domain.com/user.read"); + }); + + var response = await _httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStringAsync(); +} +``` + +#### Manual HttpClient Configuration: + +You can also configure the handler manually for more control: + +```csharp +// Get the authorization header provider +IAuthorizationHeaderProvider headerProvider = + serviceProvider.GetRequiredService(); + +// Create the handler with default options +var handler = new MicrosoftIdentityMessageHandler( + headerProvider, + new MicrosoftIdentityMessageHandlerOptions + { + Scopes = { "https://graph.microsoft.com/.default" } + }); + +// Create HttpClient with the handler +using var httpClient = new HttpClient(handler); + +// Make requests with per-request authentication options +var request = new HttpRequestMessage(HttpMethod.Get, "https://graph.microsoft.com/v1.0/applications") + .WithAuthenticationOptions(options => + { + options.WithAgentIdentity(agentIdentity); + options.RequestAppToken = true; + }); + +var response = await httpClient.SendAsync(request); +``` + +The `MicrosoftIdentityMessageHandler` provides a flexible, composable way to add authentication to your HttpClient-based code while maintaining full compatibility with existing Microsoft Identity Web extension methods for agent identities. + +### Validate tokens from Agent identities + +Token validation of token acquired for agent identities or agent user identities is the same as for any web API. However you can: +- check if a token was issued for an agent identity and for which agent blueprint. + + ```csharp + HttpContext.User.GetParentAgentBlueprint() + ``` + returns the ClientId of the parent agent blueprint if the token is issued for an agent identity (or agent user identity)\ + +- check if a token was issued for an agent user identity. + + ```csharp + HttpContext.User.IsAgentUserIdentity() + ``` + +These 2 extensions methods, apply to both ClaimsIdentity and ClaimsPrincipal. + + +## Prerequisites + +### Microsoft Entra ID Configuration + +1. **Agent Application Configuration**: + - Register an agent application with the graph SDK + - Add client credentials for the agent application + - Grant appropriate API permissions, such as Application.ReadWrite.All to create agent identities + - Example configuration in JSON: + ```json + { + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "agent-application-id", + "ClientCredentials": [ + { + "SourceType": "StoreWithDistinguishedName", + "CertificateStorePath": "LocalMachine/My", + "CertificateDistinguishedName": "CN=YourCertName" + } + ] + } + } + ``` + +2. **Agent Identity Configuration**: + - Have the agent create an agent identity + - Grant appropriate API permissions based on what your agent identity needs to do + +3. **User Permission**: + - For agent user identity scenarios, ensure appropriate user permissions are configured. + +## How It Works + +Under the hood, the Microsoft.Identity.Web.AgentIdentities package: + +1. Uses Federated Identity Credentials (FIC) to establish trust between the agent application and agent identity and between the agent identity and the agent user identity. +2. Acquires FIC tokens using the `GetFicTokenAsync` method +3. Uses the FIC tokens to authenticate as the agent identity +4. For agent user identities, it leverages MSAL extensions to perform user token acquisition + +## Troubleshooting + +### Common Issues + +1. **Missing FIC Configuration**: Ensure Federated Identity Credentials are properly configured in Microsoft Entra ID between the agent application and agent identity. + +2. **Permission Issues**: Verify the agent application has sufficient permissions to manage agent identities and that the agent identities have enough permissions to call the downstream APIs. + +3. **Certificate Problems**: If you use a client certificate, make sure the certificate is registered in the app registration, and properly installed and accessible by the code of the agent application. + +4. **Token Acquisition Failures**: Enable logging to diagnose token acquisition failures: + ```csharp + services.AddLogging(builder => { + builder.AddConsole(); + builder.SetMinimumLevel(LogLevel.Debug); + }); + ``` + +## Resources + +- [Microsoft Entra ID documentation](https://docs.microsoft.com/en-us/azure/active-directory/) +- [Microsoft Identity Web documentation](https://github.com/AzureAD/microsoft-identity-web/wiki) +- [Workload Identity Federation](https://docs.microsoft.com/en-us/azure/active-directory/develop/workload-identity-federation) +- [Microsoft Graph SDK documentation](https://docs.microsoft.com/en-us/graph/sdks/sdks-overview) \ No newline at end of file diff --git a/src/Microsoft.Identity.Web.GraphServiceClient/Readme.md b/docs/calling-downstream-apis/GraphServiceClient-Readme.md similarity index 100% rename from src/Microsoft.Identity.Web.GraphServiceClient/Readme.md rename to docs/calling-downstream-apis/GraphServiceClient-Readme.md diff --git a/docs/calling-downstream-apis/azure-sdks.md b/docs/calling-downstream-apis/azure-sdks.md new file mode 100644 index 000000000..f877d340b --- /dev/null +++ b/docs/calling-downstream-apis/azure-sdks.md @@ -0,0 +1,499 @@ +# Calling Azure SDKs with MicrosoftIdentityTokenCredential + +This guide explains how to use `MicrosoftIdentityTokenCredential` from Microsoft.Identity.Web.Azure to authenticate Azure SDK clients (Storage, KeyVault, ServiceBus, etc.) with Microsoft Identity. + +## Overview + +The `MicrosoftIdentityTokenCredential` class implements Azure SDK's `TokenCredential` interface, enabling seamless integration between Microsoft.Identity.Web and Azure SDK clients. This allows you to use the same authentication configuration and token caching infrastructure across your entire application. + +### Benefits + +- **Unified Authentication**: Use the same auth configuration for web apps, APIs, and Azure services +- **Token Caching**: Automatic token caching and refresh +- **Delegated & App Permissions**: Support for both user and application tokens +- **Agent Identities**: Compatible with agent identities feature +- **Managed Identity**: Seamless integration with Azure Managed Identity + +## Installation + +Install the Azure integration package: + +```bash +dotnet add package Microsoft.Identity.Web.Azure +``` + +Then install the Azure SDK client packages you need: + +```bash +# Examples +dotnet add package Azure.Storage.Blobs +dotnet add package Azure.Security.KeyVault.Secrets +dotnet add package Azure.Messaging.ServiceBus +dotnet add package Azure.Data.Tables +``` + +## ASP.NET Core Setup + +### 1. Configure Services + +Add Azure token credential support: + +```csharp +using Microsoft.Identity.Web; + +var builder = WebApplication.CreateBuilder(args); + +// Add authentication +builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd")) + .EnableTokenAcquisitionToCallDownstreamApi() + .AddInMemoryTokenCaches(); + +// Add Azure token credential support +builder.Services.AddMicrosoftIdentityAzureTokenCredential(); + +builder.Services.AddControllersWithViews(); + +var app = builder.Build(); +app.UseAuthentication(); +app.UseAuthorization(); +app.MapControllers(); +app.Run(); +``` + +### 2. Configure appsettings.json + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + "ClientCredentials": [ + { + "SourceType": "SignedAssertionFromManagedIdentity" + } + ] + } +} +``` + +## Using MicrosoftIdentityTokenCredential + +### Inject and Use with Azure SDK Clients + +This code sample shows how to use MicrosoftIdentityTokenCredential with the Blob Storage. The same principle applies to all Azure SDKs + +```csharp +using Azure.Storage.Blobs; +using Microsoft.Identity.Web; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +[Authorize] +public class StorageController : Controller +{ + private readonly MicrosoftIdentityTokenCredential _credential; + private readonly IConfiguration _configuration; + + public StorageController( + MicrosoftIdentityTokenCredential credential, + IConfiguration configuration) + { + _credential = credential; + _configuration = configuration; + } + + public async Task ListBlobs() + { + // Create Azure SDK client with credential + var blobClient = new BlobServiceClient( + new Uri($"https://{_configuration["StorageAccountName"]}.blob.core.windows.net"), + _credential); + + var container = blobClient.GetBlobContainerClient("mycontainer"); + var blobs = new List(); + + await foreach (var blob in container.GetBlobsAsync()) + { + blobs.Add(blob.Name); + } + + return View(blobs); + } +} +``` + +## Delegated Permissions (User Tokens) + +Call Azure services on behalf of signed-in user. + +### Azure Storage Example + +```csharp +using Azure.Storage.Blobs; +using Microsoft.Identity.Web; + +[Authorize] +public class FileController : Controller +{ + private readonly MicrosoftIdentityTokenCredential _credential; + + public FileController(MicrosoftIdentityTokenCredential credential) + { + _credential = credential; + } + + public async Task UploadFile(IFormFile file) + { + // Credential will automatically acquire delegated token + var blobClient = new BlobServiceClient( + new Uri("https://myaccount.blob.core.windows.net"), + _credential); + + var container = blobClient.GetBlobContainerClient("uploads"); + await container.CreateIfNotExistsAsync(); + + var blob = container.GetBlobClient(file.FileName); + await blob.UploadAsync(file.OpenReadStream(), overwrite: true); + + return Ok($"File {file.FileName} uploaded"); + } +} +``` + + +## Application Permissions (App-Only Tokens) + +Call Azure services with application permissions (no user context). + +### Configuration + +```csharp +public class AzureService +{ + private readonly MicrosoftIdentityTokenCredential _credential; + + public AzureService(MicrosoftIdentityTokenCredential credential) + { + _credential = credential; + } + + public async Task> ListBlobsAsync() + { + // Configure credential for app-only token + _credential.Options.RequestAppToken = true; + + var blobClient = new BlobServiceClient( + new Uri("https://myaccount.blob.core.windows.net"), + _credential); + + var container = blobClient.GetBlobContainerClient("data"); + var blobs = new List(); + + await foreach (var blob in container.GetBlobsAsync()) + { + blobs.Add(blob.Name); + } + + return blobs; + } +} +``` + +### Daemon Application Example + +```csharp +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Web; +using Azure.Storage.Blobs; + +class Program +{ + static async Task Main(string[] args) + { + // Build service provider + var tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance(); + tokenAcquirerFactory.Services.AddMicrosoftIdentityAzureTokenCredential(); + var sp = tokenAcquirerFactory.Build(); + + // Get credential + var credential = sp.GetRequiredService(); + credential.Options.RequestAppToken = true; + + // Use with Azure SDK + var blobClient = new BlobServiceClient( + new Uri("https://myaccount.blob.core.windows.net"), + credential); + + var container = blobClient.GetBlobContainerClient("data"); + + await foreach (var blob in container.GetBlobsAsync()) + { + Console.WriteLine($"Blob: {blob.Name}"); + } + } +} +``` + +## Using with Agent Identities + +`MicrosoftIdentityTokenCredential` supports agent identities through the `Options` property: + +```csharp +using Microsoft.Identity.Web; + +public class AgentService +{ + private readonly MicrosoftIdentityTokenCredential _credential; + + public AgentService(MicrosoftIdentityTokenCredential credential) + { + _credential = credential; + } + + public async Task> ListBlobsForAgentAsync(string agentIdentity) + { + // Configure for agent identity + _credential.Options.WithAgentIdentity(agentIdentity); + _credential.Options.RequestAppToken = true; + + var blobClient = new BlobServiceClient( + new Uri("https://myaccount.blob.core.windows.net"), + _credential); + + var container = blobClient.GetBlobContainerClient("agent-data"); + var blobs = new List(); + + await foreach (var blob in container.GetBlobsAsync()) + { + blobs.Add(blob.Name); + } + + return blobs; + } + + public async Task GetSecretForAgentUserAsync(string agentIdentity, Guid userOid, string secretName) + { + // Configure for agent user identity + _credential.Options.WithAgentUserIdentity(agentIdentity, userOid); + + var secretClient = new SecretClient( + new Uri("https://myvault.vault.azure.net"), + _credential); + + var secret = await secretClient.GetSecretAsync(secretName); + return secret.Value.Value; + } +} +``` + +See [Agent Identities documentation](./AgentIdentities-Readme.md) for more details. + +## FIC+Managed Identity Integration + +`MicrosoftIdentityTokenCredential` works seamlessly with FIC+Azure Managed Identity: + +### Configuration for Managed Identity + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + "ClientCredentials": [ + { + "SourceType": "SignedAssertionFromManagedIdentity" + } + ] + } +} +``` + +### Using System-Assigned Managed Identity + +```csharp +// No additional code needed! +// When deployed to Azure, the credential automatically uses managed identity + +public class StorageService +{ + private readonly MicrosoftIdentityTokenCredential _credential; + + public StorageService(MicrosoftIdentityTokenCredential credential) + { + _credential = credential; + _credential.Options.RequestAppToken = true; + } + + public async Task> ListContainersAsync() + { + // Uses managed identity when running in Azure + var blobClient = new BlobServiceClient( + new Uri("https://myaccount.blob.core.windows.net"), + _credential); + + var containers = new List(); + await foreach (var container in blobClient.GetBlobContainersAsync()) + { + containers.Add(container.Name); + } + + return containers; + } +} +``` + +### Using User-Assigned Managed Identity + +```json +{ + "AzureAd": { + "ClientCredentials": [ + { + "SourceType": "SignedAssertionFromManagedIdentity", + "ManagedIdentityClientId": "user-assigned-identity-client-id" + } + ] + } +} +``` + +## OWIN Implementation + +For ASP.NET applications using OWIN: + +```csharp +using Microsoft.Identity.Web; +using Microsoft.Identity.Web.OWIN; +using Owin; + +public class Startup +{ + public void Configuration(IAppBuilder app) + { + app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType); + app.UseCookieAuthentication(new CookieAuthenticationOptions()); + + OwinTokenAcquirerFactory factory = TokenAcquirerFactory.GetDefaultInstance(); + + app.AddMicrosoftIdentityWebApp(factory); + factory.Services + .AddMicrosoftIdentityAzureTokenCredential(); + factory.Build(); + } +} +``` + +## Best Practices + +### 1. Reuse Azure SDK Clients + +Azure SDK clients are thread-safe and should be reused, but `MicrosoftIdentityTokenCredential` is a scoped service. You can't use it with AddAzureServices() which creates singletons. + +### 2. Use Managed Identity in Production + +```csharp +// β Good: Certificateless auth with managed identity +{ + "ClientCredentials": [ + { + "SourceType": "SignedAssertionFromManagedIdentity" + } + ] +} +``` + +### 3. Handle Azure SDK Exceptions + +```csharp +using Azure; + +try +{ + var blob = await blobClient.DownloadAsync(); +} +catch (RequestFailedException ex) when (ex.Status == 404) +{ + // Blob not found +} +catch (RequestFailedException ex) when (ex.Status == 403) +{ + // Insufficient permissions +} +catch (RequestFailedException ex) +{ + _logger.LogError(ex, "Azure SDK call failed with status {Status}", ex.Status); +} +``` + +### 5. Use Configuration for URIs + +```csharp +// β Bad: Hardcoded URIs +var blobClient = new BlobServiceClient(new Uri("https://myaccount.blob.core.windows.net"), credential); + +// β Good: Configuration-driven +var storageUri = _configuration["Azure:Storage:Uri"]; +var blobClient = new BlobServiceClient(new Uri(storageUri), credential); +``` + +## Troubleshooting + +### Error: "ManagedIdentityCredential authentication failed" + +**Cause**: Managed identity not enabled or misconfigured. + +**Solution**: +- Enable managed identity on Azure resource (App Service, VM, etc.) +- For user-assigned identity, specify `ManagedIdentityClientId` +- Verify identity has required role assignments + +### Error: "This request is not authorized to perform this operation" + +**Cause**: Missing Azure RBAC role assignment. + +**Solution**: +- Assign appropriate role to managed identity or user +- Example: "Storage Blob Data Contributor" for blob operations +- Wait up to 5 minutes for role assignments to propagate + +### Token Acquisition Fails Locally + +**Cause**: Managed identity only works in Azure. + +**Solution**: Use different credential source locally: + +```json +{ + "ClientCredentials": [ + { + "SourceType": "ClientSecret", + "ClientSecret": "secret-for-local-dev" + } + ] +} +``` + +### Scope Errors with Azure Resources + +**Cause**: Incorrect scope format. + +**Solution**: Use Azure resource-specific scopes: +- Storage: `https://storage.azure.com/user_impersonation` or `.default` +- KeyVault: `https://vault.azure.net/user_impersonation` or `.default` +- Service Bus: `https://servicebus.azure.net/user_impersonation` or `.default` + +## Related Documentation + +- [Azure SDK for .NET Documentation](https://learn.microsoft.com/dotnet/azure/sdk/overview) +- [Managed Identity Documentation](https://learn.microsoft.com/azure/active-directory/managed-identities-azure-resources/overview) +- [Credentials Configuration](../authentication/credentials/credentials-README.md) +- [Agent Identities](./AgentIdentities-Readme.md) +- [Calling Downstream APIs Overview](calling-downstream-apis-README.md) + +--- + +**Next Steps**: Learn about [calling custom APIs](custom-apis.md) with IDownstreamApi and IAuthorizationHeaderProvider. diff --git a/docs/calling-downstream-apis/calling-downstream-apis-README.md b/docs/calling-downstream-apis/calling-downstream-apis-README.md new file mode 100644 index 000000000..b135b591d --- /dev/null +++ b/docs/calling-downstream-apis/calling-downstream-apis-README.md @@ -0,0 +1,426 @@ +# Calling Downstream APIs with Microsoft.Identity.Web + +This guide helps you choose and implement the right approach for calling downstream APIs (Microsoft Graph, Azure services, or custom APIs) from your ASP.NET Core, OWIN or .NET applications using Microsoft.Identity.Web. + +## π― Choosing the Right Approach + +Use this decision tree to select the best method for your scenario: + +| API Type / Scenario | Decision / Criteria | Recommended Client/Class | +|--------------------------------------|---------------------------------------|---------------------------------------------------------| +| Microsoft Graph | You need to call Microsoft Graph APIS | GraphServiceClient | +| Azure SDK (Storage, KeyVault, etc.) | You need to call Azure APIs (Azure SDK) | MicrosoftIdentityTokenCredential with Azure SDK clients | +| Custom API | simple, configurable | IDownstreamApi | +| Custom API | using HttpClient + delegating handler | MicrosoftIdentityMessageHandler | +| Custom API | using your HttpClient | IAuthorizationHeaderProvider | + +## π Comparison Table + +| Approach | Best For | Complexity | Configuration | Flexibility | +|----------|----------|------------|---------------|-------------| +| **GraphServiceClient** | Microsoft Graph APIs | Low | Simple | Medium | +| **MicrosoftIdentityTokenCredential** | Azure SDK clients | Low | Simple | Low | +| **IDownstreamApi** | REST APIs with standard patterns | Low | JSON + Code | Medium | +| **MicrosoftIdentityMessageHandler** | HttpClient with auth pipeline | Medium | Code | High | +| **IAuthorizationHeaderProvider** | Custom auth logic | High | Code | Very High | + +## π Token Acquisition Patterns + +Microsoft.Identity.Web supports three main token acquisition patterns: + +```mermaid +graph LR + A[Token Acquisition] --> B[DelegatedOn behalf of user] + A --> C[App-OnlyApplication permissions in all apps] + A --> D[On-Behalf-Of OBOin web API] + + B --> B1[Web Apps] + B --> B2[Daemon acting as user / user agent] + C --> C1[Daemon Apps] + C --> C2[Web APIs with app permissions] + D --> D1[Web APIs calling other APIs] + + style B fill:#cfe2ff + style C fill:#fff3cd + style D fill:#f8d7da +``` + +### Delegated Permissions (User Tokens) +- **Scenario**: Web app calls API on behalf of signed-in user, and autonomous agent user identity. +- **Token type**: Access token with delegated permissions +- **Methods**: `CreateAuthorizationHeaderForUserAsync()`, `GetForUserAsync()` + +### Application Permissions (App-Only Tokens) +- **Scenario**: Daemon app or background service calls API. Autonmous agent identity +- **Token type**: Access token with application permissions +- **Methods**: `CreateAuthorizationHeaderForAppAsync()`, `GetForAppAsync()` + +### On-Behalf-Of (OBO) +- **Scenario**: Web API receives user token, calls another API on behalf of that user and interactive agents. +- **Token type**: New access token via OBO flow +- **Methods**: `CreateAuthorizationHeaderForUserAsync()` from web API context + +## π Quick Start Examples + +### Microsoft Graph (Recommended for Graph APIs) + +```csharp +// Installation +// dotnet add package Microsoft.Identity.Web.GraphServiceClient + +// Startup configuration +using Microsoft.Identity.Web; + +builder.Services.AddMicrosoftGraph(); + +// Usage in controller +public class HomeController : Controller +{ + private readonly GraphServiceClient _graphClient; + + public HomeController(GraphServiceClient graphClient) + { + _graphClient = graphClient; + } + + public async Task Profile() + { + // Delegated - calls on behalf of signed-in user + var user = await _graphClient.Me.GetAsync(); + + // App-only - requires app permissions + var users = await _graphClient.Users + .GetAsync(r => r.Options.WithAppOnly()); + + return View(user); + } +} +``` + +[π Learn more about Microsoft Graph integration](microsoft-graph.md) + +[π GraphServiceClient migration and detailed usage](GraphServiceClient-Readme.md) + +### Azure SDKs (Recommended for Azure Services) + +```csharp +// Installation +// dotnet add package Microsoft.Identity.Web.Azure +// dotnet add package Azure.Storage.Blobs + +// Startup configuration +using Microsoft.Identity.Web; + +builder.Services.AddMicrosoftIdentityAzureTokenCredential(); + +// Usage +public class StorageService +{ + private readonly MicrosoftIdentityTokenCredential _credential; + + public StorageService(MicrosoftIdentityTokenCredential credential) + { + _credential = credential; + } + + public async Task> ListBlobsAsync() + { + var blobClient = new BlobServiceClient( + new Uri("https://myaccount.blob.core.windows.net"), + _credential); + + var container = blobClient.GetBlobContainerClient("mycontainer"); + var blobs = new List(); + + await foreach (var blob in container.GetBlobsAsync()) + { + blobs.Add(blob.Name); + } + + return blobs; + } +} +``` + +[π Learn more about Azure SDK integration](azure-sdks.md) + +### IDownstreamApi (Recommended for Custom REST APIs) + +```csharp +// Installation +// dotnet add package Microsoft.Identity.Web.DownstreamApi + +// appsettings.json +{ + "DownstreamApis": { + "MyApi": { + "BaseUrl": "https://myapi.example.com", + "Scopes": ["api://myapi/read", "api://myapi/write"] + } + } +} + +// Startup configuration +using Microsoft.Identity.Web; + +builder.Services.AddDownstreamApis( + builder.Configuration.GetSection("DownstreamApis")); + +// Usage +public class ApiService +{ + private readonly IDownstreamApi _api; + + public ApiService(IDownstreamApi api) + { + _api = api; + } + + public async Task GetProductAsync(int id) + { + // Delegated - on behalf of user + return await _api.GetForUserAsync( + "MyApi", + $"api/products/{id}" + ); + } + + public async Task> GetAllProductsAsync() + { + // App-only - using app permissions + return await _api.GetForAppAsync>( + "MyApi", + "api/products"); + } +} +``` + +[π Learn more about IDownstreamApi](custom-apis.md) + +### MicrosoftIdentityMessageHandler (For HttpClient Integration) + +```csharp +// Startup configuration +using Microsoft.Identity.Web; + +builder.Services.AddHttpClient("MyApiClient", client => +{ + client.BaseAddress = new Uri("https://myapi.example.com"); +}) +.AddHttpMessageHandler(sp => new MicrosoftIdentityMessageHandler( + sp.GetRequiredService(), + new MicrosoftIdentityMessageHandlerOptions + { + Scopes = new[] { "api://myapi/.default" } + })); + +// Usage +public class ApiService +{ + private readonly HttpClient _httpClient; + + public ApiService(IHttpClientFactory httpClientFactory) + { + _httpClient = httpClientFactory.CreateClient("MyApiClient"); + } + + public async Task GetProductAsync(int id) + { + var request = new HttpRequestMessage(HttpMethod.Get, $"api/products/{id}") + .WithAuthenticationOptions(options => + { + options.RequestAppToken = false; // Use delegated token + options.scopes = [ "myApi.scopes" ]; + }); + + var response = await _httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + + return await response.Content.ReadFromJsonAsync(); + } +} +``` + +[π Learn more about MicrosoftIdentityMessageHandler](custom-apis.md#microsoftidentitymessagehandler) + +### IAuthorizationHeaderProvider (Maximum Flexibility) + +```csharp +// Direct usage for custom scenarios +public class CustomAuthService +{ + private readonly IAuthorizationHeaderProvider _headerProvider; + + public CustomAuthService(IAuthorizationHeaderProvider headerProvider) + { + _headerProvider = headerProvider; + } + + public async Task CallApiAsync() + { + // Get auth header (includes "Bearer " + token) + string authHeader = await _headerProvider + .CreateAuthorizationHeaderForUserAsync( + scopes: new[] { "api://myapi/.default" }); + + using var client = new HttpClient(); + client.DefaultRequestHeaders.Add("Authorization", authHeader); + client.DefaultRequestHeaders.Add("X-Custom-Header", "MyValue"); + + var response = await client.GetStringAsync("https://myapi.example.com/data"); + return response; + } +} +``` + +[π Learn more about IAuthorizationHeaderProvider](custom-apis.md#iauthorizationheaderprovider) + +## π Configuration Patterns + +Microsoft.Identity.Web supports both JSON configuration and code-based configuration. + +### appsettings.json Configuration + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + "ClientCredentials": [ + { + "SourceType": "SignedAssertionFromManagedIdentity" + } + ] + }, + "DownstreamApis": { + "MicrosoftGraph": { + "BaseUrl": "https://graph.microsoft.com/v1.0", + "Scopes": ["User.Read", "Mail.Read"] + }, + "MyApi": { + "BaseUrl": "https://myapi.example.com", + "Scopes": ["api://myapi/read"] + } + } +} +``` + +**Note**: For daemon/console apps, set `appsettings.json` properties: **"Copy to Output Directory" = "Copy if newer"** + +[π Learn more about credentials configuration](../authentication/credentials/credentials-README.md) + +### Code-Based Configuration + +```csharp +// Explicit configuration in code +builder.Services.Configure(options => +{ + options.Instance = "https://login.microsoftonline.com/"; + options.TenantId = "your-tenant-id"; + options.ClientId = "your-client-id"; + options.ClientCredentials = new[] + { + CertificateDescription.FromKeyVault( + "https://myvault.vault.azure.net", + "MyCertificate") + }; +}); + +builder.Services.AddDownstreamApi("MyApi", options => +{ + options.BaseUrl = "https://myapi.example.com"; + options.Scopes = new[] { "api://myapi/read" }; +}); +``` + +## π Scenario-Specific Guides + +The best approach depends on where you're calling the API from: + +### From Web Apps +- **Primary pattern**: Delegated permissions (on behalf of user) +- **Token acquisition**: Happens automatically during sign-in +- **Special considerations**: Incremental consent, handling consent failures + +[π Read the Web Apps guide](from-web-apps.md) + +### From Web APIs +- **Primary pattern**: On-Behalf-Of (OBO) flow +- **Token acquisition**: Exchange incoming token for downstream token +- **Special considerations**: Long-running processes, token caching, agent identities. + +[π Read the Web APIs guide](from-web-apis.md) + +### From Daemon Apps scenarios (also happen in web apps and APIs) +- **Primary pattern**: Application permissions (app-only) +- **Token acquisition**: Client credentials flow +- **Special considerations**: No user context, requires admin consent +- **Advanced**: Autonomous agents, agent user identities + +[π Read the Daemon Applications guide](../getting-started/daemon-app.md) + +## β οΈ Error Handling + +All token acquisition methods can throw exceptions that you should handle. +In web apps the `[AuthorizeForScope(scopes)]` attribute handles user incremental +consent or re-signing. + +```csharp +using Microsoft.Identity.Abstractions; + +try +{ + var result = await _api.GetForUserAsync("MyApi", "api/data"); +} +catch (MicrosoftIdentityWebChallengeUserException ex) +{ + // User needs to sign in or consent to additional scopes + // In web apps, this triggers a redirect to Azure AD + throw; +} +catch (HttpRequestException ex) +{ + // Downstream API returned error + _logger.LogError(ex, "API call failed"); +} +``` + +### Common Error Scenarios + +| Exception | Meaning | Solution | +|-----------|---------|----------| +| `MicrosoftIdentityWebChallengeUserException` | User consent required | Redirect to Azure AD for consent. Use AuthorizeForScopes attribute or ConsentHandler class | +| `MsalUiRequiredException` | Interactive auth needed | Handle in web apps with challenge | +| `MsalServiceException` | Azure AD service error | Check configuration, retry | +| `HttpRequestException` | Downstream API error | Handle API-specific errors | + +## π Related Documentation + +- **[Credentials Configuration](../authentication/credentials/credentials-README.md)** - How to configure authentication credentials +- **[Web App Scenarios](../getting-started/quickstart-webapp.md)** - Building web applications +- **[Web API Scenarios](../getting-started/quickstart-webapi.md)** - Building and protecting APIs +- **[Agent Identities](./AgentIdentities-Readme.md)** - Calling downstream APIs from agent identities. + +## π¦ NuGet Packages + +| Package | Purpose | When to Use | +|---------|---------|-------------| +| **Microsoft.Identity.Web.TokenAcquisition** | Token acquisition services | core package | +| **Microsoft.Identity.Web.DownstreamApi** | IDownstreamApi abstraction | Calling REST APIs | +| **Microsoft.Identity.Web.GraphServiceClient** | Microsoft Graph integration | Calling Microsoft Graph ([migration guide](GraphServiceClient-Readme.md)) | +| **Microsoft.Identity.Web.Azure** | Azure SDK integration | Calling Azure services | +| **Microsoft.Identity.Web** | ASP.NET Core web apps and web APIs | ASP.NET Core | +| **Microsoft.Identity.Web.OWIN** | ASP.NET OWIN web apps and web APIs | OWIN | + +## π Next Steps + +1. **Choose your approach** using the decision tree above +2. **Read the scenario-specific guide** for your application type +3. **Configure credentials** following the [credentials guide](../authentication/credentials/credentials-README.md) +4. **Implement and test** using the code examples provided +5. **Handle errors** gracefully using the patterns shown + +--- + +**Version Support**: This documentation covers Microsoft.Identity.Web 3.14.1+ with .NET 8 and .NET 9 examples. diff --git a/docs/calling-downstream-apis/custom-apis.md b/docs/calling-downstream-apis/custom-apis.md index 0b0200cd9..3d4f17638 100644 --- a/docs/calling-downstream-apis/custom-apis.md +++ b/docs/calling-downstream-apis/custom-apis.md @@ -1,44 +1,296 @@ -# Calling Custom APIs with MicrosoftIdentityMessageHandler +# Calling Custom APIs -This guide explains how to use `MicrosoftIdentityMessageHandler` for HttpClient integration to call custom downstream APIs with automatic Microsoft Identity authentication. +This guide explains the different approaches for calling your own protected APIs using Microsoft.Identity.Web: IDownstreamApi, IAuthorizationHeaderProvider, and MicrosoftIdentityMessageHandler. -## Table of Contents +## Overview -- [Overview](#overview) -- [MicrosoftIdentityMessageHandler - For HttpClient Integration](#microsoftidentitymessagehandler---for-httpclient-integration) - - [Before: Manual Setup](#before-manual-setup) - - [After: Using Extension Methods](#after-using-extension-methods) - - [Configuration Examples](#configuration-examples) -- [Per-Request Options](#per-request-options) -- [Advanced Scenarios](#advanced-scenarios) +When calling custom REST APIs, you have three main options depending on your needs: -## Overview +| Approach | Complexity | Flexibility | Use Case | +|----------|------------|-------------|----------| +| **IDownstreamApi** | Low | Medium | Standard REST APIs with configuration | +| **MicrosoftIdentityMessageHandler** | Medium | High | HttpClient with DI and composable pipeline | +| **IAuthorizationHeaderProvider** | High | Very High | Complete control over HTTP requests | -`MicrosoftIdentityMessageHandler` is a `DelegatingHandler` that automatically adds authorization headers to outgoing HTTP requests. The new `AddMicrosoftIdentityMessageHandler` extension methods make it easy to configure HttpClient instances with automatic Microsoft Identity authentication. +## IDownstreamApi - Recommended for Most Scenarios -## MicrosoftIdentityMessageHandler - For HttpClient Integration +`IDownstreamApi` provides a simple, configuration-driven approach for calling REST APIs with automatic token acquisition. + +### Installation + +```bash +dotnet add package Microsoft.Identity.Web.DownstreamApi +``` + +### Configuration + +Define your API in appsettings.json: + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + "ClientCredentials": [ + { + "SourceType": "ClientSecret", + "ClientSecret": "your-client-secret" + } + ] + }, + "DownstreamApis": { + "MyApi": { + "BaseUrl": "https://api.example.com", + "Scopes": ["api://my-api-client-id/read", "api://my-api-client-id/write"], + "RelativePath": "api/v1", + "RequestAppToken": false + }, + "PartnerApi": { + "BaseUrl": "https://partner.example.com", + "Scopes": ["api://partner-api-id/.default"], + "RequestAppToken": true + } + } +} +``` + +### ASP.NET Core Setup + +```csharp +using Microsoft.Identity.Web; + +var builder = WebApplication.CreateBuilder(args); -### Before: Manual Setup +// Add authentication +builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd")) + .EnableTokenAcquisitionToCallDownstreamApi() + .AddInMemoryTokenCaches(); + +// Register downstream APIs +builder.Services.AddDownstreamApis( + builder.Configuration.GetSection("DownstreamApis")); + +builder.Services.AddControllersWithViews(); + +var app = builder.Build(); +app.UseAuthentication(); +app.UseAuthorization(); +app.MapControllers(); +app.Run(); +``` -Previously, you had to manually configure the message handler: +### Basic Usage ```csharp -// In Program.cs or Startup.cs -services.AddHttpClient("MyApiClient", client => +using Microsoft.Identity.Abstractions; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +[Authorize] +public class ProductsController : Controller { - client.BaseAddress = new Uri("https://api.example.com"); -}) -.AddHttpMessageHandler(serviceProvider => new MicrosoftIdentityMessageHandler( - serviceProvider.GetRequiredService(), - new MicrosoftIdentityMessageHandlerOptions - { - Scopes = { "https://api.example.com/.default" } - })); + private readonly IDownstreamApi _api; + + public ProductsController(IDownstreamApi api) + { + _api = api; + } + + // GET request + public async Task Index() + { + var products = await _api.GetForUserAsync>( + "MyApi", + "products"); + + return View(products); + } + + // Call downstream API with GET request with query parameters + public async Task Details(int id) + { + var product = await _api.GetForUserAsync( + "MyApi", + $"products/{id}"); + + return View(product); + } + + // Call downstream API with POST request + [HttpPost] + public async Task Create([FromBody] Product product) + { + var created = await _api.PostForUserAsync( + "MyApi", + "products", + product); + + return CreatedAtAction(nameof(Details), new { id = created.Id }, created); + } + + // Call downstream API with PUT request + [HttpPut("{id}")] + public async Task Update(int id, [FromBody] Product product) + { + var updated = await _api.PutForUserAsync( + "MyApi", + $"products/{id}", + product); + + return Ok(updated); + } + + // Call downstream API with DELETE request + [HttpDelete("{id}")] + public async Task Delete(int id) + { + await _api.DeleteForUserAsync( + "MyApi", + $"products/{id}"); + + return NoContent(); + } +} ``` -### After: Using Extension Methods +### Advanced IDownstreamApi Options -Now you can use the convenient extension methods: +#### Custom Headers and Options + +```csharp +public async Task GetDataWithHeaders() +{ + var options = new DownstreamApiOptions + { + CustomizeHttpRequestMessage = message => + { + message.Headers.Add("X-Custom-Header", "MyValue"); + message.Headers.Add("X-Request-Id", Guid.NewGuid().ToString()); + message.Headers.Add("X-Correlation-Id", HttpContext.TraceIdentifier); + } + }; + + var data = await _api.CallApiForUserAsync( + "MyApi", + options, + content: null); + + return Ok(data); +} +``` + +#### Override Configuration Per Request + +```csharp +public async Task CallDifferentEndpoint() +{ + var options = new DownstreamApiOptions + { + BaseUrl = "https://alternative-api.example.com", + RelativePath = "v2/data", + Scopes = new[] { "api://alternative/.default" }, + RequestAppToken = true + }; + + var data = await _api.CallApiForAppAsync( + "MyApi", + options); + + return Ok(data); +} +``` + +#### Query Parameters + +```csharp +public async Task Search(string query, int page, int pageSize) +{ + var options = new DownstreamApiOptions + { + RelativePath = $"search?q={Uri.EscapeDataString(query)}&page={page}&pageSize={pageSize}" + }; + + var results = await _api.GetForUserAsync( + "MyApi", + options); + + return Ok(results); +} +``` + +You can also use the options.ExtraQueryParameters dictionary. + +#### Handling Response Headers + +```csharp +public async Task GetWithHeaders() +{ + var response = await _api.CallApiAsync( + "MyApi", + options => + { + options.RelativePath = "data"; + }); + + // Access response headers + if (response.Headers.TryGetValues("X-RateLimit-Remaining", out var values)) + { + var remaining = values.FirstOrDefault(); + _logger.LogInformation("Rate limit remaining: {Remaining}", remaining); + } + + return Ok(response.Content); +} +``` + +### App-Only Tokens with IDownstreamApi + +```csharp +[ApiController] +[Route("api/[controller]")] +public class DataController : ControllerBase +{ + private readonly IDownstreamApi _api; + + public DataController(IDownstreamApi api) + { + _api = api; + } + + [HttpGet("batch")] + public async Task GetBatchData() + { + // Call with application permissions + var data = await _api.GetForAppAsync( + "MyApi", + "batch/process"); + + return Ok(data); + } +} +``` + +## MicrosoftIdentityMessageHandler - For HttpClient Integration + +### When to Use + +- You need fine-grained control over HTTP requests +- You want to compose multiple message handlers +- You're integrating with existing HttpClient-based code +- You need access to raw HttpResponseMessage + +## How to use +`MicrosoftIdentityMessageHandler` is a `DelegatingHandler` that adds authentication to HttpClient requests. Use this when you need full HttpClient functionality with automatic token acquisition. +The `AddMicrosoftIdentityMessageHandler` extension methods provide a clean, flexible way to configure HttpClient with automatic Microsoft Identity authentication: + +- **Parameterless**: For per-request configuration flexibility +- **Options instance**: For pre-configured options objects +- **Action delegate**: For inline configuration (most common) +- **IConfiguration**: For configuration from appsettings.json + +Choose the overload that best fits your scenario and enjoy automatic authentication for your downstream API calls. #### 1. Parameterless Overload (Per-Request Configuration) @@ -247,7 +499,7 @@ services.AddHttpClient("InventoryApiClient", client => "InventoryApi"); ``` -## Per-Request Options +### Per-Request Options You can override default options on a per-request basis using the `WithAuthenticationOptions` extension method: @@ -287,9 +539,9 @@ public class MyService } ``` -## Advanced Scenarios +### Advanced Scenarios -### Agent Identity +#### Agent Identity Use agent identity when your application needs to act on behalf of another application: @@ -303,7 +555,7 @@ services.AddHttpClient("AgentClient") }); ``` -### Composing with Other Handlers +#### Composing with Other Handlers You can chain multiple handlers in the pipeline: @@ -317,7 +569,7 @@ services.AddHttpClient("ApiClient") .AddHttpMessageHandler(); ``` -### WWW-Authenticate Challenge Handling +#### WWW-Authenticate Challenge Handling `MicrosoftIdentityMessageHandler` automatically handles WWW-Authenticate challenges for Conditional Access scenarios: @@ -372,13 +624,325 @@ public class MyService } ``` -## Summary +## IAuthorizationHeaderProvider - Maximum Control -The `AddMicrosoftIdentityMessageHandler` extension methods provide a clean, flexible way to configure HttpClient with automatic Microsoft Identity authentication: +`IAuthorizationHeaderProvider` gives you direct access to authorization headers for complete control over HTTP requests. -- **Parameterless**: For per-request configuration flexibility -- **Options instance**: For pre-configured options objects -- **Action delegate**: For inline configuration (most common) -- **IConfiguration**: For configuration from appsettings.json +### When to Use + +- You need complete control over HTTP request construction +- You're integrating with non-standard HTTP APIs +- You need to use HttpClient without DI +- You're building custom HTTP abstractions + +### Basic Usage + +```csharp +using Microsoft.Identity.Abstractions; + +[Authorize] +public class CustomApiController : Controller +{ + private readonly IAuthorizationHeaderProvider _headerProvider; + private readonly ILogger _logger; + + public CustomApiController( + IAuthorizationHeaderProvider headerProvider, + ILogger logger) + { + _headerProvider = headerProvider; + _logger = logger; + } + + public async Task GetData() + { + // Get authorization header (includes "Bearer " prefix) + var authHeader = await _headerProvider.CreateAuthorizationHeaderForUserAsync( + scopes: new[] { "api://my-api/read" }); + + using var client = new HttpClient(); + client.DefaultRequestHeaders.Add("Authorization", authHeader); + client.DefaultRequestHeaders.Add("X-Custom-Header", "MyValue"); + + var response = await client.GetAsync("https://api.example.com/data"); + response.EnsureSuccessStatusCode(); + + var content = await response.Content.ReadAsStringAsync(); + return Content(content, "application/json"); + } +} +``` + +### App-Only Tokens + +```csharp +public async Task GetBackgroundData() +{ + // Get app-only authorization header + var authHeader = await _headerProvider.CreateAuthorizationHeaderForAppAsync( + scopes: new[] { "api://my-api/.default" }); + + using var client = new HttpClient(); + client.DefaultRequestHeaders.Add("Authorization", authHeader); + + var response = await client.GetAsync("https://api.example.com/background"); + var data = await response.Content.ReadFromJsonAsync(); + + return Ok(data); +} +``` + +### With Custom HTTP Libraries + +```csharp +public async Task CallWithRestSharp() +{ + var authHeader = await _headerProvider.CreateAuthorizationHeaderForUserAsync( + scopes: new[] { "api://my-api/read" }); + + // Example with RestSharp + var client = new RestClient("https://api.example.com"); + var request = new RestRequest("data", Method.Get); + request.AddHeader("Authorization", authHeader); + + var response = await client.ExecuteAsync(request); + + return Ok(response.Data); +} +``` + +### Advanced Options + +```csharp +public async Task GetDataWithOptions() +{ + var options = new AuthorizationHeaderProviderOptions + { + Scopes = new[] { "api://my-api/read" }, + RequestAppToken = false, + AcquireTokenOptions = new AcquireTokenOptions + { + AuthenticationOptionsName = JwtBearerDefaults.AuthenticationScheme, + ForceRefresh = false, + Claims = null + } + }; + + var authHeader = await _headerProvider.CreateAuthorizationHeaderAsync(options); + + using var client = new HttpClient(); + client.DefaultRequestHeaders.Add("Authorization", authHeader); + + var response = await client.GetAsync("https://api.example.com/data"); + var data = await response.Content.ReadFromJsonAsync(); + + return Ok(data); +} +``` + +## Comparison and Decision Guide + +### Use IDownstreamApi When: + +β Calling standard REST APIs +β Want configuration-driven approach +β Need automatic serialization/deserialization +β Want minimal code +β Following Microsoft.Identity.Web patterns + +**Example:** +```csharp +var product = await _api.GetForUserAsync("MyApi", "products/123"); +``` + +### Use MicrosoftIdentityMessageHandler When: + +β Need full HttpClient capabilities +β Want to compose multiple handlers +β Using HttpClientFactory patterns +β Need access to HttpResponseMessage +β Integrating with existing HttpClient code + +**Example:** +```csharp +var response = await _httpClient.GetAsync("api/products/123"); +var product = await response.Content.ReadFromJsonAsync(); +``` + +### Use IAuthorizationHeaderProvider When: + +β Need complete control over HTTP requests +β Using custom HTTP libraries +β Building custom abstractions +β Can't use HttpClientFactory +β Need to manually construct requests + +**Example:** +```csharp +var authHeader = await _headerProvider.CreateAuthorizationHeaderForUserAsync(scopes); +client.DefaultRequestHeaders.Add("Authorization", authHeader); +``` + +## Error Handling + +### IDownstreamApi Errors + +```csharp +try +{ + var data = await _api.GetForUserAsync("MyApi", "data"); +} +catch (MicrosoftIdentityWebChallengeUserException ex) +{ + // User needs to consent + _logger.LogWarning(ex, "Consent required for scopes: {Scopes}", string.Join(", ", ex.Scopes)); + throw; // Let ASP.NET Core handle consent flow +} +catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound) +{ + return NotFound("Resource not found"); +} +catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.Unauthorized) +{ + return Unauthorized("API returned 401"); +} +catch (Exception ex) +{ + _logger.LogError(ex, "API call failed"); + return StatusCode(500, "An error occurred"); +} +``` + +### MicrosoftIdentityMessageHandler Errors + +```csharp +try +{ + var response = await _httpClient.GetAsync("api/data"); + + if (!response.IsSuccessStatusCode) + { + var error = await response.Content.ReadAsStringAsync(); + _logger.LogError("API returned {StatusCode}: {Error}", response.StatusCode, error); + return StatusCode((int)response.StatusCode, error); + } + + var data = await response.Content.ReadFromJsonAsync(); + return Ok(data); +} +catch (HttpRequestException ex) +{ + _logger.LogError(ex, "HTTP request failed"); + return StatusCode(500, "Failed to call API"); +} +``` + +## Best Practices + +### 1. Configure Timeout Values + +```csharp +builder.Services.AddDownstreamApi("MyApi", options => +{ + options.BaseUrl = "https://api.example.com"; + options.HttpClientName = "MyApi"; +}); + +builder.Services.AddHttpClient("MyApi", client => +{ + client.Timeout = TimeSpan.FromSeconds(30); +}); +``` + +### 2. Use Typed Clients + +```csharp +public interface IProductApiClient +{ + Task> GetProductsAsync(); + Task GetProductAsync(int id); + Task CreateProductAsync(Product product); +} + +public class ProductApiClient : IProductApiClient +{ + private readonly IDownstreamApi _api; + + public ProductApiClient(IDownstreamApi api) + { + _api = api; + } + + public Task
Thank you for using our application.