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 + +``` + +**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 Scheme
Specified?} + 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 using
managed identity + MSI->>MIW: Return signed assertion + MIW->>AAD: Request access token
with assertion + AAD->>AAD: Validate assertion
and 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[Development
Client Secrets] --> Test[Testing
Client Secrets
+ Key Vault] + Test --> Staging[Staging
Certificates or
Certificateless] + Staging --> Prod[Production
Certificates or
Certificateless
NO 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 certificate
management?} + + Q1 -->|Yes| Q2{Running on Azure?} + Q1 -->|No| Q5{Production
environment?} + + Q2 -->|Yes| FIC[βœ… FIC + Managed Identity
Certificateless] + Q2 -->|No| Q3{Can use other
certificateless?} + + Q3 -->|Yes| OtherCertless[βœ… Other Certificateless
Methods] + Q3 -->|No| Q5 + + Q5 -->|Yes| Q6{Need credential
rotation?} + Q5 -->|No| DevCreds[ℹ️ Development Credentials
Secrets or File Certs] + + Q6 -->|Yes| Q7{On Windows?} + Q6 -->|No| KeyVault[βœ… Key Vault
with Managed Certs] + + Q7 -->|Yes| CertStore[βœ… Certificate Store
with 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 encryption
with public key + + Note over App,AAD: Runtime: Acquire and decrypt token + MIW->>AAD: Request token
(using client credentials) + AAD->>AAD: Encrypt token with
app'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 with
private 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 Caching
Decision]) --> Q1{Production
Environment?} + + Q1 -->|No - Dev/Test| DevChoice[In-Memory Cache
AddInMemoryTokenCaches] + Q1 -->|Yes| Q2{Multiple Server
Instances?} + + Q2 -->|No - Single Server| Q3{App Restarts
Acceptable?} + Q3 -->|Yes| DevChoice + Q3 -->|No| DistChoice + + Q2 -->|Yes| DistChoice[Distributed Cache
AddDistributedTokenCaches] + + DistChoice --> Q4{Cache
Implementation?} + + Q4 -->|High Performance| Redis[Redis Cache
StackExchange.Redis
⭐ Recommended] + Q4 -->|Azure Native| Azure[Azure Cache for Redis
or Azure Cosmos DB] + Q4 -->|On-Premises| SQL[SQL Server Cache
AddDistributedSqlServerCache] + Q4 -->|Testing| DistMem[Distributed Memory
❌ Not for production] + + Redis --> L1L2[Automatic L1+L2
Caching] + Azure --> L1L2 + SQL --> L1L2 + DistMem --> L1L2 + + L1L2 --> Config[Configure Options
MsalDistributedTokenCacheAdapterOptions] + DevChoice --> MemConfig[Configure Memory Options
MsalMemoryTokenCacheOptions] + + 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[Delegated
On behalf of user] + A --> C[App-Only
Application permissions in all apps] + A --> D[On-Behalf-Of OBO
in 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> GetProductsAsync() => + _api.GetForUserAsync>("MyApi", "products"); + + public Task GetProductAsync(int id) => + _api.GetForUserAsync("MyApi", $"products/{id}"); + + public Task CreateProductAsync(Product product) => + _api.PostForUserAsync("MyApi", "products", product); +} + +// Register +builder.Services.AddScoped(); +``` + +### 3. Log Request Details + +```csharp +public async Task GetDataWithLogging() +{ + _logger.LogInformation("Calling MyApi for data"); + + var stopwatch = Stopwatch.StartNew(); + + try + { + var data = await _api.GetForUserAsync("MyApi", "data"); + + stopwatch.Stop(); + _logger.LogInformation("API call succeeded in {ElapsedMs}ms", stopwatch.ElapsedMilliseconds); + + return Ok(data); + } + catch (Exception ex) + { + stopwatch.Stop(); + _logger.LogError(ex, "API call failed after {ElapsedMs}ms", stopwatch.ElapsedMilliseconds); + throw; + } +} +``` + +## OWIN Implementation + +```csharp +using Microsoft.Identity.Web; +using Microsoft.Identity.Web.OWIN; +using Owin; + +public class Startup +{ + public void Configuration(IAppBuilder app) + { + OwinTokenAcquirerFactory factory = TokenAcquirerFactory.GetDefaultInstance(); + + app.AddMicrosoftIdentityWebApp(factory); + factory.Services + .AddDownstreamApis(factory.Configuration.GetSection("DownstreamAPI")) + .AddInMemoryTokenCaches(); + factory.Build(); + } +} +``` + +## Related Documentation + +- [Calling from Web Apps](from-web-apps.md) +- [Calling from Web APIs](from-web-apis.md) +- [Microsoft Graph Integration](microsoft-graph.md) +- [Agent Identities](./AgentIdentities-Readme.md) + +--- -Choose the overload that best fits your scenario and enjoy automatic authentication for your downstream API calls! +**Next Steps**: Review the [main documentation](calling-downstream-apis-README.md) for decision tree and comparison of all approaches. diff --git a/docs/calling-downstream-apis/from-web-apis.md b/docs/calling-downstream-apis/from-web-apis.md new file mode 100644 index 000000000..9f63894c9 --- /dev/null +++ b/docs/calling-downstream-apis/from-web-apis.md @@ -0,0 +1,597 @@ +# Calling Downstream APIs from Web APIs + +This guide explains how to call downstream APIs from ASP.NET Core and OWIN web APIs using Microsoft.Identity.Web, focusing on the **On-Behalf-Of (OBO) flow** where your API receives a token from a client and exchanges it for a new token to call another API. + +## Overview + +The On-Behalf-Of (OBO) flow enables your web API to call downstream APIs on behalf of the user who called your API. This maintains the user's identity and permissions throughout the call chain. + +### On-Behalf-Of Flow + +```mermaid +sequenceDiagram + participant Client as Client App + participant YourAPI as Your Web API + participant AzureAD as Azure AD + participant DownstreamAPI as Downstream API + + Client->>YourAPI: 1. Call with access token + Note over YourAPI: Validate token + YourAPI->>AzureAD: 2. OBO request with user token + AzureAD->>AzureAD: 3. Validate & check consent + AzureAD->>YourAPI: 4. New access token for downstream API + Note over YourAPI: Cache token for user + YourAPI->>DownstreamAPI: 5. Call with new token + DownstreamAPI->>YourAPI: 6. Return data + YourAPI->>Client: 7. Return processed data +``` + +## Prerequisites + +- Web API configured with JWT Bearer authentication +- App registration with API permissions to downstream API +- Client app must have permissions to call your API +- User must have consented to both your API and downstream API + +## ASP.NET Core Implementation + +### 1. Configure Authentication + +Set up JWT Bearer authentication with explicit authentication scheme: + +```csharp +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")) + .EnableTokenAcquisitionToCallDownstreamApi() + .AddInMemoryTokenCaches(); + +builder.Services.AddAuthorization(); +builder.Services.AddControllers(); + +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-api-client-id", + "ClientCredentials": [ + { + "SourceType": "ClientSecret", + "ClientSecret": "your-client-secret" + } + ], + "Audience": "api://your-api-client-id" + }, + "DownstreamApis": { + "GraphAPI": { + "BaseUrl": "https://graph.microsoft.com/v1.0", + "Scopes": ["https://graph.microsoft.com/.default"] + }, + "PartnerAPI": { + "BaseUrl": "https://partnerapi.example.com", + "Scopes": ["api://partner-api-id/read"] + } + } +} +``` + +### 3. Add Downstream API Support + +```csharp +using Microsoft.Identity.Web; + +builder.Services.AddDownstreamApis( + builder.Configuration.GetSection("DownstreamApis")); +``` + +### 4. Call Downstream API from Your API + +```csharp +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Identity.Web; +using Microsoft.Identity.Abstractions; + +[Authorize] +[ApiController] +[Route("api/[controller]")] +public class DataController : ControllerBase +{ + private readonly IDownstreamApi _downstreamApi; + private readonly ILogger _logger; + + public DataController( + IDownstreamApi downstreamApi, + ILogger logger) + { + _downstreamApi = downstreamApi; + _logger = logger; + } + + [HttpGet("userdata")] + public async Task> GetUserData() + { + try + { + // Call downstream API using OBO flow + // Token from incoming request is automatically used + var userData = await _downstreamApi.GetForUserAsync( + "PartnerAPI", + "api/users/me"); + + return Ok(userData); + } + catch (MicrosoftIdentityWebChallengeUserException ex) + { + // User needs to consent to downstream API permissions + _logger.LogWarning(ex, "User consent required for downstream API"); + return Unauthorized(new { error = "consent_required", scopes = ex.Scopes }); + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "Downstream API call failed"); + return StatusCode(500, "Failed to retrieve data from downstream service"); + } + } + + [HttpPost("process")] + public async Task> ProcessData([FromBody] DataRequest request) + { + // Call downstream API with POST + var result = await _downstreamApi.PostForUserAsync( + "PartnerAPI", + "api/process", + request); + + return Ok(result); + } +} +``` + +## Token cache + +### In-Memory Cache (Development) + +```csharp +builder.Services.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")) + .EnableTokenAcquisitionToCallDownstreamApi() + .AddInMemoryTokenCaches(); +``` + +⚠️ **Warning**: Use distributed cache for production. + +### Distributed Cache (Production) + +For production APIs with multiple instances, use distributed caching: + +```csharp +using Microsoft.Extensions.Caching.StackExchangeRedis; + +builder.Services.AddStackExchangeRedisCache(options => +{ + options.Configuration = builder.Configuration.GetConnectionString("Redis"); + options.InstanceName = "MyWebApi"; +}); + +builder.Services.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")) + .EnableTokenAcquisitionToCallDownstreamApi() + .AddDistributedTokenCaches(); +``` + +### Other Distributed Cache Options + +```csharp +// SQL Server +builder.Services.AddDistributedSqlServerCache(options => +{ + options.ConnectionString = builder.Configuration.GetConnectionString("TokenCacheDb"); + options.SchemaName = "dbo"; + options.TableName = "TokenCache"; +}); + +// Cosmos DB +builder.Services.AddCosmosDbTokenCaches(options => +{ + options.DatabaseId = "TokenCache"; + options.ContainerId = "Tokens"; +}); +``` + +## Long-Running Processes with OBO + +For long-running background processes, you need special handling because the user's token may expire. + +### The Challenge + +```mermaid +graph TD + A[Client calls API] --> B[API receives user token] + B --> C[API starts long process] + C --> D{Token expires?} + D -->|Yes| E[❌ OBO fails] + D -->|No| F[βœ… OBO succeeds] + + style E fill:#f8d7da + style F fill:#d4edda +``` + +### Long-Running Process Pattern + +```csharp +[Authorize] +[ApiController] +[Route("api/[controller]")] +public class ProcessingController : ControllerBase +{ + private readonly IDownstreamApi _downstreamApi; + private readonly IBackgroundTaskQueue _taskQueue; + + public ProcessingController( + IDownstreamApi downstreamApi, + IBackgroundTaskQueue taskQueue) + { + _downstreamApi = downstreamApi; + _taskQueue = taskQueue; + } + + [HttpPost("start")] + public async Task> StartLongProcess([FromBody] ProcessRequest request) + { + var processId = Guid.NewGuid(); + + // Queue the long-running task + _taskQueue.QueueBackgroundWorkItem(async (cancellationToken) => + { + await ProcessDataAsync(processId, request, cancellationToken); + }); + + return Accepted(new ProcessStatus + { + ProcessId = processId, + Status = "Started" + }); + } + + private async Task ProcessDataAsync( + Guid processId, + ProcessRequest request, + CancellationToken cancellationToken) + { + try + { + // The cached refresh token allows token acquisition even if original token expired + var data = await _downstreamApi.GetForUserAsync( + "PartnerAPI", + options => { + options.RelativePath = "api/process/data"; + options.AcquireTokenOptions.LongRunningWebApiSessionKey = processId.ToString() + }, + cancellationToken: cancellationToken); + + // Process data... + await Task.Delay(TimeSpan.FromMinutes(5), cancellationToken); + + // Call API again (token may need refresh) + await _downstreamApi.PostForUserAsync( + "PartnerAPI", + options => { + options.RelativePath = "api/process/complete"; + options.AcquireTokenOptions.LongRunningWebApiSessionKey = processId.ToString() + }, + data, + cancellationToken: cancellationToken); + } + catch (Exception ex) + { + // Log error and update process status + } + } +} +``` + +### Important Considerations + +2. **Token Cache**: Must use distributed cache for background processes +3. **User Context**: `HttpContext.User` is available in the background worker +4. **Error Handling**: Token may still expire if user revokes consent + +## Error Handling Specific to APIs + +### MicrosoftIdentityWebChallengeUserException + +In web APIs, you can't redirect users to consent. Instead, return a proper error response: + +```csharp +[HttpGet("data")] +public async Task GetData() +{ + try + { + var data = await _downstreamApi.GetForUserAsync("PartnerAPI", "api/data"); + return Ok(data); + } + catch (MicrosoftIdentityWebChallengeUserException ex) + { + // Return 401 with consent information + return Unauthorized(new + { + error = "consent_required", + error_description = "Additional user consent required", + scopes = ex.Scopes, + claims = ex.Claims + }); + } +} +``` + +### Client Handling Consent Requirement + +The client app should handle the 401 response and trigger consent: + +```csharp +// Client app code +var response = await httpClient.GetAsync("https://yourapi.example.com/api/data"); + +if (response.StatusCode == HttpStatusCode.Unauthorized) +{ + var error = await response.Content.ReadFromJsonAsync(); + + if (error?.error == "consent_required") + { + // Trigger incremental consent in client app + // This will redirect user to Azure AD for consent + throw new MsalUiRequiredException(error.error_description, error.scopes); + } +} +``` + +### Downstream API Failures + +```csharp +[HttpGet("data")] +public async Task GetData() +{ + try + { + var data = await _downstreamApi.GetForUserAsync("PartnerAPI", "api/data"); + return Ok(data); + } + catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound) + { + return NotFound("Resource not found in downstream service"); + } + catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.BadRequest) + { + return BadRequest("Invalid request to downstream service"); + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "Downstream API returned {StatusCode}", ex.StatusCode); + return StatusCode(502, "Downstream service error"); + } +} +``` + +## OWIN Implementation (.NET Framework) + +### 1. Configure Startup.cs + +```csharp +using Microsoft.Identity.Web; +using Microsoft.Identity.Web.OWIN; +using Owin; + +public class Startup +{ + public void Configuration(IAppBuilder app) + { + OwinTokenAcquirerFactory factory = TokenAcquirerFactory.GetDefaultInstance(); + app.AddMicrosoftIdentityWebApi(factory); + factory.Services + .AddMicrosoftGraph() + .AddDownstreamApis(factory.Configuration.GetSection("DownstreamAPIs")); + factory.Build(); + } +} +``` + +### 2. Call API from Controllers + +```csharp +using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Web; +using System.Web.Http; + +[Authorize] +public class DataController : ApiController +{ + private readonly IDownstreamApi _downstreamApi; + + public DataController() + { + GraphServiceClient graphServiceClient = this.GetGraphServiceClient(); + var me = await graphServiceClient.Me.Request().GetAsync(); + + // OR - Example calling a downstream directly with the IDownstreamApi helper (uses the + // authorization header provider, encapsulates MSAL.NET) + // downstreamApi won't be null if you added services.AddMicrosoftGraph() + // in the Startup.auth.cs + IDownstreamApi downstreamApi = this.GetDownstreamApi(); + var result = await downstreamApi.CallApiForUserAsync("DownstreamAPI"); + + // OR - Get an authorization header (uses the token acquirer) + IAuthorizationHeaderProvider authorizationHeaderProvider = + this.GetAuthorizationHeaderProvider(); + } + + [HttpGet] + [Route("api/data")] + public async Task GetData() + { + var data = await _downstreamApi.GetForUserAsync( + "PartnerAPI", + options => options.RelativePath = "api/data", + options => options.Scopes = new[] { "api://partner/read" }); + + return Ok(data); + } +} +``` + +## Calling Multiple Downstream APIs + +Your API can call multiple downstream APIs in a single request: + +```csharp +[HttpGet("dashboard")] +public async Task> GetDashboard() +{ + try + { + // Call multiple APIs in parallel + var userTask = _downstreamApi.GetForUserAsync( + "GraphAPI", "me"); + + var dataTask = _downstreamApi.GetForUserAsync( + "PartnerAPI", "api/data"); + + var settingsTask = _downstreamApi.GetForUserAsync( + "PartnerAPI", "api/settings"); + + await Task.WhenAll(userTask, dataTask, settingsTask); + + return Ok(new Dashboard + { + User = userTask.Result, + Data = dataTask.Result, + Settings = settingsTask.Result + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to retrieve dashboard data"); + return StatusCode(500, "Failed to retrieve dashboard"); + } +} +``` + +## Best Practices + +### 1. Always Use Distributed Cache in Production + +```csharp +// ❌ Bad: In-memory cache in production +.AddInMemoryTokenCaches(); + +// βœ… Good: Distributed cache in production +.AddDistributedTokenCaches(); +``` + +### 3. Log + +```csharp +builder.Services.AddLogging(config => +{ + config.AddConsole(); + config.AddApplicationInsights(); + config.SetMinimumLevel(LogLevel.Information); +}); +``` + +### 4. Set Appropriate Timeouts + +```csharp +builder.Services.AddDownstreamApi("PartnerAPI", options => +{ + options.BaseUrl = "https://partnerapi.example.com"; + options.HttpClientName = "PartnerAPI"; +}); + +builder.Services.AddHttpClient("PartnerAPI", client => +{ + client.Timeout = TimeSpan.FromSeconds(30); +}); +``` + +### 5. Validate Incoming Tokens + +Ensure your API validates tokens properly: + +```csharp +builder.Services.AddMicrosoftIdentityWebApi(options => +{ + builder.Configuration.Bind("AzureAd", options); +}); +``` + +## Troubleshooting + +### Error: "AADSTS50013: Assertion failed signature validation" + +**Cause**: Client secret or certificate misconfigured in your API's app registration. + +**Solution**: Verify client credentials in appsettings.json match Azure AD app registration. + +### Error: "AADSTS65001: User or administrator has not consented" + +**Cause**: User hasn't consented to your API calling the downstream API. + +**Solution**: Return proper error to client app and trigger consent flow in client. + +### Error: "AADSTS500133: Assertion is not within its valid time range" + +**Cause**: Clock skew between servers or expired token. + +**Solution**: +- Sync server clocks +- Check token expiration +- Ensure token cache is working properly + +### OBO Token Not Cached + +**Cause**: Distributed cache not configured or cache key issues. + +**Solution**: +- Verify distributed cache connection +- Check that `oid` and `tid` claims exist in incoming token +- Enable debug logging to see cache operations + +### Multiple API Instances Not Sharing Cache + +**Cause**: Using in-memory cache instead of distributed cache. + +**Solution**: Switch to distributed cache (Redis, SQL Server, Cosmos DB). + +**For detailed diagnostics:** See [Logging & Diagnostics Guide](../advanced/logging.md) for correlation IDs, token cache debugging, PII logging configuration, and comprehensive troubleshooting workflows. + +## Related Documentation + +- [Long-Running Processes](#long-running-processes-with-obo) +- [Token Caching](../authentication/token-cache/token-cache-README.md) +- [Calling from Web Apps](from-web-apps.md) +- [Web API Scenarios](../getting-started/quickstart-webapi.md) +- [API Behind Gateways](../advanced/api-gateways.md) +- **[Logging & Diagnostics](../advanced/logging.md)** - Troubleshooting authentication and token issues +- **[Authorization Guide](../authentication/authorization.md)** - RequiredScope and app permission validation +- **[Customization Guide](../advanced/customization.md)** - Advanced token acquisition customization + +--- + +**Next Steps**: Learn about [calling Microsoft Graph](microsoft-graph.md) or [custom APIs](custom-apis.md) with specialized integration patterns. diff --git a/docs/calling-downstream-apis/from-web-apps.md b/docs/calling-downstream-apis/from-web-apps.md new file mode 100644 index 000000000..97a168dac --- /dev/null +++ b/docs/calling-downstream-apis/from-web-apps.md @@ -0,0 +1,1314 @@ +# Calling Downstream APIs from Web Apps + +This guide explains how to call downstream APIs from ASP.NET Core and OWIN web applications using Microsoft.Identity.Web. In web apps, you acquire tokens **on behalf of the signed-in user** to call APIs with delegated permissions. + +## Overview + +When a user signs into your web application, you can call downstream APIs (Microsoft Graph, Azure services, or custom APIs) on their behalf. Microsoft.Identity.Web handles token acquisition, caching, and automatic refresh. + +### User Token Flow + +```mermaid +sequenceDiagram + participant User as User Browser + participant WebApp as Your Web App + participant AzureAD as Microsoft Entra ID + participant API as Downstream API + + User->>WebApp: 1. Access page requiring API data + Note over WebApp: User already signed in + WebApp->>AzureAD: 2. Request access token for API
(using user's refresh token) + AzureAD->>AzureAD: 3. Validate & check consent + AzureAD->>WebApp: 4. Return access token + Note over WebApp: Cache token + WebApp->>API: 5. Call API with token + API->>WebApp: 6. Return data + WebApp->>User: 7. Render page with data +``` + +## Prerequisites + +- Web app configured with OpenID Connect authentication +- User sign-in working +- App registration with API permissions configured +- User consent obtained (or admin consent granted) + +## ASP.NET Core Implementation + +### 1. Configure Authentication and Token Acquisition + +```csharp +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")) + .EnableTokenAcquisitionToCallDownstreamApi() + .AddInMemoryTokenCaches(); + +builder.Services.AddRazorPages() + .AddMicrosoftIdentityUI(); + +builder.Services.AddAuthorization(options => +{ + options.FallbackPolicy = options.DefaultPolicy; +}); + +var app = builder.Build(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapRazorPages(); +app.Run(); +``` + +### 2. Configure appsettings.json + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + "CallbackPath": "/signin-oidc", + "SignedOutCallbackPath": "/signout-callback-oidc", + "ClientCredentials": [ + { + "SourceType": "ClientSecret", + "ClientSecret": "your-client-secret" + } + ] + }, + "DownstreamApis": { + "GraphAPI": { + "BaseUrl": "https://graph.microsoft.com/v1.0", + "Scopes": ["user.read", "mail.read"] + }, + "MyAPI": { + "BaseUrl": "https://myapi.example.com", + "Scopes": ["api://my-api-id/access_as_user"] + } + } +} +``` + +**Important:** For web apps calling downstream APIs, you need **client credentials** (certificate or secret) in addition to sign-in configuration. + +### 3. Add Downstream API Support + +**Option A: Register Named APIs** + +```csharp +using Microsoft.Identity.Web; + +// Register multiple downstream APIs +builder.Services.AddDownstreamApis( + builder.Configuration.GetSection("DownstreamApis")); +``` + +**Option B: Use Microsoft Graph Helper** + +```csharp +// Install: Microsoft.Identity.Web.GraphServiceClient +builder.Services.AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApis:GraphAPI")); +``` + +### 4. Call Downstream API from Controller + +```csharp +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Identity.Web; +using Microsoft.Identity.Abstractions; + +[Authorize] +public class ProfileController : Controller +{ + private readonly IDownstreamApi _downstreamApi; + private readonly ILogger _logger; + + public ProfileController( + IDownstreamApi downstreamApi, + ILogger logger) + { + _downstreamApi = downstreamApi; + _logger = logger; + } + + public async Task Index() + { + try + { + // Call downstream API on behalf of user + var userData = await _downstreamApi.GetForUserAsync( + "MyAPI", + options => options.RelativePath = "api/profile"); + + return View(userData); + } + catch (MicrosoftIdentityWebChallengeUserException ex) + { + // Incremental consent required + // Redirect user to consent page + return Challenge( + new AuthenticationProperties + { + RedirectUri = "/Profile" + }, + OpenIdConnectDefaults.AuthenticationScheme); + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "Failed to call downstream API"); + return View("Error"); + } + } +} +``` + +### 5. Call Downstream API from Razor Page + +```csharp +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Identity.Web; +using Microsoft.Identity.Abstractions; + +[Authorize] +public class ProfileModel : PageModel +{ + private readonly IDownstreamApi _downstreamApi; + + public UserData UserData { get; set; } + + public ProfileModel(IDownstreamApi downstreamApi) + { + _downstreamApi = downstreamApi; + } + + public async Task OnGetAsync() + { + try + { + UserData = await _downstreamApi.GetForUserAsync( + "MyAPI", + options => options.RelativePath = "api/profile"); + } + catch (MicrosoftIdentityWebChallengeUserException) + { + // Handle incremental consent + // User will be redirected to consent page + throw; + } + } +} +``` + +--- + +## Using Microsoft Graph + +For Microsoft Graph API calls, use the dedicated `GraphServiceClient`: + +### Setup + +```bash +dotnet add package Microsoft.Identity.Web.GraphServiceClient +``` + +```csharp +// Startup configuration +builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd")) + .EnableTokenAcquisitionToCallDownstreamApi() + .AddMicrosoftGraph(options => + { + options.Scopes = "user.read mail.read"; + }) + .AddInMemoryTokenCaches(); +``` + +### Usage + +```csharp +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Graph; + +[Authorize] +public class HomeController : Controller +{ + private readonly GraphServiceClient _graphClient; + + public HomeController(GraphServiceClient graphClient) + { + _graphClient = graphClient; + } + + public async Task Index() + { + // Get current user's profile + var user = await _graphClient.Me.GetAsync(); + + // Get user's emails + var messages = await _graphClient.Me.Messages + .GetAsync(config => config.QueryParameters.Top = 10); + + return View(new { User = user, Messages = messages }); + } +} +``` + +[πŸ“– Learn more about Microsoft Graph integration](microsoft-graph.md) + +--- + +## Using Azure SDK Clients + +For calling Azure services, use `MicrosoftIdentityTokenCredential`: + +### Setup + +```bash +dotnet add package Microsoft.Identity.Web.Azure +dotnet add package Azure.Storage.Blobs +``` + +```csharp +using Microsoft.Identity.Web; + +builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd")) + .EnableTokenAcquisitionToCallDownstreamApi() + .AddInMemoryTokenCaches(); + +// Add Azure token credential +builder.Services.AddMicrosoftIdentityAzureTokenCredential(); +``` + +### Usage + +```csharp +using Azure.Storage.Blobs; +using Microsoft.Identity.Web; + +public class StorageController : Controller +{ + private readonly MicrosoftIdentityTokenCredential _credential; + + public StorageController(MicrosoftIdentityTokenCredential credential) + { + _credential = credential; + } + + [Authorize] + public async Task ListBlobs() + { + 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 View(blobs); + } +} +``` + +[πŸ“– Learn more about Azure SDK integration](azure-sdks.md) + +--- + +## Using Custom APIs with IDownstreamApi + +For your own REST APIs, `IDownstreamApi` provides a simple, configuration-driven approach: + +### Configuration + +```json +{ + "DownstreamApis": { + "MyAPI": { + "BaseUrl": "https://myapi.example.com", + "Scopes": ["api://my-api-id/access_as_user"], + "RequestAppToken": false + } + } +} +``` + +### Usage - GET Request + +```csharp +// Simple GET +var data = await _downstreamApi.GetForUserAsync( + "MyAPI", + options => options.RelativePath = "api/resource"); + +// GET with query parameters +var results = await _downstreamApi.GetForUserAsync( + "MyAPI", + options => + { + options.RelativePath = "api/search"; + options.QueryParameters = new Dictionary + { + ["query"] = "test", + ["limit"] = "10" + }; + }); +``` + +### Usage - POST Request + +```csharp +var newItem = new CreateItemRequest +{ + Name = "New Item", + Description = "Item description" +}; + +var created = await _downstreamApi.PostForUserAsync( + "MyAPI", + newItem, + options => options.RelativePath = "api/items"); +``` + +### Usage - PUT and DELETE + +```csharp +// PUT request +var updated = await _downstreamApi.PutForUserAsync( + "MyAPI", + updateData, + options => options.RelativePath = "api/items/123"); + +// DELETE request +await _downstreamApi.DeleteForUserAsync( + "MyAPI", + null, + options => options.RelativePath = "api/items/123"); +``` + +[πŸ“– Learn more about custom API calls](custom-apis.md) + +--- + +## Using IAuthorizationHeaderProvider (Advanced) + +For maximum control over HTTP requests, use `IAuthorizationHeaderProvider`: + +### Setup + +```csharp +builder.Services.AddHttpClient("MyAPI", client => +{ + client.BaseAddress = new Uri("https://myapi.example.com"); +}); +``` + +### Usage + +```csharp +using Microsoft.Identity.Abstractions; + +public class CustomApiService +{ + private readonly IAuthorizationHeaderProvider _authProvider; + private readonly IHttpClientFactory _httpClientFactory; + + public CustomApiService( + IAuthorizationHeaderProvider authProvider, + IHttpClientFactory httpClientFactory) + { + _authProvider = authProvider; + _httpClientFactory = httpClientFactory; + } + + public async Task GetDataAsync() + { + // Get authorization header + var authHeader = await _authProvider.CreateAuthorizationHeaderForUserAsync( + new[] { "api://my-api-id/access_as_user" }); + + // Create HTTP request with custom logic + var client = _httpClientFactory.CreateClient("MyAPI"); + var request = new HttpRequestMessage(HttpMethod.Get, "api/resource"); + request.Headers.Add("Authorization", authHeader); + request.Headers.Add("X-Custom-Header", "custom-value"); + + var response = await client.SendAsync(request); + response.EnsureSuccessStatusCode(); + + return await response.Content.ReadFromJsonAsync(); + } +} +``` + +[πŸ“– Learn more about custom HTTP logic](custom-apis.md#iauthorizationheaderprovider---maximum-control) + +--- + +## Incremental Consent & Conditional Access + +When calling downstream APIs, your application may need to handle scenarios where user interaction is required. This happens in three main scenarios: + +1. **Incremental Consent** - Requesting additional permissions beyond what was initially granted +2. **Conditional Access** - Meeting security requirements like MFA, device compliance, or location policies +3. **Token Cache Eviction** - Repopulating the token cache after application restart or cache expiration + +Microsoft.Identity.Web provides automatic handling of these scenarios with minimal code required. + +### Understanding the Flow + +When Microsoft.Identity.Web detects that user interaction is needed, it throws a `MicrosoftIdentityWebChallengeUserException`. The framework automatically handles this through the `[AuthorizeForScopes]` attribute or the `MicrosoftIdentityConsentAndConditionalAccessHandler` service (for Blazor), which: + +1. Redirects the user to Microsoft Entra ID for consent/authentication +2. Preserves the original request URL +3. Returns the user to their intended destination after completing the flow +4. Caches the newly acquired tokens + +### Prerequisites + +To enable automatic consent handling, ensure your `Program.cs` includes: + +```csharp +builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration, "AzureAd") + .EnableTokenAcquisitionToCallDownstreamApi() + .AddDownstreamApi("MyAPI", builder.Configuration.GetSection("MyAPI")) + .AddInMemoryTokenCaches(); + +// For MVC applications - enables the account controller +builder.Services.AddControllersWithViews() + .AddMicrosoftIdentityUI(); + +// Ensure routes are mapped +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapControllers(); // Required for AccountController +``` + +--- + +### MVC Controllers - Using [AuthorizeForScopes] + +The `[AuthorizeForScopes]` attribute, set on controllers or controller actions, automatically handles `MicrosoftIdentityWebChallengeUserException` by challenging the user when additional permissions are needed. + +#### Declarative Scopes + +```csharp +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Identity.Web; +using Microsoft.Identity.Abstractions; + +[Authorize] +[AuthorizeForScopes(Scopes = new[] { "user.read" })] +public class ProfileController : Controller +{ + private readonly IDownstreamApi _downstreamApi; + + public ProfileController(IDownstreamApi downstreamApi) + { + _downstreamApi = downstreamApi; + } + + public async Task Index() + { + // AuthorizeForScopes automatically handles consent challenges + var userData = await _downstreamApi.GetForUserAsync( + "MyAPI", + options => options.RelativePath = "api/profile"); + + return View(userData); + } + + // Different action requires additional scopes + [AuthorizeForScopes(Scopes = new[] { "user.read", "mail.read" })] + public async Task Emails() + { + var emails = await _downstreamApi.GetForUserAsync( + "GraphAPI", + options => options.RelativePath = "me/messages"); + + return View(emails); + } +} +``` + +#### Configuration-Based Scopes + +Store scopes in `appsettings.json` for better maintainability: + +**appsettings.json:** +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "common", + "ClientId": "[Your-Client-ID]", + "ClientCredentials": [ + { + "SourceType": "ClientSecret", + "ClientSecret": "[Your-Client-Secret]" + } + ] + }, + "DownstreamApis": { + "TodoList": { + "BaseUrl": "https://localhost:5001", + "Scopes": [ "api://[API-Client-ID]/access_as_user" ] + }, + "GraphAPI": { + "BaseUrl": "https://graph.microsoft.com/v1.0", + "Scopes": [ "https://graph.microsoft.com/Mail.Read", "https://graph.microsoft.com/Mail.Send" ] + } + } +} +``` + +**Controller:** +```csharp +[Authorize] +[AuthorizeForScopes(ScopeKeySection = "DownstreamApis:TodoList:Scopes:0")] +public class TodoListController : Controller +{ + private readonly IDownstreamApi _downstreamApi; + + public TodoListController(IDownstreamApi downstreamApi) + { + _downstreamApi = downstreamApi; + } + + public async Task Index() + { + var todos = await _downstreamApi.GetForUserAsync>( + "TodoList", + options => options.RelativePath = "api/todolist"); + + return View(todos); + } + + [AuthorizeForScopes(ScopeKeySection = "DownstreamApis:GraphAPI:Scopes:0")] + public async Task EmailTodos() + { + // If user hasn't consented to Mail.Send, they'll be prompted + await _downstreamApi.PostForUserAsync( + "GraphAPI", + new EmailMessage { /* ... */ }, + options => options.RelativePath = "me/sendMail"); + + return RedirectToAction("Index"); + } +} +``` + +#### Azure AD B2C with User Flows + +For B2C applications with multiple user flows: + +```csharp +[Authorize] +public class AccountController : Controller +{ + private const string SignUpSignInFlow = "b2c_1_susi"; + private const string EditProfileFlow = "b2c_1_edit_profile"; + private const string ResetPasswordFlow = "b2c_1_reset"; + + [AuthorizeForScopes( + ScopeKeySection = "DownstreamApis:TodoList:Scopes:0", + UserFlow = SignUpSignInFlow)] + public async Task Index() + { + var data = await _downstreamApi.GetForUserAsync( + "TodoList", + options => options.RelativePath = "api/data"); + + return View(data); + } + + [AuthorizeForScopes( + Scopes = new[] { "openid", "offline_access" }, + UserFlow = EditProfileFlow)] + public async Task EditProfile() + { + // This triggers the B2C edit profile flow + return RedirectToAction("Index"); + } +} +``` + +--- + +### Razor Pages - Using [AuthorizeForScopes] + +Apply `[AuthorizeForScopes]` to the page model class: + +```csharp +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Identity.Web; +using Microsoft.Identity.Abstractions; + +[Authorize] +[AuthorizeForScopes(ScopeKeySection = "DownstreamApis:MyAPI:Scopes:0")] +public class IndexModel : PageModel +{ + private readonly IDownstreamApi _downstreamApi; + + public UserData UserData { get; set; } + + public IndexModel(IDownstreamApi downstreamApi) + { + _downstreamApi = downstreamApi; + } + + public async Task OnGetAsync() + { + // Automatically handles consent challenges + UserData = await _downstreamApi.GetForUserAsync( + "MyAPI", + options => options.RelativePath = "api/profile"); + } +} +``` + +--- + +### Blazor Server - Using MicrosoftIdentityConsentAndConditionalAccessHandler + +Blazor Server applications require explicit exception handling using the `MicrosoftIdentityConsentAndConditionalAccessHandler` service. + +#### Program.cs Configuration + +```csharp +builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration, "AzureAd") + .EnableTokenAcquisitionToCallDownstreamApi() + .AddDownstreamApis("TodoList", builder.Configuration.GetSection("DownstreamApis")) + .AddInMemoryTokenCaches(); + +// Register the consent handler for Blazor +builder.Services.AddServerSideBlazor() + .AddMicrosoftIdentityConsentHandler(); +``` + +#### Blazor Component + +```c# +@page "/todolist" +@using Microsoft.Identity.Web +@using Microsoft.Identity.Abstractions +@using MyApp.Models + +@inject MicrosoftIdentityConsentAndConditionalAccessHandler ConsentHandler +@inject IDownstreamApi DownstreamApi + +

My Todo List

+ +@if (todos == null) +{ +

Loading...

+} +else +{ +
    + @foreach (var todo in todos) + { +
  • @todo.Title
  • + } +
+} + +@code { + private IEnumerable todos; + + protected override async Task OnInitializedAsync() + { + await LoadTodosAsync(); + } + + [AuthorizeForScopes(ScopeKeySection = "DownstreamApis:TodoList:Scopes:0")] + private async Task LoadTodosAsync() + { + try + { + todos = await DownstreamApi.GetForUserAsync>( + "TodoList", + options => options.RelativePath = "api/todolist"); + } + catch (Exception ex) + { + // Handles MicrosoftIdentityWebChallengeUserException + // and initiates user consent/authentication flow + ConsentHandler.HandleException(ex); + } + } + + private async Task AddTodoAsync(string title) + { + try + { + await DownstreamApi.PostForUserAsync( + "TodoList", + new TodoItem { Title = title }, + options => options.RelativePath = "api/todolist"); + + await LoadTodosAsync(); + } + catch (Exception ex) + { + ConsentHandler.HandleException(ex); + } + } +} +``` + +--- + +### Manual Exception Handling (Advanced) + +If you need custom consent flow logic, handle `MicrosoftIdentityWebChallengeUserException` explicitly: + +```csharp +[Authorize] +public class AdvancedController : Controller +{ + private readonly IDownstreamApi _downstreamApi; + private readonly ILogger _logger; + + public AdvancedController( + IDownstreamApi downstreamApi, + ILogger logger) + { + _downstreamApi = downstreamApi; + _logger = logger; + } + + public async Task SendEmail() + { + try + { + await _downstreamApi.PostForUserAsync( + "GraphAPI", + new EmailMessage + { + Subject = "Test", + Body = "Test message" + }, + options => options.RelativePath = "me/sendMail"); + + return RedirectToAction("Success"); + } + catch (MicrosoftIdentityWebChallengeUserException ex) + { + // Log the consent requirement + _logger.LogWarning( + "Consent required for scopes: {Scopes}. Challenging user.", + string.Join(", ", ex.Scopes)); + + // Custom properties for redirect + var properties = new AuthenticationProperties + { + RedirectUri = Url.Action("SendEmail", "Advanced"), + }; + + // Add custom state if needed + properties.Items["consent_attempt"] = "1"; + + return Challenge(properties, OpenIdConnectDefaults.AuthenticationScheme); + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "Failed to send email"); + return View("Error"); + } + } +} +``` + +--- + +### Conditional Access Scenarios + +Conditional access policies can require additional authentication factors. The handling is identical to incremental consent: + +```csharp +[Authorize] +[AuthorizeForScopes(ScopeKeySection = "DownstreamApis:SecureAPI:Scopes:0")] +public class SecureDataController : Controller +{ + private readonly IDownstreamApi _downstreamApi; + + public SecureDataController(IDownstreamApi downstreamApi) + { + _downstreamApi = downstreamApi; + } + + public async Task Index() + { + // If conditional access requires MFA, AuthorizeForScopes + // automatically challenges the user + var sensitiveData = await _downstreamApi.GetForUserAsync( + "SecureAPI", + options => options.RelativePath = "api/sensitive"); + + return View(sensitiveData); + } +} +``` + { + // If conditional access requires MFA, AuthorizeForScopes + // automatically challenges the user + var sensitiveData = await _downstreamApi.GetForUserAsync( + "SecureAPI", + options => options.RelativePath = "api/sensitive"); + + return View(sensitiveData); + } +} +``` + +**Common conditional access triggers:** +- Multi-factor authentication (MFA) +- Compliant device requirement +- Trusted network location +- Terms of use acceptance +- Password change requirement + +--- + +### Best Practices + +βœ… **Use `[AuthorizeForScopes]`** - Simplest approach for MVC controllers and Razor Pages + +βœ… **Store scopes in configuration** - Use `ScopeKeySection = "DownstreamApis:ApiName:Scopes:0"` to reference the scopes in `appsettings.json` + +βœ… **Apply at controller level** - Set default scopes on the controller, override on specific actions + +βœ… **Handle exceptions in Blazor** - Always wrap API calls with try-catch and use `ConsentHandler.HandleException()` + +βœ… **Let re-throw exceptions** - If you catch `MicrosoftIdentityWebChallengeUserException`, re-throw it so `[AuthorizeForScopes]` can process it + +βœ… **Test conditional access** - Verify your app handles MFA and other CA policies correctly + +❌ **Don't suppress exceptions** - Catching without re-throwing breaks the consent flow + +❌ **Don't cache responses indefinitely** - Tokens expire; design for re-authentication + +--- + +### Static Permissions vs. Incremental Consent + +**Static Permissions (Admin Consent)** + +All permissions are requested during app registration and consented by a tenant administrator: + +**Pros:** +- Users never see consent prompts +- Required for first-party Microsoft apps +- Simpler user experience + +**Cons:** +- Requires tenant admin involvement +- Over-privileged from the start +- Less flexible for multi-tenant scenarios + +**Configuration:** +```csharp +// Request all pre-approved scopes for Microsoft Graph +var scopes = new[] { "https://graph.microsoft.com/.default" }; + +var userData = await _downstreamApi.GetForUserAsync( + "GraphAPI", + options => + { + options.RelativePath = "me"; + options.Scopes = scopes; // Use .default scope + }); +``` + +**Incremental Consent (Dynamic)** + +Permissions are requested as needed during runtime: + +**Pros:** +- Better security (principle of least privilege) +- Users consent to what they actually use +- Works for multi-tenant apps + +**Cons:** +- Users may be interrupted with consent prompts +- Requires handling `MicrosoftIdentityWebChallengeUserException` + +**Recommendation:** Use incremental consent for multi-tenant applications; use static permissions for first-party enterprise apps where admin consent is guaranteed + +--- + +## Token Caching + +Microsoft.Identity.Web caches tokens to improve performance and reduce calls to Microsoft Entra ID. + +### In-Memory Cache (Default) + +```csharp +builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd")) + .EnableTokenAcquisitionToCallDownstreamApi() + .AddInMemoryTokenCaches(); // In-memory cache +``` + +**Use for:** +- βœ… Development +- βœ… Single-server deployments +- βœ… Small user base + +**Limitations:** +- ❌ Not shared across instances +- ❌ Lost on app restart +- ❌ Memory consumption grows with users + +### Distributed Cache (Recommended for Production) + +```csharp +// Install: Microsoft.Identity.Web.TokenCache + +// Redis +builder.Services.AddStackExchangeRedisCache(options => +{ + options.Configuration = builder.Configuration["Redis:ConnectionString"]; + options.InstanceName = "MyApp_"; +}); + +builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd")) + .EnableTokenAcquisitionToCallDownstreamApi() + .AddDistributedTokenCaches(); + +// SQL Server +builder.Services.AddDistributedSqlServerCache(options => +{ + options.ConnectionString = builder.Configuration["SqlCache:ConnectionString"]; + options.SchemaName = "dbo"; + options.TableName = "TokenCache"; +}); + +builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd")) + .EnableTokenAcquisitionToCallDownstreamApi() + .AddDistributedTokenCaches(); +``` + +**Use for:** +- βœ… Multi-server deployments (load balanced) +- βœ… High-availability scenarios +- βœ… Large user base +- βœ… Persistent cache across restarts + +--- + +## Handling Token Acquisition Failures + +### Common Exceptions + +```csharp +try +{ + var data = await _downstreamApi.GetForUserAsync( + "MyAPI", + options => options.RelativePath = "api/resource"); +} +catch (MicrosoftIdentityWebChallengeUserException ex) +{ + // User needs to consent or reauthenticate + _logger.LogWarning($"User consent required: {ex.Message}"); + return Challenge(new AuthenticationProperties { RedirectUri = Request.Path }); +} +catch (MsalUiRequiredException ex) +{ + // User interaction required (sign-in again, MFA, etc.) + _logger.LogWarning($"User interaction required: {ex.Message}"); + return Challenge(OpenIdConnectDefaults.AuthenticationScheme); +} +catch (MsalServiceException ex) +{ + // Service error (Azure AD unavailable, etc.) + _logger.LogError(ex, "Microsoft Entra ID service error"); + return StatusCode(503, "Authentication service temporarily unavailable"); +} +catch (HttpRequestException ex) +{ + // Downstream API unreachable + _logger.LogError(ex, "Downstream API call failed"); + return StatusCode(503, "Downstream service unavailable"); +} +``` + +### Graceful Degradation + +```csharp +public async Task Dashboard() +{ + var model = new DashboardModel(); + + // Try to load optional data from downstream API + try + { + model.EnrichedData = await _downstreamApi.GetForUserAsync( + "MyAPI", + options => options.RelativePath = "api/enriched"); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to load enriched data, using defaults"); + model.EnrichedData = new EnrichedData { /* defaults */ }; + } + + return View(model); +} +``` + +--- + +## OWIN (.NET Framework) Implementation + +For OWIN-based web applications on .NET Framework: + +### 1. Install Packages + +```powershell +Install-Package Microsoft.Identity.Web.OWIN +Install-Package Microsoft.Owin.Host.SystemWeb +``` + +### 2. Configure Startup + +```csharp +using Microsoft.Identity.Web; +using Microsoft.Owin.Security; +using Microsoft.Owin.Security.Cookies; +using Owin; + +public class Startup +{ + public void Configuration(IAppBuilder app) + { + app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType); + + app.UseCookieAuthentication(new CookieAuthenticationOptions()); + + app.AddMicrosoftIdentityWebApp( + Configuration, + configSectionName: "AzureAd", + openIdConnectScheme: "OpenIdConnect", + cookieScheme: CookieAuthenticationDefaults.AuthenticationType, + subscribeToOpenIdConnectMiddlewareDiagnosticsEvents: true); + + app.EnableTokenAcquisitionToCallDownstreamApi(); + app.AddDistributedTokenCaches(); + } +} +``` + +### 3. Call Downstream API + +```csharp +using Microsoft.Identity.Web; +using System.Threading.Tasks; +using System.Web.Mvc; + +[Authorize] +public class ProfileController : Controller +{ + public async Task Index() + { + var downstreamApi = TokenAcquirerFactory.GetDefaultInstance() + .GetTokenAcquirer() + .GetDownstreamApi(); + + var userData = await downstreamApi.GetForUserAsync( + "MyAPI", + options => options.RelativePath = "api/profile"); + + return View(userData); + } +} +``` + +**Note:** OWIN support has some differences from ASP.NET Core. See [OWIN documentation](../frameworks/owin.md) for details. + +--- + +## Security Best Practices + +### Scope Management + +**Do:** +- βœ… Request only scopes you need +- βœ… Use incremental consent for advanced features +- βœ… Document required scopes in your app + +**Don't:** +- ❌ Request unnecessary scopes upfront +- ❌ Request admin-only scopes without justification +- ❌ Assume all scopes will be granted + +### Token Handling + +**Do:** +- βœ… Let Microsoft.Identity.Web manage tokens +- βœ… Use distributed cache in production +- βœ… Handle token acquisition failures gracefully + +**Don't:** +- ❌ Store tokens yourself +- ❌ Log access tokens +- ❌ Send tokens to client-side code + +### Error Handling + +**Do:** +- βœ… Catch and handle consent exceptions +- βœ… Provide clear error messages to users +- βœ… Log errors for debugging + +**Don't:** +- ❌ Expose token errors to users +- ❌ Silently fail API calls +- ❌ Ignore authentication exceptions + +--- + +## Troubleshooting + +### Problem: "AADSTS65001: The user or administrator has not consented" + +**Cause:** User hasn't consented to required scopes. + +**Solution:** +```csharp +catch (MicrosoftIdentityWebChallengeUserException ex) +{ + // Redirect to consent page + return Challenge( + new AuthenticationProperties { RedirectUri = Request.Path }, + OpenIdConnectDefaults.AuthenticationScheme); +} +``` + +### Problem: "AADSTS50076: Multi-factor authentication required" + +**Cause:** User needs to complete MFA. + +**Solution:** +```csharp +catch (MsalUiRequiredException) +{ + // Redirect user to sign in with MFA + return Challenge(OpenIdConnectDefaults.AuthenticationScheme); +} +``` + +### Problem: Tokens not persisting across app restarts + +**Cause:** Using in-memory cache. + +**Solution:** Switch to distributed cache (Redis, SQL Server, or Cosmos DB). + +### Problem: 401 Unauthorized from downstream API + +**Possible causes:** +- Wrong scopes requested +- API permission not granted in app registration +- Token expired + +**Solution:** +1. Verify scopes in appsettings.json match API requirements +2. Check app registration has API permissions +3. Ensure tokens are being cached and refreshed + +**For detailed diagnostics:** See [Logging & Diagnostics Guide](../advanced/logging.md) for correlation IDs, token cache debugging, and comprehensive troubleshooting patterns. + +--- + +## Performance Considerations + +### Token Caching Strategy + +- βœ… Use distributed cache for multi-server deployments +- βœ… Configure appropriate cache expiration +- βœ… Monitor cache performance + +### Minimize Token Requests + +```csharp +// Bad: Multiple token acquisitions +var profile = await _downstreamApi.GetForUserAsync( + "API", + options => options.RelativePath = "profile"); +var settings = await _downstreamApi.GetForUserAsync( + "API", + options => options.RelativePath = "settings"); + +// Good: Single token, multiple calls (token is cached) +// Both calls use the same cached token +var profile = await _downstreamApi.GetForUserAsync( + "API", + options => options.RelativePath = "profile"); +var settings = await _downstreamApi.GetForUserAsync( + "API", + options => options.RelativePath = "settings"); +``` + +### Parallel API Calls + +```csharp +// Call multiple APIs in parallel +var profileTask = _downstreamApi.GetForUserAsync( + "API1", + options => options.RelativePath = "profile"); +var settingsTask = _downstreamApi.GetForUserAsync( + "API2", + options => options.RelativePath = "settings"); + +await Task.WhenAll(profileTask, settingsTask); + +var profile = profileTask.Result; +var settings = settingsTask.Result; +``` + +--- + +## Additional Resources + +- **[Back to Downstream APIs Overview](./calling-downstream-apis-README.md)** - Compare all approaches +- **[Token Cache Configuration](../authentication/token-cache/token-cache-README.md)** - Production cache strategies +- **[Microsoft Graph Integration](./microsoft-graph.md)** - Graph-specific guidance +- **[Azure SDK Integration](./azure-sdks.md)** - Azure service calls +- **[Custom API Calls](./custom-apis.md)** - Custom REST APIs +- **[Calling from Web APIs (OBO)](./from-web-apis.md)** - On-Behalf-Of flow + +--- + +## Next Steps + +1. **Choose your API type** and implementation approach +2. **Configure authentication** with token acquisition enabled +3. **Add downstream API** configuration +4. **Implement error handling** for consent and failures +5. **Test incremental consent** scenarios +6. **[Configure distributed cache](../authentication/token-cache/token-cache-README.md)** for production + +--- + +**Need help?** [Open an issue](https://github.com/AzureAD/microsoft-identity-web/issues) or check the [token cache troubleshooting guide](../authentication/token-cache/troubleshooting.md). diff --git a/docs/calling-downstream-apis/microsoft-graph.md b/docs/calling-downstream-apis/microsoft-graph.md new file mode 100644 index 000000000..c8abbd230 --- /dev/null +++ b/docs/calling-downstream-apis/microsoft-graph.md @@ -0,0 +1,875 @@ +# Calling Microsoft Graph + +This guide explains how to call Microsoft Graph from your ASP.NET Core and OWIN applications using Microsoft.Identity.Web and the Microsoft Graph SDK. + +## Overview + +Microsoft Graph provides a unified API endpoint for accessing data across Microsoft 365, Windows, and Enterprise Mobility + Security. Microsoft.Identity.Web simplifies authentication and token acquisition for Graph, while the Microsoft Graph SDK provides a fluent, typed API for calling Graph endpoints. + +### Why Use Microsoft.Identity.Web.GraphServiceClient? + +- **Automatic token acquisition**: Handles user and app tokens seamlessly +- **Token caching**: Built-in caching for performance +- **Fluent API**: Type-safe, IntelliSense-friendly Graph calls +- **Incremental consent**: Request additional scopes on demand +- **Multiple authentication schemes**: Support for web apps and web APIs +- **Both v1.0 and Beta**: Use stable and preview endpoints together + +## Installation + +Install the Microsoft Graph SDK integration package: + +```bash +dotnet add package Microsoft.Identity.Web.GraphServiceClient +``` + +For Microsoft Graph Beta APIs: + +```bash +dotnet add package Microsoft.Identity.Web.GraphServiceClientBeta +``` + +## ASP.NET Core Setup + +### 1. Configure Services + +Add Microsoft Graph support to your application: + +```csharp +using Microsoft.Identity.Web; + +var builder = WebApplication.CreateBuilder(args); + +// Add authentication (web app or web API) +builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd")) + .EnableTokenAcquisitionToCallDownstreamApi() + .AddInMemoryTokenCaches(); + +// Add Microsoft Graph support +builder.Services.AddMicrosoftGraph(); + +builder.Services.AddControllersWithViews(); + +var app = builder.Build(); +app.UseAuthentication(); +app.UseAuthorization(); +app.MapControllers(); +app.Run(); +``` + +### 2. Configure appsettings.json + +Configure Graph options in your configuration file: + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + "ClientSecret": "your-client-secret", + "CallbackPath": "/signin-oidc" + }, + "DownstreamApis": { + "MicrosoftGraph": { + "BaseUrl": "https://graph.microsoft.com/v1.0", + "Scopes": ["User.Read", "User.ReadBasic.All"] + } + } +} +``` + +**Configuration with Code:** + +```csharp +builder.Services.AddMicrosoftGraph(options => +{ + builder.Configuration.GetSection("DownstreamApis:MicrosoftGraph").Bind(options); +}); +``` + +Or configure directly in code: + +```csharp +builder.Services.AddMicrosoftGraph(); +builder.Services.Configure(options => +{ + options.BaseUrl = "https://graph.microsoft.com/v1.0"; + options.Scopes = new[] { "User.Read", "Mail.Read" }; +}); +``` + +### 3. National Cloud Support + +For Microsoft Graph in national clouds, specify the BaseUrl: + +```json +{ + "DownstreamApis": { + "MicrosoftGraph": { + "BaseUrl": "https://graph.microsoft.us/v1.0", + "Scopes": ["User.Read"] + } + } +} +``` + +See [Microsoft Graph deployments](https://learn.microsoft.com/graph/deployments#microsoft-graph-and-graph-explorer-service-root-endpoints) for endpoint URLs. + +## Using GraphServiceClient + +### Inject GraphServiceClient + +Inject `GraphServiceClient` from the constructor: + +```csharp +using Microsoft.Graph; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +[Authorize] +public class ProfileController : Controller +{ + private readonly GraphServiceClient _graphClient; + + public ProfileController(GraphServiceClient graphClient) + { + _graphClient = graphClient; + } + + public async Task Index() + { + // Call Microsoft Graph + var user = await _graphClient.Me.GetAsync(); + return View(user); + } +} +``` + +## Delegated Permissions (User Tokens) + +Call Graph on behalf of the signed-in user using delegated permissions. + +### Basic User Profile + +```csharp +[Authorize] +public class ProfileController : Controller +{ + private readonly GraphServiceClient _graphClient; + + public ProfileController(GraphServiceClient graphClient) + { + _graphClient = graphClient; + } + + public async Task Me() + { + // Get current user's profile + var user = await _graphClient.Me.GetAsync(); + + return View(new UserViewModel + { + DisplayName = user.DisplayName, + Mail = user.Mail, + JobTitle = user.JobTitle + }); + } +} +``` + +### Incremental Consent + +Request additional scopes dynamically when needed: + +```csharp +[Authorize] +[AuthorizeForScopes("Mail.Read")] +public class MailController : Controller +{ + private readonly GraphServiceClient _graphClient; + + public MailController(GraphServiceClient graphClient) + { + _graphClient = graphClient; + } + + public async Task Inbox() + { + try + { + // Request Mail.Read scope dynamically + var messages = await _graphClient.Me.Messages + .GetAsync(r => r.Options.WithScopes("Mail.Read")); + + return View(messages); + } + catch (MicrosoftIdentityWebChallengeUserException) + { + // ASP.NET Core will redirect user to consent + // thansk to the AuthorizeForScopes attribute. + throw; + } + } +} +``` + +### Query Options + +Use Graph SDK query options for filtering, selecting, and ordering: + +```csharp +public async Task UnreadMessages() +{ + var messages = await _graphClient.Me.Messages + .GetAsync(requestConfiguration => + { + requestConfiguration.QueryParameters.Filter = "isRead eq false"; + requestConfiguration.QueryParameters.Select = new[] { "subject", "from", "receivedDateTime" }; + requestConfiguration.QueryParameters.Orderby = new[] { "receivedDateTime desc" }; + requestConfiguration.QueryParameters.Top = 10; + + // Request specific scope + requestConfiguration.Options.WithScopes("Mail.Read"); + }); + + return View(messages); +} +``` + +### Paging Through Results + +Handle paged results from Microsoft Graph: + +```csharp +public async Task AllUsers() +{ + var allUsers = new List(); + + // Get first page + var users = await _graphClient.Users + .GetAsync(r => r.Options.WithScopes("User.ReadBasic.All")); + + // Add first page + allUsers.AddRange(users.Value); + + // Iterate through remaining pages + var pageIterator = PageIterator + .CreatePageIterator( + _graphClient, + users, + user => + { + allUsers.Add(user); + return true; // Continue iteration + }); + + await pageIterator.IterateAsync(); + + return View(allUsers); +} +``` + +## Application Permissions (App-Only Tokens) + +Call Graph with application permissions (no user context). + +### Using WithAppOnly() + +```csharp +[Authorize] +[ApiController] +[Route("api/[controller]")] +public class AdminController : ControllerBase +{ + private readonly GraphServiceClient _graphClient; + + public AdminController(GraphServiceClient graphClient) + { + _graphClient = graphClient; + } + + [HttpGet("users/count")] + public async Task> GetUserCount() + { + // Get count using app permissions + var count = await _graphClient.Users.Count + .GetAsync(r => r.Options.WithAppOnly()); + + return Ok(count); + } + + [HttpGet("applications")] + public async Task GetApplications() + { + // List applications using app permissions + var apps = await _graphClient.Applications + .GetAsync(r => r.Options.WithAppOnly()); + + return Ok(apps.Value); + } +} +``` + +### App Permissions Configuration + +In appsettings.json, you can specify to request an app token: + +```json +{ + "DownstreamApis": { + "MicrosoftGraph": { + "BaseUrl": "https://graph.microsoft.com/v1.0", + "RequestAppToken": true + } + } +} +``` + +The scopes will automatically be set to `["https://graph.microsoft.com/.default"]`. + +### Detailed App-Only Configuration + +```csharp +public async Task GetApplicationsDetailed() +{ + var apps = await _graphClient.Applications + .GetAsync(r => + { + r.Options.WithAuthenticationOptions(options => + { + // Request app token explicitly + options.RequestAppToken = true; + + // Scopes automatically become [.default] + // No need to specify: options.Scopes = new[] { "https://graph.microsoft.com/.default" }; + }); + }); + + return Ok(apps); +} +``` + +## Multiple Authentication Schemes + +If your app uses multiple authentication schemes (e.g., web app + API), specify which scheme to use: + +```csharp +using Microsoft.AspNetCore.Authentication.JwtBearer; + +[Authorize] +public class ApiDataController : ControllerBase +{ + private readonly GraphServiceClient _graphClient; + + public ApiDataController(GraphServiceClient graphClient) + { + _graphClient = graphClient; + } + + [HttpGet("profile")] + public async Task GetProfile() + { + // Specify JWT Bearer scheme + var user = await _graphClient.Me + .GetAsync(r => r.Options + .WithAuthenticationScheme(JwtBearerDefaults.AuthenticationScheme)); + + return Ok(user); + } +} +``` + +### Detailed Scheme Configuration + +```csharp +public async Task GetMailWithScheme() +{ + var messages = await _graphClient.Me.Messages + .GetAsync(r => + { + r.Options.WithAuthenticationOptions(options => + { + // Specify authentication scheme + options.AcquireTokenOptions.AuthenticationOptionsName = + JwtBearerDefaults.AuthenticationScheme; + + // Specify scopes + options.Scopes = new[] { "Mail.Read" }; + }); + }); + + return Ok(messages); +} +``` + +## Using Both v1.0 and Beta + +You can use both Microsoft Graph v1.0 and Beta in the same application. + +### 1. Install Both Packages + +```bash +dotnet add package Microsoft.Identity.Web.GraphServiceClient +dotnet add package Microsoft.Identity.Web.GraphServiceClientBeta +``` + +### 2. Register Both Services + +```csharp +using Microsoft.Identity.Web; + +builder.Services.AddMicrosoftGraph(); +builder.Services.AddMicrosoftGraphBeta(); +``` + +### 3. Use Both Clients + +```csharp +using GraphServiceClient = Microsoft.Graph.GraphServiceClient; +using GraphBetaServiceClient = Microsoft.Graph.Beta.GraphServiceClient; + +public class MyController : Controller +{ + private readonly GraphServiceClient _graphClient; + private readonly GraphBetaServiceClient _graphBetaClient; + + public MyController( + GraphServiceClient graphClient, + GraphBetaServiceClient graphBetaClient) + { + _graphClient = graphClient; + _graphBetaClient = graphBetaClient; + } + + public async Task GetData() + { + // Use stable v1.0 endpoint + var user = await _graphClient.Me.GetAsync(); + + // Use beta endpoint for preview features + var profile = await _graphBetaClient.Me.Profile.GetAsync(); + + return View(new { user, profile }); + } +} +``` + +## Batch Requests + +Combine multiple Graph calls into a single request: + +```csharp +using Microsoft.Graph.Models; + +public async Task GetDashboard() +{ + var batchRequestContent = new BatchRequestContentCollection(_graphClient); + + // Add multiple requests to batch + var userRequest = _graphClient.Me.ToGetRequestInformation(); + var messagesRequest = _graphClient.Me.Messages.ToGetRequestInformation(); + var eventsRequest = _graphClient.Me.Events.ToGetRequestInformation(); + + var userRequestId = await batchRequestContent.AddBatchRequestStepAsync(userRequest); + var messagesRequestId = await batchRequestContent.AddBatchRequestStepAsync(messagesRequest); + var eventsRequestId = await batchRequestContent.AddBatchRequestStepAsync(eventsRequest); + + // Send batch request + var batchResponse = await _graphClient.Batch.PostAsync(batchRequestContent); + + // Extract responses + var user = await batchResponse.GetResponseByIdAsync(userRequestId); + var messages = await batchResponse.GetResponseByIdAsync(messagesRequestId); + var events = await batchResponse.GetResponseByIdAsync(eventsRequestId); + + return View(new DashboardViewModel + { + User = user, + Messages = messages.Value, + Events = events.Value + }); +} +``` + +## Common Graph Patterns + +### Get User's Manager + +```csharp +public async Task GetManager() +{ + var manager = await _graphClient.Me.Manager.GetAsync(); + + // Cast to User (manager is DirectoryObject) + if (manager is User managerUser) + { + return View(managerUser); + } + + return NotFound("Manager not found"); +} +``` + +### Get User's Photo + +```csharp +public async Task GetPhoto() +{ + try + { + var photoStream = await _graphClient.Me.Photo.Content.GetAsync(); + + return File(photoStream, "image/jpeg"); + } + catch (ServiceException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound) + { + return NotFound("Photo not available"); + } +} +``` + +### Send Email + +```csharp +public async Task SendEmail([FromBody] EmailRequest request) +{ + var message = new Message + { + Subject = request.Subject, + Body = new ItemBody + { + ContentType = BodyType.Html, + Content = request.Body + }, + ToRecipients = new List + { + new Recipient + { + EmailAddress = new EmailAddress + { + Address = request.ToEmail + } + } + } + }; + + await _graphClient.Me.SendMail + .PostAsync(new SendMailPostRequestBody + { + Message = message, + SaveToSentItems = true + }, + requestConfiguration => + { + requestConfiguration.Options.WithScopes("Mail.Send"); + }); + + return Ok("Email sent"); +} +``` + +### Create Calendar Event + +```csharp +public async Task CreateEvent([FromBody] EventRequest request) +{ + var newEvent = new Event + { + Subject = request.Subject, + Start = new DateTimeTimeZone + { + DateTime = request.StartTime.ToString("yyyy-MM-ddTHH:mm:ss"), + TimeZone = "UTC" + }, + End = new DateTimeTimeZone + { + DateTime = request.EndTime.ToString("yyyy-MM-ddTHH:mm:ss"), + TimeZone = "UTC" + }, + Attendees = request.Attendees.Select(email => new Attendee + { + EmailAddress = new EmailAddress { Address = email }, + Type = AttendeeType.Required + }).ToList() + }; + + var createdEvent = await _graphClient.Me.Events + .PostAsync(newEvent, r => r.Options.WithScopes("Calendars.ReadWrite")); + + return Ok(createdEvent); +} +``` + +### Search Users + +```csharp +public async Task SearchUsers(string searchTerm) +{ + var users = await _graphClient.Users + .GetAsync(requestConfiguration => + { + requestConfiguration.QueryParameters.Filter = + $"startswith(displayName,'{searchTerm}') or startswith(mail,'{searchTerm}')"; + requestConfiguration.QueryParameters.Select = + new[] { "displayName", "mail", "jobTitle" }; + requestConfiguration.QueryParameters.Top = 10; + + requestConfiguration.Options.WithScopes("User.ReadBasic.All"); + }); + + return Ok(users.Value); +} +``` + +## 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) + { + OwinTokenAcquirerFactory factory = TokenAcquirerFactory.GetDefaultInstance(); + app.AddMicrosoftIdentityWebApi(factory); + factory.Services + .AddMicrosoftGraph() + factory.Build(); + } +} +``` + +### 2. Call API from Controllers + +```csharp +using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Web; +using System.Web.Http; + +[Authorize] +public class DataController : ApiController +{ + private readonly IDownstreamApi _downstreamApi; + + public DataController() + { + GraphServiceClient graphServiceClient = this.GetGraphServiceClient(); + var me = await graphServiceClient.Me.Request().GetAsync(); + } +``` + +## Migration from Microsoft.Identity.Web.MicrosoftGraph 2.x + +If you're migrating from the older Microsoft.Identity.Web.MicrosoftGraph package (SDK 4.x), here are the key changes: + +### 1. Remove Old Package, Add New + +```bash +dotnet remove package Microsoft.Identity.Web.MicrosoftGraph +dotnet add package Microsoft.Identity.Web.GraphServiceClient +``` + +### 2. Update Method Calls + +The `.Request()` method has been removed in SDK 5.x: + +**Before (SDK 4.x):** +```csharp +var user = await _graphClient.Me.Request().GetAsync(); + +var messages = await _graphClient.Me.Messages + .Request() + .WithScopes("Mail.Read") + .GetAsync(); +``` + +**After (SDK 5.x):** +```csharp +var user = await _graphClient.Me.GetAsync(); + +var messages = await _graphClient.Me.Messages + .GetAsync(r => r.Options.WithScopes("Mail.Read")); +``` + +### 3. WithScopes() Location Changed + +**Before:** +```csharp +var users = await _graphClient.Users + .Request() + .WithScopes("User.Read.All") + .GetAsync(); +``` + +**After:** +```csharp +var users = await _graphClient.Users + .GetAsync(r => r.Options.WithScopes("User.Read.All")); +``` + +### 4. WithAppOnly() Location Changed + +**Before:** +```csharp +var apps = await _graphClient.Applications + .Request() + .WithAppOnly() + .GetAsync(); +``` + +**After:** +```csharp +var apps = await _graphClient.Applications + .GetAsync(r => r.Options.WithAppOnly()); +``` + +### 5. WithAuthenticationScheme() Location Changed + +**Before:** +```csharp +var user = await _graphClient.Me + .Request() + .WithAuthenticationScheme(JwtBearerDefaults.AuthenticationScheme) + .GetAsync(); +``` + +**After:** +```csharp +var user = await _graphClient.Me + .GetAsync(r => r.Options + .WithAuthenticationScheme(JwtBearerDefaults.AuthenticationScheme)); +``` + +See [Microsoft Graph .NET SDK v5 changelog](https://github.com/microsoftgraph/msgraph-sdk-dotnet/blob/dev/docs/upgrade-to-v5.md) for complete migration details. + +## Error Handling + +### Handle ServiceException + +```csharp +using Microsoft.Graph.Models.ODataErrors; + +public async Task GetData() +{ + try + { + var user = await _graphClient.Me.GetAsync(); + return Ok(user); + } + catch (ODataError ex) when (ex.ResponseStatusCode == 404) + { + return NotFound("Resource not found"); + } + catch (ODataError ex) when (ex.ResponseStatusCode == 403) + { + return Forbid("Insufficient permissions"); + } + catch (MicrosoftIdentityWebChallengeUserException) + { + // User needs to consent + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "Graph API call failed"); + return StatusCode(500, "An error occurred"); + } +} +``` + +## Best Practices + +### 1. Request Minimum Scopes + +Only request scopes you need: + +```csharp +// ❌ Bad: Requesting too many scopes +options.Scopes = new[] { "User.Read", "Mail.ReadWrite", "Calendars.ReadWrite", "Files.ReadWrite.All" }; + +// βœ… Good: Request only what you need +options.Scopes = new[] { "User.Read" }; +``` + +### 2. Use Incremental Consent + +Request additional scopes only when needed: + +```csharp +// Sign-in: Only User.Read +// Later, when accessing mail: +var messages = await _graphClient.Me.Messages + .GetAsync(r => r.Options.WithScopes("Mail.Read")); +``` + +### 3. Cache GraphServiceClient + +GraphServiceClient is safe to reuse. Register as singleton or inject from DI. + +### 4. Use Select to Reduce Response Size + +```csharp +// ❌ Bad: Getting all properties +var users = await _graphClient.Users.GetAsync(); + +// βœ… Good: Select only needed properties +var users = await _graphClient.Users + .GetAsync(r => r.QueryParameters.Select = + new[] { "displayName", "mail", "id" }); +``` + +## Troubleshooting + +### Error: "Insufficient privileges to complete the operation" + +**Cause**: App doesn't have required Graph permissions. + +**Solution**: +- Add required API permissions in app registration +- Admin consent required for app permissions +- User consent required for delegated permissions + +### Error: "AADSTS65001: The user or administrator has not consented" + +**Cause**: User hasn't consented to requested scopes. + +**Solution**: Use incremental consent with `.WithScopes()` to trigger consent flow. + +### Photo Returns 404 + +**Cause**: User doesn't have a profile photo. + +**Solution**: Handle 404 gracefully and provide default avatar. + +### Batch Request Fails + +**Cause**: Individual requests in batch may fail independently. + +**Solution**: Check each response in batch for errors: + +```csharp +var userResponse = await batchResponse.GetResponseByIdAsync(userRequestId); +if (userResponse == null) +{ + // Handle individual request failure +} +``` + +## Related Documentation + +- [Microsoft Graph Documentation](https://learn.microsoft.com/graph/) +- [Graph SDK v5 Migration Guide](https://github.com/microsoftgraph/msgraph-sdk-dotnet/blob/dev/docs/upgrade-to-v5.md) +- [Calling Downstream APIs Overview](calling-downstream-apis-README.md) +- [Calling from Web Apps](from-web-apps.md) +- [Calling from Web APIs](from-web-apis.md) + +--- + +**Next Steps**: Learn about [calling Azure SDKs](azure-sdks.md) or [custom APIs](custom-apis.md). diff --git a/docs/frameworks/aspnet-framework.md b/docs/frameworks/aspnet-framework.md new file mode 100644 index 000000000..acee34d31 --- /dev/null +++ b/docs/frameworks/aspnet-framework.md @@ -0,0 +1,110 @@ +# ASP.NET Framework & .NET Standard Support + +This guide provides an overview of Microsoft.Identity.Web support for .NET Framework and .NET Standard applications. + +--- + +## Choose Your Scenario + +Microsoft.Identity.Web provides different packages and integration patterns depending on your application type: + +### πŸ”· MSAL.NET with Microsoft.Identity.Web Packages + +**For console apps, daemon services, and non-web .NET Framework applications** + +Use Microsoft.Identity.Web.TokenCache and Microsoft.Identity.Web.Certificate packages with MSAL.NET for: +- Token cache serialization (SQL Server, Redis, Cosmos DB) +- Certificate loading from KeyVault, certificate store, or file system +- Console applications and daemon services +- .NET Standard 2.0 libraries + +**πŸ‘‰ [MSAL.NET with Microsoft.Identity.Web Guide](msal-dotnet-framework.md)** + +--- + +### 🌐 OWIN Integration for ASP.NET MVC/Web API + +**For ASP.NET MVC and Web API applications** + +Use Microsoft.Identity.Web.OWIN package for full-featured web authentication with: +- TokenAcquirerFactory for automatic token acquisition +- Controller extensions for easy access to Microsoft Graph and downstream APIs +- Distributed token cache support +- Incremental consent handling + +**πŸ‘‰ [OWIN Integration Guide](owin.md)** + +--- + +## Quick Comparison + +| Feature | MSAL.NET + TokenCache/Certificate | OWIN Integration | +|---------|-----------------------------------|------------------| +| **Package** | Microsoft.Identity.Web.TokenCache
Microsoft.Identity.Web.Certificate | Microsoft.Identity.Web.OWIN | +| **Target** | Console apps, daemons, worker services | ASP.NET MVC, ASP.NET Web API | +| **Authentication** | Manual MSAL.NET configuration | Automatic OWIN middleware | +| **Token Acquisition** | Manual with `IConfidentialClientApplication` | Automatic with controller extensions | +| **Token Cache** | βœ… All providers (SQL, Redis, Cosmos) | βœ… All providers (SQL, Redis, Cosmos) | +| **Certificate Loading** | βœ… KeyVault, store, file, Base64 | βœ… Via MSAL.NET configuration | +| **Microsoft Graph** | Manual `GraphServiceClient` setup | βœ… `this.GetGraphServiceClient()` | +| **Downstream APIs** | Manual HTTP calls with tokens | βœ… `this.GetDownstreamApi()` | +| **Incremental Consent** | Manual challenge handling | βœ… Automatic with `MsalUiRequiredException` | + +--- + +## Overview + +Starting with **Microsoft.Identity.Web 1.17+**, you have flexible options for using Microsoft Identity libraries in non-ASP.NET Core environments: + +### Available Packages + +| Package | Purpose | Target Applications | +|---------|---------|---------------------| +| **Microsoft.Identity.Web.TokenCache** | Token cache serializers for MSAL.NET | Console, daemon, worker services | +| **Microsoft.Identity.Web.Certificate** | Certificate loading utilities | Console, daemon, worker services | +| **Microsoft.Identity.Web.OWIN** | OWIN middleware integration | ASP.NET MVC, ASP.NET Web API | + +### Why Use Microsoft.Identity.Web Packages? + +| Feature | Benefit | +|---------|---------| +| **Token Cache Serialization** | Reusable cache adapters for in-memory, SQL Server, Redis, Cosmos DB | +| **Certificate Helpers** | Simplified certificate loading from KeyVault, file system, or cert stores | +| **OWIN Integration** | Seamless authentication for ASP.NET MVC/Web API | +| **.NET Standard 2.0** | Compatible with .NET Framework 4.7.2+, .NET Core, and .NET 5+ | +| **Minimal Dependencies** | Targeted packages without ASP.NET Core dependencies | + +--- + +## Next Steps + +Choose the guide that matches your application type: + +- **Console Apps, Daemons, Worker Services** β†’ [MSAL.NET with Microsoft.Identity.Web](msal-dotnet-framework.md) +- **ASP.NET MVC, ASP.NET Web API** β†’ [OWIN Integration](owin.md) + +--- + +## Sample Applications + +### MSAL.NET Samples + +- [ConfidentialClientTokenCache](https://github.com/Azure-Samples/active-directory-dotnet-v1-to-v2/tree/master/ConfidentialClientTokenCache) - Console app with token cache +- [active-directory-dotnetcore-daemon-v2](https://github.com/Azure-Samples/active-directory-dotnetcore-daemon-v2) - Daemon with certificate from KeyVault + +### OWIN Samples + +- [ms-identity-aspnet-webapp-openidconnect](https://github.com/Azure-Samples/ms-identity-aspnet-webapp-openidconnect) - ASP.NET MVC with Microsoft.Identity.Web.OWIN + +--- + +## Additional Resources + +- [Token Cache Serialization in MSAL.NET](https://learn.microsoft.com/azure/active-directory/develop/msal-net-token-cache-serialization) +- [Using Certificates with Microsoft.Identity.Web](https://github.com/AzureAD/microsoft-identity-web/wiki/Certificates) +- [OWIN Integration Guide](https://github.com/AzureAD/microsoft-identity-web/wiki/OWIN) +- [NuGet Package Dependencies](https://github.com/AzureAD/microsoft-identity-web/wiki/NuGet-package-references) + +--- + +**Supported Frameworks:** .NET Framework 4.7.2+, .NET Standard 2.0 \ No newline at end of file diff --git a/docs/frameworks/msal-dotnet-framework.md b/docs/frameworks/msal-dotnet-framework.md new file mode 100644 index 000000000..9f134dad9 --- /dev/null +++ b/docs/frameworks/msal-dotnet-framework.md @@ -0,0 +1,740 @@ +# MSAL.NET with Microsoft.Identity.Web in .NET Framework + +This guide explains how to use Microsoft.Identity.Web token cache and certificate packages with MSAL.NET in .NET Framework, .NET Standard 2.0, and classic .NET applications (.NET 4.7.2+). + +--- + +## πŸ“‹ Table of Contents + +- [Overview](#overview) +- [Package Options](#package-options) +- [Token Cache Serialization](#token-cache-serialization) +- [Certificate Management](#certificate-management) +- [Sample Applications](#sample-applications) +- [Best Practices](#best-practices) + +--- + +## Overview + +Starting with **Microsoft.Identity.Web 1.17+**, you can use Microsoft.Identity.Web utility packages with MSAL.NET in non-ASP.NET Core environments. + +### Why Use These Packages? + +| Feature | Benefit | +|---------|---------| +| **Token Cache Serialization** | Reusable cache adapters for in-memory, SQL Server, Redis, Cosmos DB | +| **Certificate Helpers** | Simplified certificate loading from KeyVault, file system, or cert stores | +| **Claims Extensions** | Utility methods for `ClaimsPrincipal` manipulation | +| **.NET Standard 2.0** | Compatible with .NET Framework 4.7.2+, .NET Core, and .NET 5+ | +| **Minimal Dependencies** | Targeted packages without ASP.NET Core dependencies | + +### Supported Scenarios + +- βœ… **.NET Framework Console Applications** (daemon scenarios) +- βœ… **Desktop Applications** (.NET Framework) +- βœ… **Worker Services** (.NET Framework) +- βœ… **.NET Standard 2.0 Libraries** (cross-platform compatibility) +- βœ… **Non-web MSAL.NET applications** + +> **Note:** For ASP.NET MVC/Web API applications, see [OWIN Integration](owin.md) instead. + +--- + +## Package Options + +### Core Packages for MSAL.NET + +| Package | Purpose | Dependencies | .NET Target | +|---------|---------|--------------|-------------| +| **Microsoft.Identity.Web.TokenCache** | Token cache serializers, `ClaimsPrincipal` extensions | Minimal | .NET Standard 2.0 | +| **Microsoft.Identity.Web.Certificate** | Certificate loading utilities | Minimal | .NET Standard 2.0 | + +### Installation + +**Package Manager Console:** +```powershell +# Token cache serialization +Install-Package Microsoft.Identity.Web.TokenCache + +# Certificate management +Install-Package Microsoft.Identity.Web.Certificate +``` + +**.NET CLI:** +```bash +dotnet add package Microsoft.Identity.Web.TokenCache +dotnet add package Microsoft.Identity.Web.Certificate +``` + +### Why Not Microsoft.Identity.Web (Core)? + +The core `Microsoft.Identity.Web` package includes ASP.NET Core dependencies (`Microsoft.AspNetCore.*`), which: +- Are incompatible with ASP.NET Framework +- Increase package size unnecessarily +- Create dependency conflicts + +**Use targeted packages instead** for .NET Framework and .NET Standard scenarios. + +--- + +## Token Cache Serialization + +### Overview + +Microsoft.Identity.Web provides token cache adapters that work seamlessly with MSAL.NET's `IConfidentialClientApplication`. + +### Pattern: Building Confidential Client with Token Cache + +```csharp +using Microsoft.Identity.Client; +using Microsoft.Identity.Web; +using Microsoft.Identity.Web.TokenCacheProviders; + +public class MsalAppBuilder +{ + private static IConfidentialClientApplication _app; + + public static IConfidentialClientApplication BuildConfidentialClientApplication() + { + if (_app == null) + { + string clientId = ConfigurationManager.AppSettings["AzureAd:ClientId"]; + string clientSecret = ConfigurationManager.AppSettings["AzureAd:ClientSecret"]; + string tenantId = ConfigurationManager.AppSettings["AzureAd:TenantId"]; + + // Create the confidential client application + _app = ConfidentialClientApplicationBuilder.Create(clientId) + .WithClientSecret(clientSecret) + .WithTenantId(tenantId) + .WithAuthority(AzureCloudInstance.AzurePublic, tenantId) + .Build(); + + // Add token cache serialization (choose one option below) + _app.AddInMemoryTokenCache(); + } + + return _app; + } +} +``` + +### Token Cache Options + +#### Option 1: In-Memory Token Cache + +**Simple in-memory cache:** +```csharp +using Microsoft.Identity.Web.TokenCacheProviders; + +_app.AddInMemoryTokenCache(); +``` + +**In-memory cache with size limits** (Microsoft.Identity.Web 1.20+): +```csharp +using Microsoft.Extensions.Caching.Memory; + +_app.AddInMemoryTokenCache(services => +{ + // Configure memory cache options + services.Configure(options => + { + options.SizeLimit = 5000000; // 5 MB limit + }); +}); +``` + +**Characteristics:** +- βœ… Fast access +- βœ… No external dependencies +- ❌ Not shared across processes +- ❌ Lost on app restart + +**Use case:** Single-instance console apps, desktop applications + +--- + +#### Option 2: Distributed In-Memory Token Cache + +**For multi-instance environments with in-memory cache:** +```csharp +_app.AddDistributedTokenCaches(services => +{ + // Requires: Microsoft.Extensions.Caching.Memory (NuGet) + services.AddDistributedMemoryCache(); +}); +``` + +**Characteristics:** +- βœ… Shared across app instances +- βœ… Better for load-balanced scenarios +- ❌ Requires additional NuGet package +- ❌ Still lost on app restart + +**Use case:** Multi-instance services with acceptable token re-acquisition + +--- + +#### Option 3: SQL Server Token Cache + +**For persistent, distributed caching:** +```csharp +using Microsoft.Extensions.Caching.SqlServer; + +_app.AddDistributedTokenCaches(services => +{ + // Requires: Microsoft.Extensions.Caching.SqlServer (NuGet) + services.AddDistributedSqlServerCache(options => + { + options.ConnectionString = ConfigurationManager.ConnectionStrings["TokenCache"].ConnectionString; + options.SchemaName = "dbo"; + options.TableName = "TokenCache"; + + // IMPORTANT: Set expiration above token lifetime + // Access tokens typically expire after 1 hour + options.DefaultSlidingExpiration = TimeSpan.FromMinutes(90); + }); +}); +``` + +**Database setup:** +```sql +-- Create the cache table +CREATE TABLE [dbo].[TokenCache] ( + [Id] NVARCHAR(449) NOT NULL, + [Value] VARBINARY(MAX) NOT NULL, + [ExpiresAtTime] DATETIMEOFFSET NOT NULL, + [SlidingExpirationInSeconds] BIGINT NULL, + [AbsoluteExpiration] DATETIMEOFFSET NULL, + PRIMARY KEY ([Id]) +); + +-- Create index for performance +CREATE INDEX [Index_ExpiresAtTime] ON [dbo].[TokenCache] ([ExpiresAtTime]); +``` + +**Characteristics:** +- βœ… Persistent across restarts +- βœ… Shared across multiple instances +- βœ… Reliable and scalable +- ⚠️ Requires SQL Server setup + +**Use case:** Production daemon services, scheduled tasks, multi-instance workers + +--- + +#### Option 4: Redis Token Cache + +**For high-performance distributed caching:** +```csharp +using StackExchange.Redis; +using Microsoft.Extensions.Caching.StackExchangeRedis; + +_app.AddDistributedTokenCaches(services => +{ + // Requires: Microsoft.Extensions.Caching.StackExchangeRedis (NuGet) + services.AddStackExchangeRedisCache(options => + { + options.Configuration = ConfigurationManager.AppSettings["Redis:ConnectionString"]; + options.InstanceName = "TokenCache_"; + }); +}); +``` + +**Production configuration:** +```csharp +services.AddStackExchangeRedisCache(options => +{ + options.Configuration = ConfigurationManager.AppSettings["Redis:ConnectionString"]; + options.InstanceName = "MyDaemonApp_"; + + // Optional: Configure Redis options + options.ConfigurationOptions = new ConfigurationOptions + { + AbortOnConnectFail = false, + ConnectTimeout = 5000, + SyncTimeout = 5000 + }; +}); +``` + +**Characteristics:** +- βœ… Extremely fast +- βœ… Shared across instances +- βœ… Persistent (with Redis persistence enabled) +- ⚠️ Requires Redis server + +**Use case:** High-volume daemon apps, distributed systems, microservices + +--- + +#### Option 5: Cosmos DB Token Cache + +**For globally distributed caching:** +```csharp +using Microsoft.Extensions.Caching.Cosmos; + +_app.AddDistributedTokenCaches(services => +{ + // Requires: Microsoft.Extensions.Caching.Cosmos (preview) + services.AddCosmosCache(options => + { + options.ContainerName = "TokenCache"; + options.DatabaseName = "IdentityCache"; + options.ClientBuilder = new CosmosClientBuilder( + ConfigurationManager.AppSettings["CosmosConnectionString"]); + options.CreateIfNotExists = true; + }); +}); +``` + +**Characteristics:** +- βœ… Globally distributed +- βœ… Highly available +- βœ… Automatic scaling +- ⚠️ Higher latency than Redis +- ⚠️ Higher cost + +**Use case:** Global daemon services, geo-distributed applications + +--- + +### Complete Example: Daemon Application + +```csharp +using Microsoft.Identity.Client; +using Microsoft.Identity.Web; +using Microsoft.Identity.Web.TokenCacheProviders; +using System; +using System.Threading.Tasks; + +namespace DaemonApp +{ + class Program + { + private static IConfidentialClientApplication _app; + + static async Task Main(string[] args) + { + // Build confidential client with token cache + _app = BuildConfidentialClient(); + + // Acquire token for app-only access + string[] scopes = new[] { "https://graph.microsoft.com/.default" }; + + try + { + var result = await _app.AcquireTokenForClient(scopes) + .ExecuteAsync(); + + Console.WriteLine($"Token acquired successfully!"); + Console.WriteLine($"Token source: {result.AuthenticationResultMetadata.TokenSource}"); + Console.WriteLine($"Expires on: {result.ExpiresOn}"); + + // Use token to call API + await CallProtectedApi(result.AccessToken); + } + catch (MsalServiceException ex) + { + Console.WriteLine($"Error acquiring token: {ex.ErrorCode}"); + Console.WriteLine($"CorrelationId: {ex.CorrelationId}"); + } + } + + private static IConfidentialClientApplication BuildConfidentialClient() + { + var app = ConfidentialClientApplicationBuilder + .Create(ConfigurationManager.AppSettings["ClientId"]) + .WithClientSecret(ConfigurationManager.AppSettings["ClientSecret"]) + .WithTenantId(ConfigurationManager.AppSettings["TenantId"]) + .Build(); + + // Add SQL Server token cache for persistence + app.AddDistributedTokenCaches(services => + { + services.AddDistributedSqlServerCache(options => + { + options.ConnectionString = ConfigurationManager + .ConnectionStrings["TokenCache"].ConnectionString; + options.SchemaName = "dbo"; + options.TableName = "TokenCache"; + options.DefaultSlidingExpiration = TimeSpan.FromMinutes(90); + }); + }); + + return app; + } + + private static async Task CallProtectedApi(string accessToken) + { + // Your API call logic + } + } +} +``` + +--- + +## Certificate Management + +### Overview + +Microsoft.Identity.Web simplifies certificate loading from various sources for client credential flows. + +### Pattern: Loading Certificates with DefaultCertificateLoader + +```csharp +using Microsoft.Identity.Web; +using Microsoft.Identity.Client; + +public class CertificateHelper +{ + public static IConfidentialClientApplication CreateAppWithCertificate() + { + string clientId = ConfigurationManager.AppSettings["AzureAd:ClientId"]; + string tenantId = ConfigurationManager.AppSettings["AzureAd:TenantId"]; + + // Define certificate source + var certDescription = CertificateDescription.FromKeyVault( + keyVaultUrl: "https://my-keyvault.vault.azure.net", + keyVaultCertificateName: "MyCertificate" + ); + + // Load certificate + ICertificateLoader certificateLoader = new DefaultCertificateLoader(); + certificateLoader.LoadIfNeeded(certDescription); + + // Create confidential client with certificate + var app = ConfidentialClientApplicationBuilder.Create(clientId) + .WithCertificate(certDescription.Certificate) + .WithTenantId(tenantId) + .Build(); + + // Add token cache + app.AddInMemoryTokenCache(); + + return app; + } +} +``` + +### Certificate Sources + +#### 1. From Azure Key Vault + +```csharp +var certDescription = CertificateDescription.FromKeyVault( + keyVaultUrl: "https://my-keyvault.vault.azure.net", + keyVaultCertificateName: "MyApplicationCert" +); + +ICertificateLoader loader = new DefaultCertificateLoader(); +loader.LoadIfNeeded(certDescription); + +var app = ConfidentialClientApplicationBuilder.Create(clientId) + .WithCertificate(certDescription.Certificate) + .WithTenantId(tenantId) + .Build(); +``` + +**Prerequisites:** +- Managed Identity or Service Principal with Key Vault access +- `Azure.Identity` NuGet package +- Key Vault permission: `Get` on certificates + +--- + +#### 2. From Certificate Store + +```csharp +var certDescription = CertificateDescription.FromStoreWithDistinguishedName( + distinguishedName: "CN=MyApp.contoso.com", + storeName: StoreName.My, + storeLocation: StoreLocation.CurrentUser +); + +ICertificateLoader loader = new DefaultCertificateLoader(); +loader.LoadIfNeeded(certDescription); + +var app = ConfidentialClientApplicationBuilder.Create(clientId) + .WithCertificate(certDescription.Certificate) + .WithTenantId(tenantId) + .Build(); +``` + +**Or find by thumbprint:** +```csharp +var certDescription = CertificateDescription.FromStoreWithThumbprint( + thumbprint: "ABCDEF1234567890ABCDEF1234567890ABCDEF12", + storeName: StoreName.My, + storeLocation: StoreLocation.LocalMachine +); +``` + +--- + +#### 3. From File System + +```csharp +var certDescription = CertificateDescription.FromPath( + path: @"C:\Certificates\MyAppCert.pfx", + password: ConfigurationManager.AppSettings["Certificate:Password"] +); + +ICertificateLoader loader = new DefaultCertificateLoader(); +loader.LoadIfNeeded(certDescription); + +var app = ConfidentialClientApplicationBuilder.Create(clientId) + .WithCertificate(certDescription.Certificate) + .WithTenantId(tenantId) + .Build(); +``` + +**Security note:** Never hardcode passwords. Use secure configuration. + +--- + +#### 4. From Base64-Encoded String + +```csharp +string base64Cert = ConfigurationManager.AppSettings["Certificate:Base64"]; + +var certDescription = CertificateDescription.FromBase64Encoded( + base64EncodedValue: base64Cert, + password: ConfigurationManager.AppSettings["Certificate:Password"] // Optional +); + +ICertificateLoader loader = new DefaultCertificateLoader(); +loader.LoadIfNeeded(certDescription); +``` + +--- + +### Configuration-Based Certificate Loading + +**App.config:** +```xml + + + + + + + + + + + + + + + + +``` + +**C# code:** +```csharp +public static CertificateDescription GetCertificateFromConfig() +{ + string sourceType = ConfigurationManager.AppSettings["Certificate:SourceType"]; + + return sourceType switch + { + "KeyVault" => CertificateDescription.FromKeyVault( + ConfigurationManager.AppSettings["Certificate:KeyVaultUrl"], + ConfigurationManager.AppSettings["Certificate:KeyVaultCertificateName"] + ), + + "StoreWithThumbprint" => CertificateDescription.FromStoreWithThumbprint( + ConfigurationManager.AppSettings["Certificate:CertificateThumbprint"], + StoreName.My, + StoreLocation.CurrentUser + ), + + _ => throw new ConfigurationErrorsException("Invalid certificate source type") + }; +} +``` + +--- + +## Sample Applications + +### Official Microsoft Samples + +| Sample | Platform | Description | +|--------|----------|-------------| +| [ConfidentialClientTokenCache](https://github.com/Azure-Samples/active-directory-dotnet-v1-to-v2/tree/master/ConfidentialClientTokenCache) | Console (.NET Framework) | Token cache serialization patterns | +| [active-directory-dotnetcore-daemon-v2](https://github.com/Azure-Samples/active-directory-dotnetcore-daemon-v2) | Console (.NET Core) | Certificate loading from Key Vault | + +--- + +## Best Practices + +### βœ… Do's + +**1. Use singleton pattern for IConfidentialClientApplication:** +```csharp +private static IConfidentialClientApplication _app; + +public static IConfidentialClientApplication GetApp() +{ + if (_app == null) + { + _app = ConfidentialClientApplicationBuilder.Create(clientId) + .WithClientSecret(clientSecret) + .WithTenantId(tenantId) + .Build(); + + _app.AddDistributedTokenCaches(/* ... */); + } + + return _app; +} +``` + +**2. Set appropriate token cache expiration:** +```csharp +// Access tokens typically expire after 1 hour +// Set cache expiration ABOVE token lifetime +options.DefaultSlidingExpiration = TimeSpan.FromMinutes(90); +``` + +**3. Use secure certificate storage:** +```csharp +// βœ… Azure Key Vault (production) +var cert = CertificateDescription.FromKeyVault(keyVaultUrl, certName); + +// βœ… Certificate store with proper permissions +var cert = CertificateDescription.FromStoreWithThumbprint( + thumbprint, StoreName.My, StoreLocation.LocalMachine); +``` + +**4. Implement proper error handling:** +```csharp +try +{ + var result = await app.AcquireTokenForClient(scopes).ExecuteAsync(); +} +catch (MsalServiceException ex) +{ + logger.Error($"Token acquisition failed. CorrelationId: {ex.CorrelationId}, ErrorCode: {ex.ErrorCode}"); + throw; +} +``` + +**5. Use distributed cache for production:** +```csharp +// βœ… Correct for daemon services +app.AddDistributedTokenCaches(services => +{ + services.AddDistributedSqlServerCache(/* ... */); +}); +``` + +### ❌ Don'ts + +**1. Don't create new IConfidentialClientApplication instances repeatedly:** +```csharp +// ❌ Wrong - creates new instance every time +public void AcquireToken() +{ + var app = ConfidentialClientApplicationBuilder.Create(clientId).Build(); + // ... +} + +// βœ… Correct - use singleton +private static readonly IConfidentialClientApplication _app = BuildApp(); +``` + +**2. Don't hardcode secrets:** +```csharp +// ❌ Wrong +.WithClientSecret("supersecretvalue123") + +// βœ… Correct +.WithClientSecret(ConfigurationManager.AppSettings["AzureAd:ClientSecret"]) +``` + +**3. Don't use in-memory cache for multi-instance services:** +```csharp +// ❌ Wrong for services with multiple instances +app.AddInMemoryTokenCache(); + +// βœ… Correct - use distributed cache +app.AddDistributedTokenCaches(services => +{ + services.AddDistributedSqlServerCache(/* ... */); +}); +``` + +**4. Don't ignore certificate validation:** +```csharp +// ❌ Wrong - skips validation +ServicePointManager.ServerCertificateValidationCallback = (sender, cert, chain, errors) => true; + +// βœ… Correct - validate certificates properly +``` + +--- + +## Migration from ADAL.NET + +### Key Differences + +| Aspect | ADAL.NET (deprecated) | MSAL.NET + Microsoft.Identity.Web | +|--------|----------------------|-----------------------------------| +| **Scopes** | Resource-based (`https://graph.microsoft.com`) | Scope-based (`https://graph.microsoft.com/.default`) | +| **Token Cache** | Manual serialization required | Built-in adapters via extension methods | +| **Certificates** | Manual X509Certificate2 loading | `DefaultCertificateLoader` with multiple sources | +| **Authority** | Fixed at construction | Can be overridden per request | + +### Migration Example + +**ADAL.NET (Old):** +```csharp +AuthenticationContext authContext = new AuthenticationContext(authority); +ClientCredential credential = new ClientCredential(clientId, clientSecret); +AuthenticationResult result = await authContext.AcquireTokenAsync(resource, credential); +``` + +**MSAL.NET with Microsoft.Identity.Web (New):** +```csharp +var app = ConfidentialClientApplicationBuilder.Create(clientId) + .WithClientSecret(clientSecret) + .WithTenantId(tenantId) + .Build(); + +app.AddInMemoryTokenCache(); // Add token cache + +string[] scopes = new[] { "https://graph.microsoft.com/.default" }; +AuthenticationResult result = await app.AcquireTokenForClient(scopes).ExecuteAsync(); +``` + +--- + +## See Also + +- **[Daemon Applications Guide](../getting-started/daemon-app.md)** - Complete guide for daemon apps, autonomous agents, agent user identities +- **[OWIN Integration](owin.md)** - For ASP.NET MVC and Web API applications +- **[ASP.NET Framework Overview](aspnet-framework.md)** - Choose the right package for your scenario +- **[Credentials Guide](../authentication/credentials/credentials-README.md)** - Certificate and client secret management +- **[Logging & Diagnostics](../advanced/logging.md)** - Troubleshoot token acquisition issues + +--- + +## Additional Resources + +- [Token Cache Serialization in MSAL.NET](https://learn.microsoft.com/azure/active-directory/develop/msal-net-token-cache-serialization) +- [Using Certificates with Microsoft.Identity.Web](https://github.com/AzureAD/microsoft-identity-web/wiki/Certificates) +- [NuGet Package Dependencies](https://github.com/AzureAD/microsoft-identity-web/wiki/NuGet-package-references) +- [MSAL.NET Documentation](https://learn.microsoft.com/azure/active-directory/develop/msal-overview) + +--- + +**Supported Frameworks:** .NET Framework 4.7.2+, .NET Standard 2.0 diff --git a/docs/frameworks/owin.md b/docs/frameworks/owin.md new file mode 100644 index 000000000..5e7176563 --- /dev/null +++ b/docs/frameworks/owin.md @@ -0,0 +1,785 @@ +# OWIN Integration with Microsoft.Identity.Web + +This guide explains how to use Microsoft.Identity.Web.OWIN package with ASP.NET MVC and Web API applications running on .NET Framework 4.7.2+. + +--- + +## πŸ“‹ Table of Contents + +- [Overview](#overview) +- [Installation](#installation) +- [Configuration](#configuration) +- [Startup Setup](#startup-setup) +- [Controller Integration](#controller-integration) +- [Calling Microsoft Graph](#calling-microsoft-graph) +- [Calling Downstream APIs](#calling-downstream-apis) +- [Sample Applications](#sample-applications) +- [Best Practices](#best-practices) + +--- + +## Overview + +The **Microsoft.Identity.Web.OWIN** package brings the power of Microsoft.Identity.Web to ASP.NET MVC and Web API applications using OWIN middleware. + +### Why Use Microsoft.Identity.Web.OWIN? + +| Feature | Benefit | +|---------|---------| +| **TokenAcquirerFactory** | Automatic token acquisition with caching | +| **Controller Extensions** | Easy access to `GraphServiceClient` and `IDownstreamApi` | +| **Distributed Token Cache** | Built-in support for SQL Server, Redis, Cosmos DB | +| **Automatic Token Refresh** | Handles token refresh transparently | +| **Incremental Consent** | Seamless consent flow integration | + +### Supported Scenarios + +- βœ… **ASP.NET MVC Web Applications** (.NET Framework 4.7.2+) +- βœ… **ASP.NET Web API** (.NET Framework 4.7.2+) +- βœ… **Hybrid Apps** (MVC + Web API) +- βœ… **Calling Microsoft Graph** from controllers +- βœ… **Calling Downstream APIs** with automatic authentication + +--- + +## Installation + +**Package Manager Console:** +```powershell +Install-Package Microsoft.Identity.Web.OWIN +``` + +**.NET CLI:** +```bash +dotnet add package Microsoft.Identity.Web.OWIN +``` + +**Dependencies automatically included:** +- Microsoft.Identity.Web.TokenAcquisition +- Microsoft.Identity.Web.TokenCache +- Microsoft.Owin +- System.Web + +--- + +## Configuration + +### Web.config + +```xml + + + + + + + + + + + + + + + + + + + + + + + + +``` + +### appsettings.json (Alternative) + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + "ClientSecret": "your-client-secret", + "RedirectUri": "https://localhost:44368/", + "PostLogoutRedirectUri": "https://localhost:44368/" + }, + "DownstreamApi": { + "MicrosoftGraph": { + "BaseUrl": "https://graph.microsoft.com/v1.0", + "Scopes": "user.read" + }, + "TodoListService": { + "BaseUrl": "https://localhost:44351", + "Scopes": "api://todo-api-client-id/.default" + } + } +} +``` + +--- + +## Startup Setup + +### App_Start/Startup.Auth.cs + +**Complete setup with Microsoft.Identity.Web.OWIN:** + +```csharp +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Configuration; +using Microsoft.Identity.Web; +using Microsoft.Identity.Web.OWIN; +using Microsoft.Identity.Web.TokenCacheProviders.Distributed; +using Microsoft.Owin.Security; +using Microsoft.Owin.Security.Cookies; +using Microsoft.Owin.Security.OpenIdConnect; +using Owin; +using System; +using System.Configuration; +using System.Web; + +namespace MyMvcApp +{ + public partial class Startup + { + public void ConfigureAuth(IAppBuilder app) + { + // Set default authentication type + app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType); + + // Configure cookie authentication + app.UseCookieAuthentication(new CookieAuthenticationOptions + { + CookieName = "MyApp.Auth", + ExpireTimeSpan = TimeSpan.FromHours(1), + SlidingExpiration = true + }); + + // Configure OpenID Connect authentication + app.UseOpenIdConnectAuthentication( + new OpenIdConnectAuthenticationOptions + { + ClientId = ConfigurationManager.AppSettings["AzureAd:ClientId"], + Authority = $"https://login.microsoftonline.com/{ConfigurationManager.AppSettings["AzureAd:TenantId"]}", + RedirectUri = ConfigurationManager.AppSettings["AzureAd:RedirectUri"], + PostLogoutRedirectUri = ConfigurationManager.AppSettings["AzureAd:PostLogoutRedirectUri"], + + Scope = "openid profile email offline_access", + ResponseType = "code id_token", + + TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + NameClaimType = "preferred_username" + }, + + Notifications = new OpenIdConnectAuthenticationNotifications + { + AuthenticationFailed = context => + { + context.HandleResponse(); + context.Response.Redirect("/Error?message=" + context.Exception.Message); + return Task.FromResult(0); + } + } + }); + + // Configure Microsoft Identity Web services + var services = CreateOwinServiceCollection(); + + // Add token acquisition + services.AddTokenAcquisition(); + + // Add Microsoft Graph support + services.AddMicrosoftGraph(); + + // Add downstream API support + services.AddDownstreamApi("MicrosoftGraph", services.BuildServiceProvider() + .GetRequiredService().GetSection("DownstreamApi:MicrosoftGraph")); + + services.AddDownstreamApi("TodoListService", services.BuildServiceProvider() + .GetRequiredService().GetSection("DownstreamApi:TodoListService")); + + // Configure token cache (choose one option) + ConfigureTokenCache(services); + + // Build service provider + var serviceProvider = services.BuildServiceProvider(); + + // Create and register token acquirer factory + var tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance(); + tokenAcquirerFactory.Build(serviceProvider); + + // Add OWIN token acquisition middleware + app.Use(tokenAcquirerFactory); + } + + private IServiceCollection CreateOwinServiceCollection() + { + var services = new ServiceCollection(); + + // Add configuration from appsettings.json and/or Web.config + IConfiguration configuration = new ConfigurationBuilder() + .AddJsonFile("appsettings.json", optional: true) + .AddInMemoryCollection(new Dictionary + { + ["AzureAd:Instance"] = ConfigurationManager.AppSettings["AzureAd:Instance"], + ["AzureAd:TenantId"] = ConfigurationManager.AppSettings["AzureAd:TenantId"], + ["AzureAd:ClientId"] = ConfigurationManager.AppSettings["AzureAd:ClientId"], + ["AzureAd:ClientSecret"] = ConfigurationManager.AppSettings["AzureAd:ClientSecret"], + ["DownstreamApi:MicrosoftGraph:BaseUrl"] = ConfigurationManager.AppSettings["DownstreamApi:MicrosoftGraph:BaseUrl"], + ["DownstreamApi:MicrosoftGraph:Scopes"] = ConfigurationManager.AppSettings["DownstreamApi:MicrosoftGraph:Scopes"], + }) + .Build(); + + services.AddSingleton(configuration); + + return services; + } + + private void ConfigureTokenCache(IServiceCollection services) + { + // Option 1: In-memory cache (development) + services.AddDistributedTokenCaches(cacheServices => + { + cacheServices.AddDistributedMemoryCache(); + }); + + // Option 2: SQL Server cache (production) + /* + services.AddDistributedTokenCaches(cacheServices => + { + cacheServices.AddDistributedSqlServerCache(options => + { + options.ConnectionString = ConfigurationManager.ConnectionStrings["TokenCache"].ConnectionString; + options.SchemaName = "dbo"; + options.TableName = "TokenCache"; + options.DefaultSlidingExpiration = TimeSpan.FromMinutes(90); + }); + }); + */ + + // Option 3: Redis cache (production, high-scale) + /* + services.AddDistributedTokenCaches(cacheServices => + { + cacheServices.AddStackExchangeRedisCache(options => + { + options.Configuration = ConfigurationManager.AppSettings["Redis:ConnectionString"]; + options.InstanceName = "MyMvcApp_"; + }); + }); + */ + } + } +} +``` + +--- + +## Controller Integration + +### MVC Controllers + +**Using controller extension methods:** + +```csharp +using Microsoft.Identity.Web; +using Microsoft.Identity.Web.OWIN; +using Microsoft.Graph; +using System.Threading.Tasks; +using System.Web.Mvc; + +namespace MyMvcApp.Controllers +{ + [Authorize] + public class HomeController : Controller + { + // GET: Home/Index + public async Task Index() + { + try + { + // Access Microsoft Graph using extension method + var graphClient = this.GetGraphServiceClient(); + var user = await graphClient.Me.GetAsync(); + + ViewBag.UserName = user.DisplayName; + ViewBag.Email = user.Mail ?? user.UserPrincipalName; + ViewBag.JobTitle = user.JobTitle; + + return View(); + } + catch (MsalUiRequiredException) + { + // Incremental consent required + return new ChallengeResult(); + } + catch (Exception ex) + { + return View("Error", new ErrorViewModel { Message = ex.Message }); + } + } + + // GET: Home/Profile + public async Task Profile() + { + var graphClient = this.GetGraphServiceClient(); + + // Get user profile + var user = await graphClient.Me + .GetAsync(requestConfig => requestConfig.QueryParameters.Select = new[] { "displayName", "mail", "jobTitle", "department" }); + + return View(user); + } + + // GET: Home/Photo + public async Task Photo() + { + var graphClient = this.GetGraphServiceClient(); + + try + { + // Get user photo + var photoStream = await graphClient.Me.Photo.Content.GetAsync(); + return File(photoStream, "image/jpeg"); + } + catch (ServiceException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound) + { + return File(Server.MapPath("~/Content/images/default-user.png"), "image/png"); + } + } + } +} +``` + +### Web API Controllers + +**Using ApiController extension methods:** + +```csharp +using Microsoft.Identity.Web; +using Microsoft.Identity.Web.OWIN; +using Microsoft.Identity.Abstractions; +using System.Threading.Tasks; +using System.Web.Http; + +namespace MyWebApi.Controllers +{ + [Authorize] + [RoutePrefix("api/todos")] + public class TodoController : ApiController + { + // GET: api/todos + [HttpGet] + [Route("")] + public async Task GetTodos() + { + try + { + // Call downstream API using extension method + var downstreamApi = this.GetDownstreamApi(); + + var todos = await downstreamApi.GetForUserAsync>( + "TodoListService", + options => + { + options.RelativePath = "api/todolist"; + }); + + return Ok(todos); + } + catch (MsalUiRequiredException) + { + return Unauthorized(); + } + catch (HttpRequestException ex) + { + return InternalServerError(ex); + } + } + + // POST: api/todos + [HttpPost] + [Route("")] + public async Task CreateTodo([FromBody] TodoItem todo) + { + var downstreamApi = this.GetDownstreamApi(); + + var createdTodo = await downstreamApi.PostForUserAsync( + "TodoListService", + todo, + options => + { + options.RelativePath = "api/todolist"; + }); + + return Created($"api/todos/{createdTodo.Id}", createdTodo); + } + } +} +``` + +--- + +## Calling Microsoft Graph + +### Setup Microsoft Graph Client + +**Already configured in Startup.Auth.cs:** +```csharp +services.AddMicrosoftGraph(); +``` + +### Using GraphServiceClient in Controllers + +```csharp +[Authorize] +public class GraphController : Controller +{ + public async Task MyProfile() + { + var graphClient = this.GetGraphServiceClient(); + var user = await graphClient.Me.GetAsync(); + + return View(user); + } + + public async Task MyManager() + { + var graphClient = this.GetGraphServiceClient(); + var manager = await graphClient.Me.Manager.GetAsync(); + + return View(manager); + } + + public async Task MyDirectReports() + { + var graphClient = this.GetGraphServiceClient(); + var directReports = await graphClient.Me.DirectReports.GetAsync(); + + return View(directReports.Value); + } + + public async Task SendEmail([FromBody] EmailMessage message) + { + var graphClient = this.GetGraphServiceClient(); + + var email = new Message + { + Subject = message.Subject, + Body = new ItemBody + { + ContentType = BodyType.Text, + Content = message.Body + }, + ToRecipients = new[] + { + new Recipient + { + EmailAddress = new EmailAddress + { + Address = message.To + } + } + } + }; + + await graphClient.Me.SendMail.PostAsync(new SendMailPostRequestBody + { + Message = email + }); + + return RedirectToAction("Index"); + } +} +``` + +--- + +## Calling Downstream APIs + +### Configure Downstream API + +**In Startup.Auth.cs:** +```csharp +services.AddDownstreamApi("TodoListService", configuration.GetSection("DownstreamApi:TodoListService")); +``` + +**In Web.config:** +```xml + + +``` + +### Using IDownstreamApi in Controllers + +```csharp +[Authorize] +public class TodoController : Controller +{ + // GET all todos + public async Task Index() + { + var downstreamApi = this.GetDownstreamApi(); + + var todos = await downstreamApi.GetForUserAsync>( + "TodoListService", + options => + { + options.RelativePath = "api/todolist"; + }); + + return View(todos); + } + + // GET specific todo + public async Task Details(int id) + { + var downstreamApi = this.GetDownstreamApi(); + + var todo = await downstreamApi.GetForUserAsync( + "TodoListService", + options => + { + options.RelativePath = $"api/todolist/{id}"; + }); + + return View(todo); + } + + // POST new todo + [HttpPost] + public async Task Create(TodoItem todo) + { + var downstreamApi = this.GetDownstreamApi(); + + var createdTodo = await downstreamApi.PostForUserAsync( + "TodoListService", + todo, + options => + { + options.RelativePath = "api/todolist"; + }); + + return RedirectToAction("Index"); + } + + // PUT update todo + [HttpPost] + public async Task Edit(int id, TodoItem todo) + { + var downstreamApi = this.GetDownstreamApi(); + + await downstreamApi.CallApiForUserAsync( + "TodoListService", + options => + { + options.HttpMethod = HttpMethod.Put; + options.RelativePath = $"api/todolist/{id}"; + options.RequestBody = todo; + }); + + return RedirectToAction("Index"); + } + + // DELETE todo + [HttpPost] + public async Task Delete(int id) + { + var downstreamApi = this.GetDownstreamApi(); + + await downstreamApi.CallApiForUserAsync( + "TodoListService", + options => + { + options.HttpMethod = HttpMethod.Delete; + options.RelativePath = $"api/todolist/{id}"; + }); + + return RedirectToAction("Index"); + } +} +``` + +--- + +## Sample Applications + +### Official Microsoft Samples + +| Sample | Description | +|--------|-------------| +| [ms-identity-aspnet-webapp-openidconnect](https://github.com/Azure-Samples/ms-identity-aspnet-webapp-openidconnect) | ASP.NET MVC app with Microsoft.Identity.Web.OWIN | +| Key Files | `App_Start/Startup.Auth.cs`, `Controllers/HomeController.cs` | + +**Quick start:** +```bash +git clone https://github.com/Azure-Samples/ms-identity-aspnet-webapp-openidconnect +cd ms-identity-aspnet-webapp-openidconnect +# Update Web.config with your Azure AD app registration +# Run in Visual Studio +``` + +--- + +## Best Practices + +### βœ… Do's + +**1. Use distributed cache in production:** +```csharp +// βœ… Production +services.AddDistributedTokenCaches(cacheServices => +{ + cacheServices.AddDistributedSqlServerCache(options => + { + options.ConnectionString = ConfigurationManager.ConnectionStrings["TokenCache"].ConnectionString; + options.SchemaName = "dbo"; + options.TableName = "TokenCache"; + options.DefaultSlidingExpiration = TimeSpan.FromMinutes(90); + }); +}); +``` + +**2. Handle incremental consent gracefully:** +```csharp +try +{ + var graphClient = this.GetGraphServiceClient(); + var user = await graphClient.Me.GetAsync(); +} +catch (MsalUiRequiredException) +{ + // User needs to consent to additional scopes + return new ChallengeResult(); +} +``` + +**3. Use correlation IDs for troubleshooting:** +```csharp +var downstreamApi = this.GetDownstreamApi(); +var correlationId = Guid.NewGuid(); + +var result = await downstreamApi.GetForUserAsync( + "TodoListService", + options => + { + options.RelativePath = $"api/todolist/{id}"; + options.TokenAcquisitionOptions = new TokenAcquisitionOptions + { + CorrelationId = correlationId + }; + }); +``` + +**4. Implement proper error handling:** +```csharp +try +{ + // Call API +} +catch (MsalUiRequiredException) +{ + return new ChallengeResult(); +} +catch (HttpRequestException ex) +{ + logger.Error($"API call failed: {ex.Message}"); + return View("Error"); +} +``` + +### ❌ Don'ts + +**1. Don't use in-memory cache for web farms:** +```csharp +// ❌ Wrong for load-balanced scenarios +services.AddDistributedTokenCaches(cacheServices => +{ + cacheServices.AddDistributedMemoryCache(); +}); + +// βœ… Correct +services.AddDistributedTokenCaches(cacheServices => +{ + cacheServices.AddDistributedSqlServerCache(/* ... */); +}); +``` + +**2. Don't hardcode configuration:** +```csharp +// ❌ Wrong +ClientId = "your-client-id-here" + +// βœ… Correct +ClientId = ConfigurationManager.AppSettings["AzureAd:ClientId"] +``` + +**3. Don't ignore token expiration:** +```csharp +// βœ… Microsoft.Identity.Web.OWIN handles this automatically +// No manual token refresh needed! +``` + +--- + +## Troubleshooting + +### Common Issues + +**Issue 1: "Cannot find IAuthorizationHeaderProvider"** + +**Solution:** Ensure `OwinTokenAcquirerFactory` is registered in `Startup.Auth.cs`: +```csharp +var tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance(); +tokenAcquirerFactory.Build(serviceProvider); +app.Use(tokenAcquirerFactory); +``` + +**Issue 2: "Cannot find GraphServiceClient"** + +**Solution:** Add `AddMicrosoftGraph()` in `Startup.Auth.cs`: +```csharp +services.AddMicrosoftGraph(); +``` + +**Issue 3: Token cache not persisting** + +**Solution:** Verify distributed cache configuration: +```csharp +services.AddDistributedTokenCaches(cacheServices => +{ + cacheServices.AddDistributedSqlServerCache(options => + { + // Ensure connection string is correct + options.ConnectionString = ConfigurationManager.ConnectionStrings["TokenCache"].ConnectionString; + }); +}); +``` + +--- + +## See Also + +- **[MSAL.NET with Microsoft.Identity.Web](msal-dotnet-framework.md)** - For console apps and daemon services +- **[ASP.NET Framework Overview](aspnet-framework.md)** - Choose the right package for your scenario +- **[Authorization Guide](../authentication/authorization.md)** - Scope validation and authorization policies +- **[Customization Guide](../advanced/customization.md)** - Configure OWIN authentication options +- **[Logging & Diagnostics](../advanced/logging.md)** - Troubleshoot OWIN authentication issues + +--- + +## Additional Resources + +- [Microsoft.Identity.Web.OWIN on GitHub](https://github.com/AzureAD/microsoft-identity-web) +- [OWIN Integration Wiki](https://github.com/AzureAD/microsoft-identity-web/wiki/OWIN) +- [Sample: ASP.NET MVC with OWIN](https://github.com/Azure-Samples/ms-identity-aspnet-webapp-openidconnect) +- [Token Cache Serialization](../authentication/token-cache/token-cache-README.md) + +--- + +**Supported Frameworks:** ASP.NET MVC, ASP.NET Web API (.NET Framework 4.7.2+) diff --git a/docs/getting-started/daemon-app.md b/docs/getting-started/daemon-app.md new file mode 100644 index 000000000..b018f16b2 --- /dev/null +++ b/docs/getting-started/daemon-app.md @@ -0,0 +1,994 @@ +# Daemon Applications & Agent Identities with Microsoft.Identity.Web + +This guide explains how to build daemon applications, background services, and autonomous agents using Microsoft.Identity.Web. These applications run without user interaction and authenticate using **application identity** (client credentials) or **agent identities**. + +## Overview + +Microsoft.Identity.Web supports three types of non-interactive applications: + +| **Scenario** | **Authentication Type** | **Token Type** | **Use Case** | +|-------------|------------------------|----------------|-------------| +| **Standard Daemon** | Client credentials (secret/certificate) | App-only access token | Background services, scheduled jobs, data processing | +| **Autonomous Agent** | Agent identity with client credentials | App-only access token for agent | Copilot agents, autonomous services acting on behalf of an Agent identity. (Usually in a protected Web API) | +| **Agent User Identity** | Agent user identity | Agent user identity with client credentials | Autonomous services acting on behalf of an Agent user identity. (Usually in a protected Web API) | + +## Table of Contents + +- [Quick Start](#quick-start) +- [Standard Daemon Applications](#standard-daemon-applications) +- [Autonomous Agents (Agent Identity)](#autonomous-agents-agent-identity) +- [Agent User Identity](#agent-user-identity) +- [Service Configuration](#service-configuration) +- [Calling APIs](#calling-apis) +- [Token Caching](#token-caching) +- [Azure Samples](#azure-samples) +- [Troubleshooting](#troubleshooting) + +--- + +## Quick Start + +### Prerequisites + +- .NET 8.0 or later +- Azure AD app registration with **client credentials** (client secret or certificate) +- For agent scenarios: Agent identities configured in your Azure AD tenant + +### Installation + +```bash +dotnet add package Microsoft.Identity.Web +dotnet add package Microsoft.Extensions.Hosting +``` + +### Two Configuration Approaches + +Microsoft.Identity.Web provides two ways to configure daemon applications: + +#### Option 1: TokenAcquirerFactory (Recommended for Simple Scenarios) + +**Best for:** Quick prototypes, console apps, testing, and simple daemon services. + +```csharp +using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Web; + +// Get the token acquirer factory instance +var tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance(); + +// Configure downstream API and Microsoft Graph (optional) +tokenAcquirerFactory.Services.AddDownstreamApis( + tokenAcquirerFactory.Configuration.GetSection("DownstreamApis")) + .AddMicrosoftGraph(); + +var serviceProvider = tokenAcquirerFactory.Build(); + +// Call Microsoft Graph +var graphClient = serviceProvider.GetRequiredService(); +var users = await graphClient.Users.GetAsync(); +``` + +**Advantages:** +- βœ… Minimal boilerplate code +- βœ… Automatically loads `appsettings.json` +- βœ… Perfect for simple scenarios +- βœ… One-line initialization +- ❌ Not suitable for tests running in parallel (singleton) + +#### Option 2: Full ServiceCollection (Recommended for Production) + +**Best for:** Production applications, complex scenarios, dependency injection, testability. + +```csharp +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Identity.Web; + +var host = Host.CreateDefaultBuilder(args) + .ConfigureServices((context, services) => + { + // Configure authentication + services.Configure( + context.Configuration.GetSection("AzureAd")); + + // Add token acquisition (true = singleton lifetime) + services.AddTokenAcquisition(true); + + // Add token cache (in-memory for development) + services.AddInMemoryTokenCaches(); + + // Add HTTP client for API calls + services.AddHttpClient(); + + // Add Microsoft Graph (optional) + services.AddMicrosoftGraph(); + + // Add your background service + services.AddHostedService(); + }) + .Build(); + +await host.RunAsync(); +``` + +**Advantages:** +- βœ… Full control over configuration providers +- βœ… Better testability with constructor injection +- βœ… Integrates with ASP.NET Core hosting model +- βœ… Supports complex scenarios (multiple auth schemes) +- βœ… Production-ready architecture +- βœ… Required for tests running on paralell + + +**Note:** The parameter `true` in `AddTokenAcquisition(true)` means the service is registered as a **singleton** (single instance for the app lifetime). Use `false` for scoped lifetime in web applications. + +> **πŸ’‘ Recommendation:** Start with `TokenAcquirerFactory` for prototypes and tests. Migrate to the full `ServiceCollection` pattern when building production applications or in tests. + +--- + +## Standard Daemon Applications + +Standard daemon applications authenticate using **client credentials** (client secret or certificate) and obtain **app-only access tokens** to call APIs. + +### Configuration + +**appsettings.json:** + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + + "ClientSecret": "your-client-secret", + + "ClientCredentials": [ + // Option 1: Client Secret + { + "SourceType": "ClientSecret", + "ClientSecret": "your-client-secret", + }, + // Option 2: Certificate (recommended for production) + { + "SourceType": "StoreWithDistinguishedName", + "CertificateStorePath": "CurrentUser/My", + "CertificateDistinguishedName": "CN=DaemonAppCert" + } + // More options: https://aka.ms/ms-id-web/client-credentials + ] + } +} +``` + +**Important:** Set your `appsettings.json` to copy to output directory: + +```xml + + + PreserveNewest + + +``` + +This is done automatically in ASP.NET Core applications, but not for daemon apps (or OWIN) + +### Service Configuration (Recommended Pattern) + +**Program.cs:** + +```csharp +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Identity.Web; + +var host = Host.CreateDefaultBuilder(args) + .ConfigureServices((context, services) => + { + IConfiguration configuration = context.Configuration; + + // Configure Microsoft Identity options + services.Configure( + configuration.GetSection("AzureAd")); + + // Add token acquisition (true = singleton) + services.AddTokenAcquisition(true); + + // Add token cache + services.AddInMemoryTokenCaches(); // For development + // services.AddDistributedTokenCaches(); // For production + + // Add HTTP client + services.AddHttpClient(); + + // Add Microsoft Graph SDK (optional) + services.AddMicrosoftGraph(); + + // Add your background service + services.AddHostedService(); + }) + .Build(); + +await host.RunAsync(); +``` + +### Calling Microsoft Graph + +**DaemonWorker.cs:** + +```csharp +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Graph; +using Microsoft.Identity.Abstractions; + +public class DaemonWorker : BackgroundService +{ + private readonly GraphServiceClient _graphClient; + private readonly ILogger _logger; + + public DaemonWorker( + GraphServiceClient graphClient, + ILogger logger) + { + _graphClient = graphClient; + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + try + { + // Call Microsoft Graph with app-only permissions + var users = await _graphClient.Users + .GetAsync(cancellationToken: stoppingToken); + + _logger.LogInformation($"Found {users?.Value?.Count} users"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error calling Microsoft Graph"); + } + + await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken); + } + } +} +``` + +### Using IAuthorizationHeaderProvider + +For more control over HTTP calls: + +```csharp +using Microsoft.Identity.Abstractions; + +public class DaemonService +{ + private readonly IAuthorizationHeaderProvider _authProvider; + private readonly HttpClient _httpClient; + + public DaemonService( + IAuthorizationHeaderProvider authProvider, + IHttpClientFactory httpClientFactory) + { + _authProvider = authProvider; + _httpClient = httpClientFactory.CreateClient(); + } + + public async Task CallApiAsync() + { + // Get authorization header for app-only access + string authHeader = await _authProvider + .CreateAuthorizationHeaderForAppAsync( + scopes: "https://graph.microsoft.com/.default"); + + // Add to HTTP request + _httpClient.DefaultRequestHeaders.Clear(); + _httpClient.DefaultRequestHeaders.Add("Authorization", authHeader); + + var response = await _httpClient.GetStringAsync( + "https://graph.microsoft.com/v1.0/users"); + + return response; + } +} +``` + +See also [Calling downstream APIs](../calling-downstream-apis/calling-downstream-apis-README.md) to learn about all the ways +Microsoft Identity Web proposes to call downstream APIs. + +--- + +## Autonomous Agents (Agent Identity) + +**Autonomous agents** use **agent identities** to obtain app-only tokens. This is useful for Copilot scenarios, autonomous services. + +⚠️ Microsoft recommends that agents calling downstream APIs happens in protected web APIs even if these autonomous agents will acquire an app token + + +### Configuration + +```csharp +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Configuration; +using Microsoft.Identity.Web; + +var services = new ServiceCollection(); + +// Configuration +var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["AzureAd:Instance"] = "https://login.microsoftonline.com/", + ["AzureAd:TenantId"] = "your-tenant-id", + ["AzureAd:ClientId"] = "your-agent-app-client-id", + ["AzureAd:ClientCredentials:0:SourceType"] = "StoreWithDistinguishedName", + ["AzureAd:ClientCredentials:0:CertificateStorePath"] = "CurrentUser/My", + ["AzureAd:ClientCredentials:0:CertificateDistinguishedName"] = "CN=YourCert" + }) + .Build(); + +services.AddSingleton(configuration); + +// Configure Microsoft Identity +services.Configure( + configuration.GetSection("AzureAd")); + +services.AddTokenAcquisition(true); +services.AddInMemoryTokenCaches(); +services.AddHttpClient(); +services.AddMicrosoftGraph(); + +// Add agent identities support +services.AddAgentIdentities(); + +var serviceProvider = services.BuildServiceProvider(); +``` + +### Acquiring Tokens with Agent Identity + +```csharp +using Microsoft.Identity.Abstractions; +using Microsoft.Graph; + +// Your agent identity GUID +string agentIdentityId = "d84da24a-2ea2-42b8-b5ab-8637ec208024"; + +// Option 1: Using IAuthorizationHeaderProvider +IAuthorizationHeaderProvider authProvider = + serviceProvider.GetRequiredService(); + +var options = new AuthorizationHeaderProviderOptions() + .WithAgentIdentity(agentIdentityId); + +string authHeader = await authProvider.CreateAuthorizationHeaderForAppAsync( + scopes: "https://graph.microsoft.com/.default", + options); + +// Option 2: Using Microsoft Graph SDK +GraphServiceClient graphClient = + serviceProvider.GetRequiredService(); + +var applications = await graphClient.Applications.GetAsync(request => +{ + request.Options.WithAuthenticationOptions(authOptions => + { + authOptions.WithAgentIdentity(agentIdentityId); + }); +}); +``` + +### Complete Autonomous Agent Example + +```csharp +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Configuration; +using Microsoft.Graph; +using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Web; + +public class AutonomousAgentService +{ + private readonly GraphServiceClient _graphClient; + private readonly IAuthorizationHeaderProvider _authProvider; + private readonly string _agentIdentityId; + + public AutonomousAgentService( + string agentIdentityId, + IServiceProvider serviceProvider) + { + _agentIdentityId = agentIdentityId; + _graphClient = serviceProvider.GetRequiredService(); + _authProvider = serviceProvider.GetRequiredService(); + } + + public async Task GetAuthorizationHeaderAsync() + { + var options = new AuthorizationHeaderProviderOptions() + .WithAgentIdentity(_agentIdentityId); + + return await _authProvider.CreateAuthorizationHeaderForAppAsync( + "https://graph.microsoft.com/.default", + options); + } + + public async Task> ListApplicationsAsync() + { + var apps = await _graphClient.Applications.GetAsync(request => + { + request.Options.WithAuthenticationOptions(options => + { + options.WithAgentIdentity(_agentIdentityId); + }); + }); + + return apps?.Value ?? Enumerable.Empty(); + } +} +``` + +--- + +## Agent User Identity + +**Agent user identity** allows agents to act **on behalf of an agent user** with delegated permissions. This is for agents having their mailbox, etc ... + +### Prerequisites + +- Agent blueprint registered in Azure AD +- Agent identity created and linked to the agent application +- Agent user identity associated with the agent identity + +### Configuration + +```csharp +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Identity.Web; +using System.Security.Cryptography.X509Certificates; + +var services = new ServiceCollection(); + +// Configure agent application +services.Configure(options => +{ + options.Instance = "https://login.microsoftonline.com/"; + options.TenantId = "your-tenant-id"; + options.ClientId = "your-agent-app-client-id"; + + // Use certificate for agent authentication + options.ClientCredentials = new[] + { + CertificateDescription.FromStoreWithDistinguishedName( + "CN=YourCertificate", + StoreLocation.CurrentUser, + StoreName.My) + }; +}); + +// Add services (true = singleton) +services.AddSingleton(new ConfigurationBuilder().Build()); +services.AddTokenAcquisition(true); +services.AddInMemoryTokenCaches(); +services.AddHttpClient(); +services.AddMicrosoftGraph(); +services.AddAgentIdentities(); + +var serviceProvider = services.BuildServiceProvider(); +``` + +### Acquiring User Tokens with Agent Identity + +#### By Username (UPN) + +```csharp +using Microsoft.Identity.Abstractions; +using Microsoft.Graph; + +string agentIdentityId = "your-agent-identity-id"; +string userUpn = "user@yourtenant.onmicrosoft.com"; + +// Get authorization header +IAuthorizationHeaderProvider authProvider = + serviceProvider.GetRequiredService(); + +var options = new AuthorizationHeaderProviderOptions() + .WithAgentUserIdentity( + agentApplicationId: agentIdentityId, + username: userUpn); + +string authHeader = await authProvider.CreateAuthorizationHeaderForUserAsync( + scopes: new[] { "https://graph.microsoft.com/.default" }, + options); + +// Or use Microsoft Graph SDK +GraphServiceClient graphClient = + serviceProvider.GetRequiredService(); + +var me = await graphClient.Me.GetAsync(request => +{ + request.Options.WithAuthenticationOptions(options => + options.WithAgentUserIdentity(agentIdentityId, userUpn)); +}); +``` + +#### By User Object ID + +```csharp +string agentIdentityId = "your-agent-identity-id"; +Guid userObjectId = Guid.Parse("user-object-id"); + +var options = new AuthorizationHeaderProviderOptions() + .WithAgentUserIdentity( + agentApplicationId: agentIdentityId, + userId: userObjectId); + +string authHeader = await authProvider.CreateAuthorizationHeaderForUserAsync( + scopes: new[] { "https://graph.microsoft.com/.default" }, + options); + +// With Graph SDK +var me = await graphClient.Me.GetAsync(request => +{ + request.Options.WithAuthenticationOptions(options => + options.WithAgentUserIdentity(agentIdentityId, userObjectId)); +}); +``` + +### Token Caching with ClaimsPrincipal + +For better performance, cache user tokens using `ClaimsPrincipal`: + +```csharp +using System.Security.Claims; +using Microsoft.Identity.Abstractions; + +// First call - creates cache entry +ClaimsPrincipal userPrincipal = new ClaimsPrincipal(); + +string authHeader = await authProvider.CreateAuthorizationHeaderForUserAsync( + scopes: new[] { "https://graph.microsoft.com/.default" }, + options, + userPrincipal); + +// ClaimsPrincipal now has uid and utid claims for caching +bool hasUserId = userPrincipal.HasClaim(c => c.Type == "uid"); +bool hasTenantId = userPrincipal.HasClaim(c => c.Type == "utid"); + +// Subsequent calls - uses cache +authHeader = await authProvider.CreateAuthorizationHeaderForUserAsync( + scopes: new[] { "https://graph.microsoft.com/.default" }, + options, + userPrincipal); // Reuse the same principal +``` + +### Tenant Override + +For multi-tenant scenarios, override the tenant at runtime: + +```csharp +var options = new AuthorizationHeaderProviderOptions() + .WithAgentUserIdentity(agentIdentityId, userUpn); + +// Override tenant (useful when app is configured with "common") +options.AcquireTokenOptions.Tenant = "specific-tenant-id"; + +string authHeader = await authProvider.CreateAuthorizationHeaderForUserAsync( + scopes: new[] { "https://graph.microsoft.com/.default" }, + options); + +// With Graph SDK +var me = await graphClient.Me.GetAsync(request => +{ + request.Options.WithAuthenticationOptions(options => + { + options.WithAgentUserIdentity(agentIdentityId, userUpn); + options.AcquireTokenOptions.Tenant = "specific-tenant-id"; + }); +}); +``` + +### Complete Agent User Identity Example + +```csharp +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Graph; +using Microsoft.Identity.Abstractions; +using System.Security.Claims; + +public class AgentUserService +{ + private readonly IAuthorizationHeaderProvider _authProvider; + private readonly GraphServiceClient _graphClient; + private readonly string _agentIdentityId; + + public AgentUserService( + string agentIdentityId, + IServiceProvider serviceProvider) + { + _agentIdentityId = agentIdentityId; + _authProvider = serviceProvider.GetRequiredService(); + _graphClient = serviceProvider.GetRequiredService(); + } + + public async Task GetUserProfileAsync(string userUpn) + { + var me = await _graphClient.Me.GetAsync(request => + { + request.Options.WithAuthenticationOptions(options => + options.WithAgentUserIdentity(_agentIdentityId, userUpn)); + }); + + return me!; + } + + public async Task GetUserProfileByIdAsync(Guid userObjectId) + { + var me = await _graphClient.Me.GetAsync(request => + { + request.Options.WithAuthenticationOptions(options => + options.WithAgentUserIdentity(_agentIdentityId, userObjectId)); + }); + + return me!; + } + + public async Task GetAuthHeaderForUserAsync( + string userUpn, + ClaimsPrincipal? cachedPrincipal = null) + { + var options = new AuthorizationHeaderProviderOptions() + .WithAgentUserIdentity(_agentIdentityId, userUpn); + + return await _authProvider.CreateAuthorizationHeaderForUserAsync( + scopes: new[] { "https://graph.microsoft.com/.default" }, + options, + cachedPrincipal ?? new ClaimsPrincipal()); + } +} +``` + +--- + +## Service Configuration + +### Extension Method Pattern (Recommended) + +Create a reusable extension method for consistent configuration: + +```csharp +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Identity.Web; +using Microsoft.Identity.Web.TokenCacheProviders.InMemory; + +public static class ServiceCollectionExtensions +{ + public static IServiceProvider ConfigureServicesForAgentIdentities( + this IServiceCollection services, + IConfiguration configuration) + { + // Add configuration + services.AddSingleton(configuration); + + // Configure Microsoft Identity options + services.Configure( + configuration.GetSection("AzureAd")); + + services.AddTokenAcquisition(true); + + // Add token caching + services.AddInMemoryTokenCaches(); + + // Add HTTP client + services.AddHttpClient(); + + // Add Microsoft Graph (optional) + services.AddMicrosoftGraph(); + + // Add agent identities support + services.AddAgentIdentities(); + + return services.BuildServiceProvider(); + } +} +``` + +### Usage + +```csharp +var services = new ServiceCollection(); +var configuration = new ConfigurationBuilder() + .AddJsonFile("appsettings.json") + .Build(); + +var serviceProvider = services.ConfigureServicesForAgentIdentities(configuration); +``` + +--- + +## Calling APIs + +### Microsoft Graph + +```csharp +using Microsoft.Graph; + +GraphServiceClient graphClient = + serviceProvider.GetRequiredService(); + +// Standard daemon (app-only) +var users = await graphClient.Users.GetAsync(); + +// Autonomous agent (app-only with agent identity) +var apps = await graphClient.Applications.GetAsync(request => +{ + request.Options.WithAuthenticationOptions(options => + { + options.WithAgentIdentity("agent-identity-id"); + options.RequestAppToken = true; + }); +}); + +// Agent user identity (delegated with user context) +var me = await graphClient.Me.GetAsync(request => +{ + request.Options.WithAuthenticationOptions(options => + options.WithAgentUserIdentity("agent-identity-id", "user@tenant.com")); +}); +``` + +### Custom APIs with IDownstreamApi + +```csharp +using Microsoft.Identity.Abstractions; + +IDownstreamApi downstreamApi = + serviceProvider.GetRequiredService(); + +// Standard daemon +var result = await downstreamApi.GetForAppAsync( + serviceName: "MyApi", + options => options.RelativePath = "api/data"); + +// With agent identity +var result = await downstreamApi.GetForAppAsync( + serviceName: "MyApi", + options => + { + options.RelativePath = "api/data"; + options.WithAgentIdentity("agent-identity-id"); + }); + +// Agent user identity +var result = await downstreamApi.GetForUserAsync( + serviceName: "MyApi", + options => + { + options.RelativePath = "api/data"; + options.WithAgentUserIdentity("agent-identity-id", "user@tenant.com"); + }); +``` + +### Manual HTTP Calls + +```csharp +using Microsoft.Identity.Abstractions; + +IAuthorizationHeaderProvider authProvider = + serviceProvider.GetRequiredService(); + +HttpClient httpClient = new HttpClient(); + +// Standard daemon +string authHeader = await authProvider.CreateAuthorizationHeaderForAppAsync( + "https://graph.microsoft.com/.default"); + +httpClient.DefaultRequestHeaders.Add("Authorization", authHeader); +var response = await httpClient.GetStringAsync("https://graph.microsoft.com/v1.0/users"); + +// With agent identity +var options = new AuthorizationHeaderProviderOptions() + .WithAgentIdentity("agent-identity-id"); + +authHeader = await authProvider.CreateAuthorizationHeaderForAppAsync( + "https://graph.microsoft.com/.default", + options); + +// Agent user identity +var userOptions = new AuthorizationHeaderProviderOptions() + .WithAgentUserIdentity("agent-identity-id", "user@tenant.com"); + +authHeader = await authProvider.CreateAuthorizationHeaderForUserAsync( + new[] { "https://graph.microsoft.com/.default" }, + userOptions); +``` + +--- + +## Token Caching + +### Development: In-Memory Cache + +```csharp +services.AddInMemoryTokenCaches(); +``` + +### Production: Distributed Cache + +#### SQL Server + +```csharp +services.AddDistributedSqlServerCache(options => +{ + options.ConnectionString = configuration["ConnectionStrings:TokenCache"]; + options.SchemaName = "dbo"; + options.TableName = "TokenCache"; +}); +services.AddDistributedTokenCaches(); +``` + +#### Redis + +```csharp +services.AddStackExchangeRedisCache(options => +{ + options.Configuration = configuration["Redis:ConnectionString"]; + options.InstanceName = "TokenCache_"; +}); +services.AddDistributedTokenCaches(); +``` + +#### Cosmos DB + +```csharp +services.AddCosmosDbTokenCaches(options => +{ + options.CosmosDbConnectionString = configuration["CosmosDb:ConnectionString"]; + options.DatabaseId = "TokenCache"; + options.ContainerId = "Tokens"; +}); +``` + +**Learn more:** [Token Cache Configuration](../authentication/token-cache/token-cache-README.md) + +--- + +## Azure Samples + +Microsoft provides comprehensive samples demonstrating daemon app patterns: + +### Sample Repository + +**[active-directory-dotnetcore-daemon-v2](https://github.com/Azure-Samples/active-directory-dotnetcore-daemon-v2)** + +This repository contains multiple scenarios: + +| **Sample** | **Description** | **Link** | +|-----------|----------------|----------| +| **1-Call-MSGraph** | Basic daemon calling Microsoft Graph with client credentials | [View Sample](https://github.com/Azure-Samples/active-directory-dotnetcore-daemon-v2/tree/master/1-Call-MSGraph) | +| **2-Call-OwnApi** | Daemon calling your own protected web API | [View Sample](https://github.com/Azure-Samples/active-directory-dotnetcore-daemon-v2/tree/master/2-Call-OwnApi) | +| **3-Using-KeyVault** | Daemon using Azure Key Vault for certificate storage | [View Sample](https://github.com/Azure-Samples/active-directory-dotnetcore-daemon-v2/tree/master/3-Using-KeyVault) | +| **4-Multi-Tenant** | Multi-tenant daemon application | [View Sample](https://github.com/Azure-Samples/active-directory-dotnetcore-daemon-v2/tree/master/4-Multi-Tenant) | +| **5-Call-MSGraph-ManagedIdentity** | Daemon using Managed Identity on Azure | [View Sample](https://github.com/Azure-Samples/active-directory-dotnetcore-daemon-v2/tree/master/5-Call-MSGraph-ManagedIdentity) | + +### Key Differences from Samples + +The Azure samples use **`TokenAcquirerFactory.GetDefaultInstance()`** for simplicityβ€”this is the recommended approach for **simple console apps, prototypes, and tests**. This guide shows both patterns: + +**TokenAcquirerFactory Pattern (Azure Samples):** +```csharp +// Simple, perfect for prototypes and tests +var tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance(); +tokenAcquirerFactory.Services.AddDownstreamApi("MyApi", ...); +var serviceProvider = tokenAcquirerFactory.Build(); +``` + +**Full ServiceCollection Pattern (Production Apps):** +```csharp +// More control, testable, follows DI best practices +var services = new ServiceCollection(); +services.AddTokenAcquisition(true); // true = singleton +services.Configure(...); +var serviceProvider = services.BuildServiceProvider(); +``` + +**When to use which:** +- **Use `TokenAcquirerFactory`** for: Console apps, quick prototypes, unit tests, simple daemon services +- **Use `ServiceCollection`** for: Production applications, ASP.NET Core integration, complex DI scenarios, background services with `IHostedService` + +Both approaches are fully supported and production-ready. Choose based on your application's complexity and integration needs. + +--- + +## Troubleshooting + +### AADSTS700016: Application not found + +**Cause:** Invalid `ClientId` or application not registered in the tenant. + +**Solution:** Verify the `ClientId` in your configuration matches your Azure AD app registration. + +### AADSTS7000215: Invalid client secret + +**Cause:** Client secret is incorrect, expired, or not configured. + +**Solution:** +- Verify the secret in Azure portal matches your configuration +- Check secret expiration date +- Consider using certificates for production + +### AADSTS700027: Client assertion contains invalid signature + +**Cause:** Certificate not found, expired, or private key not accessible. + +**Solution:** +- Verify certificate is installed in correct certificate store +- Check certificate distinguished name matches configuration +- Ensure application has permission to read private key +- See [Certificate Configuration Guide](../frameworks/msal-dotnet-framework.md#certificate-loading) + +### AADSTS650052: The app needs access to a service + +**Cause:** Required API permissions not granted or admin consent missing. + +**Solution:** +1. Navigate to Azure portal β†’ App registrations β†’ Your app β†’ API permissions +2. Add required permissions (e.g., `User.Read.All` for Microsoft Graph) +3. Click "Grant admin consent" button + +### Agent Identity Errors + +#### AADSTS50105: The signed in user is not assigned to a role + +**Cause:** Agent identity not properly configured or not assigned to the application. + +**Solution:** +- Verify agent identity exists in Azure AD +- Ensure agent identity is linked to your application +- Check that agent identity has required permissions + +#### Tokens acquired but with wrong permissions + +**Cause:** Using agent user identity but requesting app permissions, or vice versa. + +**Solution:** +- For **app-only tokens**: Use `CreateAuthorizationHeaderForAppAsync` with `WithAgentIdentity` +- For **delegated tokens**: Use `CreateAuthorizationHeaderForUserAsync` with `WithAgentUserIdentity` +- Ensure API permissions match token type (application vs. delegated) + +### Token Caching Issues + +**Problem:** Tokens not cached, forcing new acquisition each time. + +**Solution:** +- For agent user identity: Reuse the same `ClaimsPrincipal` instance across calls +- Verify distributed cache connection (if using Redis/SQL) +- Enable debug logging to see cache operations + +**Detailed diagnostics:** [Logging & Diagnostics Guide](../advanced/logging.md) + +--- + +## See Also + +- **[Calling Downstream APIs from Web APIs](../calling-downstream-apis/from-web-apis.md)** - OBO patterns +- **[MSAL.NET Framework Guide](../frameworks/msal-dotnet-framework.md)** - Token cache and certificate configuration for .NET Framework +- **[Certificate Configuration](../authentication/credentials/credentials-README.md)** - Loading certificates from KeyVault, store, file, Base64 +- **[Token Cache Configuration](../authentication/token-cache/token-cache-README.md)** - Production caching strategies +- **[Logging & Diagnostics](../advanced/logging.md)** - Troubleshooting token acquisition issues +- **[Customization Guide](../advanced/customization.md)** - Advanced configuration patterns + +--- + +## Additional Resources + +- [Microsoft identity platform daemon app documentation](https://learn.microsoft.com/azure/active-directory/develop/scenario-daemon-overview) +- [Azure Samples: Daemon Applications](https://github.com/Azure-Samples/active-directory-dotnetcore-daemon-v2) +- [Microsoft.Identity.Web NuGet Package](https://www.nuget.org/packages/Microsoft.Identity.Web) +- [Microsoft.Identity.Abstractions API Reference](https://learn.microsoft.com/dotnet/api/microsoft.identity.abstractions) + +--- diff --git a/docs/getting-started/packages.md b/docs/getting-started/packages.md new file mode 100644 index 000000000..c9db898b3 --- /dev/null +++ b/docs/getting-started/packages.md @@ -0,0 +1,125 @@ +# Microsoft.Identity.Web NuGet Packages + +Microsoft.Identity.Web is a set of libraries that simplifies adding authentication and authorization support to applications integrating with the Microsoft identity platform. This page provides an overview of all NuGet packages produced by this project. + +## πŸ“¦ Package Overview + +### Core Packages + +These packages provide the fundamental functionality for authentication and token management. + +| Package | Description | +|---------|-------------| +| **Microsoft.Identity.Web** | The main package that enables ASP.NET Core web apps and web APIs to use the Microsoft identity platform. Used for web applications that sign in users and protected web APIs that optionally call downstream web APIs. | +| **Microsoft.Identity.Web.UI** | Provides UI components for ASP.NET Core web apps that use Microsoft.Identity.Web, including sign-in/sign-out controllers and views. | +| **Microsoft.Identity.Web.TokenAcquisition** | Implementation for higher-level API for confidential client applications (ASP.NET Core and SDK/.NET). Handles token acquisition and management. | +| **Microsoft.Identity.Web.TokenCache** | Provides token cache serializers for MSAL.NET confidential client applications. Supports in-memory, distributed, and session-based caching. | + +### Credential Management Packages + +These packages handle different authentication credential types. + +| Package | Description | +|---------|-------------| +| **Microsoft.Identity.Web.Certificate** | Provides certificate management capabilities for MSAL.NET, including loading certificates from Azure Key Vault and local stores. | +| **Microsoft.Identity.Web.Certificateless** | Enables certificateless authentication scenarios such as managed identities and workload identity federation. | + +### Downstream API & Integration Packages + +These packages help you call protected APIs and integrate with Azure services. + +| Package | Description | +|---------|-------------| +| **Microsoft.Identity.Web.DownstreamApi** | Provides a higher-level interface for calling downstream protected APIs from confidential client applications with automatic token management. | +| **Microsoft.Identity.Web.Azure** | Enables ASP.NET Core web apps and web APIs to use the Azure SDKs with the Microsoft identity platform, providing `TokenCredential` implementations. | +| **Microsoft.Identity.Web.OWIN** | Enables ASP.NET web apps (OWIN/Katana) and web APIs on .NET Framework to use the Microsoft identity platform. Specifically for web applications that sign in users and protected web APIs that optionally call downstream web APIs. | + +### Microsoft Graph Packages + +These packages provide integration with Microsoft Graph for calling Microsoft 365 services. + +| Package | Description | +|---------|-------------| +| **Microsoft.Identity.Web.MicrosoftGraph** | Enables web applications and web APIs to call Microsoft Graph using the Microsoft Graph SDK v4. For web apps that sign in users and call Microsoft Graph, and protected web APIs that call Microsoft Graph. | +| **Microsoft.Identity.Web.MicrosoftGraphBeta** | Enables web applications and web APIs to call Microsoft Graph Beta using the Microsoft Graph SDK v4. For accessing preview features not yet available in the production Graph API. | +| **Microsoft.Identity.Web.GraphServiceClient** | Enables web applications and web APIs to call Microsoft Graph using the Microsoft Graph SDK v5 and above. Recommended for new projects using the latest Graph SDK. | +| **Microsoft.Identity.Web.GraphServiceClientBeta** | Enables web applications and web APIs to call Microsoft Graph Beta using the Microsoft Graph SDK v5 and above. For accessing preview features with the latest Graph SDK. | + +### Advanced Scenarios Packages + +These packages support specialized authentication scenarios. + +| Package | Description | +|---------|-------------| +| **Microsoft.Identity.Web.Diagnostics** | Provides diagnostic and logging support for troubleshooting authentication issues in Microsoft.Identity.Web. | +| **Microsoft.Identity.Web.OidcFIC** | Implementation for Cloud Federation Identity Credential (FIC) credential provider. Enables cross-cloud authentication scenarios. | +| **Microsoft.Identity.Web.AgentIdentities** | Helper methods for Agent identity blueprint to act as agent identities. Enables building autonomous agents and copilot scenarios. | + +## 🎯 Choosing the Right Package + +### For Web Applications (Sign in users) + +```bash +dotnet add package Microsoft.Identity.Web +dotnet add package Microsoft.Identity.Web.UI +``` + +### For Protected Web APIs + +```bash +dotnet add package Microsoft.Identity.Web +``` + +### For Daemon Applications / Background Services + +```bash +dotnet add package Microsoft.Identity.Web.TokenAcquisition +``` + +### For Calling Microsoft Graph + +**Using Graph SDK v5+ (Recommended):** +```bash +dotnet add package Microsoft.Identity.Web.GraphServiceClient +``` + +**Using Graph SDK v4:** +```bash +dotnet add package Microsoft.Identity.Web.MicrosoftGraph +``` + +### For Using Azure SDKs + +```bash +dotnet add package Microsoft.Identity.Web.Azure +``` + +### For Calling Custom Downstream APIs + +```bash +dotnet add package Microsoft.Identity.Web.DownstreamApi +``` + +### For Agent/Copilot Scenarios + +```bash +dotnet add package Microsoft.Identity.Web.AgentIdentities +``` + +### For OWIN Applications (.NET Framework) + +```bash +dotnet add package Microsoft.Identity.Web.OWIN +``` + +## πŸ“š Related Documentation + +- [Quick Start: Sign in users in a Web App](./quickstart-webapp.md) +- [Quick Start: Protect a Web API](./quickstart-webapi.md) +- [Daemon Applications & Agent Identities](./daemon-app.md) +- [Calling Downstream APIs](../calling-downstream-apis/calling-downstream-apis-README.md) +- [Agent Identities Guide](../calling-downstream-apis/AgentIdentities-Readme.md) + +## πŸ”— NuGet Gallery + +All packages are available on [NuGet.org](https://www.nuget.org/packages?q=Microsoft.Identity.Web). diff --git a/docs/getting-started/quickstart-webapi.md b/docs/getting-started/quickstart-webapi.md new file mode 100644 index 000000000..fdb853d79 --- /dev/null +++ b/docs/getting-started/quickstart-webapi.md @@ -0,0 +1,377 @@ +# Quickstart: Protect an ASP.NET Core Web API + +This guide shows you how to protect a web API with Microsoft Entra ID (formerly Azure AD) using Microsoft.Identity.Web. + +**Time to complete:** ~10 minutes + +## Prerequisites + +- [.NET 9 SDK](https://dotnet.microsoft.com/download/dotnet/9.0) +- A Microsoft Entra ID tenant ([create a free account](https://azure.microsoft.com/free/?WT.mc_id=A261C142F)) +- An app registration for your API + +## Option 1: Create from Template (Fastest) + +### 1. Create the project + +```bash +dotnet new webapi --auth SingleOrg --name MyWebApi +cd MyWebApi +``` + +### 2. Configure app registration + +Update `appsettings.json` with your app registration details: + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-api-client-id" + } +} +``` + +### 3. Run the API + +```bash +dotnet run +``` + +Your API is now protected at `https://localhost:5001`. + +βœ… **Done!** Requests now require a valid access token. + +--- + +## Option 2: Add to Existing Web API + +### 1. Install NuGet package + +```bash +dotnet add package Microsoft.Identity.Web +``` + + +### 2. Configure authentication in `Program.cs` + +```csharp +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Identity.Web; + +var builder = WebApplication.CreateBuilder(args); + +// Add authentication +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(builder.Configuration, "AzureAd"); + +// Add authorization +builder.Services.AddAuthorization(); + +builder.Services.AddControllers(); + +var app = builder.Build(); + +app.UseHttpsRedirection(); + +app.UseAuthentication(); // ⭐ Add authentication middleware +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); +``` + +### 3. Add configuration to `appsettings.json` + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-api-client-id" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Identity.Web": "Information" + } + } +} +``` + +### 4. Protect your API endpoints + +**Require authentication for all endpoints:** + +```csharp +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +[Authorize] // ⭐ Require valid access token +[ApiController] +[Route("api/[controller]")] +public class WeatherForecastController : ControllerBase +{ + [HttpGet] + public IEnumerable Get() + { + // Access user information + var userId = User.FindFirst("oid")?.Value; + var userName = User.Identity?.Name; + + return Enumerable.Range(1, 5).Select(index => new WeatherForecast + { + Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = "Protected data" + }); + } +} +``` + +**Require specific scopes:** + +```csharp +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Identity.Web; + +[Authorize] +[ApiController] +[Route("api/[controller]")] +public class TodoController : ControllerBase +{ + [HttpGet] + [RequiredScope("access_as_user")] // ⭐ Require specific scope + public IActionResult GetAll() + { + return Ok(new[] { "Todo 1", "Todo 2" }); + } + + [HttpPost] + [RequiredScope("write")] // ⭐ Different scope for write operations + public IActionResult Create([FromBody] string item) + { + return Created("", item); + } +} +``` + +### 5. Run and test + +```bash +dotnet run +``` + +Test with a tool like Postman or curl: + +```bash +curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" https://localhost:5001/api/weatherforecast +``` + +βœ… **Success!** Your API now validates bearer tokens. + +--- + +## App Registration Setup + +### 1. Register your API + +1. Sign in to the [Azure portal](https://portal.azure.com) +2. Navigate to **Microsoft Entra ID** > **App registrations** > **New registration** +3. Enter a name (e.g., "My Web API") +4. Select **Single tenant** (most common for APIs) +5. No redirect URI needed for APIs +6. Click **Register** + +### 2. Expose an API scope + +1. In your API app registration, go to **Expose an API** +2. Click **Add a scope** +3. Accept the default Application ID URI or customize it (e.g., `api://your-api-client-id`) +4. Add a scope: + - **Scope name:** `access_as_user` + - **Who can consent:** Admins and users + - **Admin consent display name:** "Access My Web API" + - **Admin consent description:** "Allows the app to access the web API on behalf of the signed-in user" +5. Click **Add scope** + +### 3. Note the Application ID + +Copy the **Application (client) ID** - this is your `ClientId` in `appsettings.json`. + +--- + +## Create a Client App Registration (For Testing) + +To call your API, you need a client app: + +### 1. Register a client application + +1. In **Microsoft Entra ID** > **App registrations**, create another registration +2. Name it (e.g., "My API Client") +3. Select account types +4. Add redirect URI: `https://localhost:7000/signin-oidc` (if it's a web app) +5. Click **Register** + +### 2. Grant API permissions + +1. In the client app registration, go to **API permissions** +2. Click **Add a permission** > **My APIs** +3. Select your API registration +4. Check the `access_as_user` scope +5. Click **Add permissions** +6. Click **Grant admin consent** (if required) + +### 3. Create a client secret (for confidential clients) + +1. Go to **Certificates & secrets** +2. Click **New client secret** +3. Add a description and expiration +4. Click **Add** +5. **Copy the secret value immediately** - you won't be able to see it again + +--- + +## Test Your Protected API + +### Using Postman + +1. Create a new request in Postman +2. Set up OAuth 2.0 authentication: + - **Grant Type:** Authorization Code (for user context) or Client Credentials (for app context) + - **Auth URL:** `https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/authorize` + - **Access Token URL:** `https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token` + - **Client ID:** Your client app's client ID + - **Client Secret:** Your client app's secret + - **Scope:** `api://your-api-client-id/access_as_user` +3. Click **Get New Access Token** +4. Use the token to call your API + +### Using code (C# example) + +```csharp +// In a console app or client application +using Microsoft.Identity.Client; + +var app = ConfidentialClientApplicationBuilder + .Create("client-app-id") + .WithClientSecret("client-secret") + .WithAuthority("https://login.microsoftonline.com/{tenant-id}") + .Build(); + +var result = await app.AcquireTokenForClient( + new[] { "api://your-api-client-id/.default" } +).ExecuteAsync(); + +var accessToken = result.AccessToken; + +// Use the token to call your API +using var client = new HttpClient(); +client.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", accessToken); + +var response = await client.GetAsync("https://localhost:5001/api/weatherforecast"); +``` + +--- + +## Common Configuration Options + +### Require specific scopes in configuration + +Instead of using the `[RequiredScope]` attribute, configure required scopes globally: + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-api-client-id", + "Scopes": "access_as_user" + } +} +``` + +### Accept tokens from multiple tenants + +For multi-tenant APIs: + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "common", + "ClientId": "your-api-client-id" + } +} +``` + +### Configure token validation + +```csharp +builder.Services.AddMicrosoftIdentityWebApiAuthentication(builder.Configuration) + .EnableTokenAcquisitionToCallDownstreamApi() // If your API calls other APIs + .AddInMemoryTokenCaches(); +``` + +--- + +## Next Steps + +Now that you have a protected API: + +### Learn More + +βœ… **[Authorization Guide](../authentication/authorization.md)** - RequiredScope attribute, authorization policies, tenant filtering +βœ… **[Customization Guide](../advanced/customization.md)** - Configure JWT bearer options and validation parameters +βœ… **[Logging & Diagnostics](../advanced/logging.md)** - Troubleshoot authentication issues with correlation IDs + +### Advanced Scenarios + +βœ… **[Call downstream APIs](../calling-downstream-apis/from-web-apis.md)** - Call Microsoft Graph or other APIs on behalf of users +βœ… **[Configure token cache](../authentication/token-cache/token-cache-README.md)** - Production cache strategies for OBO scenarios +βœ… **[Long-running processes](../calling-downstream-apis/from-web-apis.md#long-running-processes-with-obo)** - Handle background jobs with OBO tokens +βœ… **[Deploy behind API Gateway](../advanced/api-gateways.md)** - Azure API Management, Azure Front Door, Application Gateway + +## Troubleshooting + +### 401 Unauthorized + +**Problem:** API returns 401 even with a token. + +**Possible causes:** +- Token audience (`aud` claim) doesn't match your API's `ClientId` +- Token is expired +- Token is for the wrong tenant +- Required scope is missing + +**Solution:** Decode the token at [jwt.ms](https://jwt.ms) and verify the claims. See [Logging & Diagnostics](../advanced/logging.md) for detailed troubleshooting. + +### AADSTS50013: Invalid signature + +**Problem:** Token signature validation fails. + +**Solution:** Ensure your `TenantId` and `ClientId` are correct. The token must be issued by the expected authority. Enable detailed logging to see validation errors. + +### Scopes not found in token + +**Problem:** `[RequiredScope]` attribute fails. + +**Solution:** +1. Verify the client app has permission to the scope +2. Ensure admin consent was granted (if required) +3. See [Authorization Guide](../authentication/authorization.md) for complete scope validation patterns +3. Check that the scope is requested when acquiring the token (e.g., `api://your-api/.default` or specific scopes) + +**See more:** [Web API Troubleshooting Guide](../calling-downstream-apis/from-web-apis.md#troubleshooting) + +--- + +## Learn More + +- [Web API Scenario Documentation](./quickstart-webapi.md) +- [Protected Web API Tutorial](https://learn.microsoft.com/azure/active-directory/develop/tutorial-web-api-dotnet-protect-endpoint) +- [API Samples](https://github.com/Azure-Samples/active-directory-dotnet-native-aspnetcore-v2) diff --git a/docs/getting-started/quickstart-webapp.md b/docs/getting-started/quickstart-webapp.md new file mode 100644 index 000000000..29bc2a26e --- /dev/null +++ b/docs/getting-started/quickstart-webapp.md @@ -0,0 +1,281 @@ +# Quickstart: Sign in Users in an ASP.NET Core Web App + +This guide shows you how to create a web app that signs in users with Microsoft Entra ID (formerly Azure AD) using Microsoft.Identity.Web. + +**Time to complete:** ~10 minutes + +## Prerequisites + +- [.NET 9 SDK](https://dotnet.microsoft.com/download/dotnet/9.0) +- A Microsoft Entra ID tenant ([create a free account](https://azure.microsoft.com/free/?WT.mc_id=A261C142F)) +- An app registration in your Entra tenant + +## Option 1: Create from Template (Fastest) + +### 1. Create the project + +```bash +dotnet new webapp --auth SingleOrg --name MyWebApp +cd MyWebApp +``` + +### 2. Configure app registration + +Update `appsettings.json` with your app registration details: + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + "CallbackPath": "/signin-oidc" + } +} +``` + +### 3. Run the application + +```bash +dotnet run +``` + +Navigate to `https://localhost:5001` and click **Sign in**. + +βœ… **Done!** You now have a working web app that signs in users. + +--- + +## Option 2: Add to Existing Web App + +### 1. Install NuGet packages + +```bash +dotnet add package Microsoft.Identity.Web +dotnet add package Microsoft.Identity.Web.UI +``` + +### 2. Configure authentication in `Program.cs` + +```csharp +using Microsoft.Identity.Web; +using Microsoft.Identity.Web.UI; + +var builder = WebApplication.CreateBuilder(args); + +// Add authentication +builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(builder.Configuration, "AzureAd") + .EnableTokenAcquisitionToCallDownstreamApi() // Optional: if calling APIs + .AddInMemoryTokenCaches(); // For production, use distributed cache + +// Add Razor Pages or MVC +builder.Services.AddRazorPages() + .AddMicrosoftIdentityUI(); // Adds sign-in/sign-out UI + +var app = builder.Build(); + +// Configure middleware +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler("/Error"); + app.UseHsts(); +} + +app.UseHttpsRedirection(); +app.UseStaticFiles(); +app.UseRouting(); + +app.UseAuthentication(); // ⭐ Add authentication middleware +app.UseAuthorization(); + +app.MapRazorPages(); +app.MapControllers(); + +app.Run(); +``` + +### 3. Add configuration to `appsettings.json` + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "common", // or your tenant ID for single-tenant + "ClientId": "your-client-id-from-app-registration", + "CallbackPath": "/signin-oidc" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Identity.Web": "Information" + } + } +} +``` + +**Tenant ID values:** +- `common` - Work/school + personal Microsoft accounts +- `organizations` - Work/school accounts only +- `consumers` - Personal Microsoft accounts only +- `` - Specific tenant only (single-tenant app) + +### 4. Protect your pages + +**For Razor Pages:** + +```csharp +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc.RazorPages; + +[Authorize] // ⭐ Require authentication +public class IndexModel : PageModel +{ + public void OnGet() + { + var userName = User.Identity?.Name; + var userEmail = User.FindFirst("preferred_username")?.Value; + } +} +``` + +**For MVC Controllers:** + +```csharp +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +[Authorize] // ⭐ Require authentication +public class HomeController : Controller +{ + public IActionResult Index() + { + var userName = User.Identity?.Name; + return View(); + } +} +``` + +### 5. Add sign-in/sign-out links + +**In your layout (`_Layout.cshtml`):** + +```html + +``` + +### 6. Run and test + +```bash +dotnet run +``` + +βœ… **Success!** Your existing app now supports sign-in. + +--- + +## App Registration Setup + +If you haven't created an app registration yet: + +### 1. Register your application + +1. Sign in to the [Azure portal](https://portal.azure.com) +2. Navigate to **Microsoft Entra ID** > **App registrations** > **New registration** +3. Enter a name (e.g., "My Web App") +4. Select supported account types: + - **Single tenant** - Users in your organization only + - **Multi-tenant** - Users in any organization + - **Multi-tenant + personal** - Users in any organization + personal Microsoft accounts +5. Add a redirect URI: `https://localhost:5001/signin-oidc` (for development) +6. Click **Register** + +### 2. Note the application (client) ID + +Copy the **Application (client) ID** from the overview page - you'll need this for `ClientId` in `appsettings.json`. + +### 3. Note the directory (tenant) ID + +Copy the **Directory (tenant) ID** from the overview page - you'll need this for `TenantId` in `appsettings.json`. + +--- + +## Common Configuration Options + +### Enable ID token issuance (if needed) + +Some scenarios require enabling ID token (if you don't want client credentials): + +1. In your app registration, go to **Authentication** +2. Under **Implicit grant and hybrid flows**, check **ID tokens** +3. Click **Save** + +### Configure logout URL + +1. In your app registration, go to **Authentication** +2. Under **Front-channel logout URL**, add: `https://localhost:5001/signout-oidc` +3. Click **Save** + +--- + +## Next Steps + +Now that you have a working web app with sign-in: + +### Learn More + +βœ… **[Authorization Guide](../authentication/authorization.md)** - Protect controllers with policies and scopes +βœ… **[Customization Guide](../advanced/customization.md)** - OpenID Connect events, login hints, claims transformation +βœ… **[Logging & Diagnostics](../advanced/logging.md)** - Troubleshoot authentication issues with correlation IDs + +### Advanced Scenarios + +βœ… **[Call downstream APIs](../calling-downstream-apis/from-web-apps.md)** - Call Microsoft Graph or your own API +βœ… **[Configure token cache](../authentication/token-cache/token-cache-README.md)** - Set up distributed caching for production +βœ… **[Handle incremental consent](../calling-downstream-apis/from-web-apps.md#incremental-consent--conditional-access)** - Request additional permissions dynamically + +## Troubleshooting + +### AADSTS50011: No reply address is registered + +**Problem:** The redirect URI in your code doesn't match the app registration. + +**Solution:** Ensure the redirect URI in your app registration matches your `CallbackPath` (`/signin-oidc` by default). + +### AADSTS700016: Application not found + +**Problem:** The `ClientId` in your configuration doesn't match any app registration. + +**Solution:** Verify you've copied the correct Application (client) ID from your app registration. + +### "Authority" configuration error + +**Problem:** Missing or invalid `Instance` or `TenantId`. + +**Solution:** Ensure `Instance` is `https://login.microsoftonline.com/` and `TenantId` is valid. See [Logging & Diagnostics](../advanced/logging.md) for detailed troubleshooting. + +**See more:** [Web App Troubleshooting Guide](../calling-downstream-apis/from-web-apps.md#troubleshooting) + +--- + +## Learn More + +- [Web Apps Scenario Documentation](./quickstart-webapp.md) +- [Complete Web App Tutorial](https://learn.microsoft.com/azure/active-directory/develop/tutorial-web-app-dotnet-sign-in-users) +- [Microsoft.Identity.Web Samples](https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2) \ No newline at end of file diff --git a/docs/sidecar/Sidecar.md b/docs/sidecar/Sidecar.md new file mode 100644 index 000000000..bcd6bb129 --- /dev/null +++ b/docs/sidecar/Sidecar.md @@ -0,0 +1,3 @@ +# Microsoft Entra Identity Sidecar Documentation + +See [Microsoft Entra SDK sidecar](https://learn.microsoft.com/en-us/entra/msidweb/agent-id-sdk/overview) \ No newline at end of file diff --git a/internal-links.csv b/internal-links.csv new file mode 100644 index 000000000..022b09af1 --- /dev/null +++ b/internal-links.csv @@ -0,0 +1,423 @@ +"File","LinkText","LinkTarget" +"docs\advanced\api-gateways.md","Web Apps Behind Proxies","web-apps-behind-proxies.md" +"docs\advanced\api-gateways.md","Quickstart: Web API","../getting-started/quickstart-webapi.md" +"docs\advanced\api-gateways.md","Calling Downstream APIs from Web APIs","../calling-downstream-apis/from-web-apis.md" +"docs\advanced\api-gateways.md","Authorization Guide","../authentication/authorization.md" +"docs\advanced\api-gateways.md","Logging & Diagnostics","logging.md" +"docs\advanced\api-gateways.md","Multiple Authentication Schemes","multiple-auth-schemes.md" +"docs\advanced\customization.md","Overview","#overview" +"docs\advanced\customization.md","Configuration Customization","#configuration-customization" +"docs\advanced\customization.md","Event Handler Customization","#event-handler-customization" +"docs\advanced\customization.md","Token Acquisition Customization","#token-acquisition-customization" +"docs\advanced\customization.md","UI Customization","#ui-customization" +"docs\advanced\customization.md","Sign-In Experience Customization","#sign-in-experience-customization" +"docs\advanced\customization.md","Best Practices","#best-practices" +"docs\advanced\customization.md","Authorization Guide","../authentication/authorization.md" +"docs\advanced\customization.md","Logging & Diagnostics","logging.md" +"docs\advanced\customization.md","Token Cache","../authentication/token-cache/README.md" +"docs\advanced\customization.md","Quickstart: Web App","../getting-started/quickstart-webapp.md" +"docs\advanced\logging.md","Overview","#overview" +"docs\advanced\logging.md","Quick Start","#quick-start" +"docs\advanced\logging.md","Configuration","#configuration" +"docs\advanced\logging.md","Log Levels","#log-levels" +"docs\advanced\logging.md","PII Logging","#pii-logging" +"docs\advanced\logging.md","Correlation IDs","#correlation-ids" +"docs\advanced\logging.md","Token Cache Logging","#token-cache-logging" +"docs\advanced\logging.md","Troubleshooting","#troubleshooting" +"docs\advanced\logging.md","Customization Guide","customization.md" +"docs\advanced\logging.md","Authorization Guide","../authentication/authorization.md" +"docs\advanced\logging.md","Token Cache Troubleshooting","../authentication/token-cache/troubleshooting.md" +"docs\advanced\logging.md","Calling Downstream APIs","../calling-downstream-apis/README.md" +"docs\advanced\web-apps-behind-proxies.md","Quickstart: Web App","../getting-started/quickstart-webapp.md" +"docs\advanced\web-apps-behind-proxies.md","APIs Behind Gateways","api-gateways.md" +"docs\advanced\web-apps-behind-proxies.md","Token Cache Configuration","../authentication/token-cache/README.md" +"docs\advanced\web-apps-behind-proxies.md","Customization Guide","customization.md" +"docs\advanced\web-apps-behind-proxies.md","Logging & Diagnostics","logging.md" +"docs\authentication\credentials\certificateless.md","Back to Credentials Overview","./README.md" +"docs\authentication\credentials\certificateless.md","Certificates Guide","./certificates.md" +"docs\authentication\credentials\certificateless.md","Calling Downstream APIs","../../calling-downstream-apis/README.md" +"docs\authentication\credentials\certificateless.md","Deployment Guide","../../deployment/azure-app-service.md" +"docs\authentication\credentials\certificateless.md","troubleshooting guides","../../scenarios/web-apps/troubleshooting.md" +"docs\authentication\credentials\certificates.md","Certificateless Authentication","./certificateless.md" +"docs\authentication\credentials\certificates.md","Azure Key Vault","#azure-key-vault" +"docs\authentication\credentials\certificates.md","Certificate Store","#certificate-store" +"docs\authentication\credentials\certificates.md","File Path","#file-path" +"docs\authentication\credentials\certificates.md","Base64 Encoded","#base64-encoded" +"docs\authentication\credentials\certificates.md","Back to Credentials Overview","./README.md" +"docs\authentication\credentials\certificates.md","Certificateless Authentication","./certificateless.md" +"docs\authentication\credentials\certificates.md","Client Secrets","./client-secrets.md" +"docs\authentication\credentials\certificates.md","Calling Downstream APIs","../../calling-downstream-apis/README.md" +"docs\authentication\credentials\certificates.md","troubleshooting guides","../../scenarios/web-apps/troubleshooting.md" +"docs\authentication\credentials\client-secrets.md","Certificateless Authentication","./certificateless.md" +"docs\authentication\credentials\client-secrets.md","Certificate-Based Authentication","./certificates.md" +"docs\authentication\credentials\client-secrets.md","Certificate-Based Authentication","./certificates.md" +"docs\authentication\credentials\client-secrets.md","Certificateless Authentication","./certificateless.md" +"docs\authentication\credentials\client-secrets.md","Certificateless Authentication","./certificateless.md" +"docs\authentication\credentials\client-secrets.md","Certificate-Based Authentication","./certificates.md" +"docs\authentication\credentials\client-secrets.md","Back to Credentials Overview","./README.md" +"docs\authentication\credentials\client-secrets.md","Calling Downstream APIs","../../calling-downstream-apis/README.md" +"docs\authentication\credentials\client-secrets.md","Security Best Practices","../../advanced/security-best-practices.md" +"docs\authentication\credentials\client-secrets.md","troubleshooting guides","../../scenarios/web-apps/troubleshooting.md" +"docs\authentication\credentials\README.md","Learn more about certificateless authentication β†’","./certificateless.md" +"docs\authentication\credentials\README.md","Learn more about Key Vault certificates β†’","./certificates.md#key-vault" +"docs\authentication\credentials\README.md","Learn more about certificate store β†’","./certificates.md#certificate-store" +"docs\authentication\credentials\README.md","Learn more about file-based certificates β†’","./certificates.md#file-path" +"docs\authentication\credentials\README.md","Learn more about base64 certificates β†’","./certificates.md#base64-encoded" +"docs\authentication\credentials\README.md","Learn more about client secrets β†’","./client-secrets.md" +"docs\authentication\credentials\README.md","Learn more about token decryption β†’","./token-decryption.md" +"docs\authentication\credentials\README.md","Custom Signed Assertion Providers","../../advanced/custom-credential-providers.md" +"docs\authentication\credentials\README.md","Certificateless Authentication β†’","./certificateless.md" +"docs\authentication\credentials\README.md","Certificates β†’","./certificates.md" +"docs\authentication\credentials\README.md","Client Secrets β†’","./client-secrets.md" +"docs\authentication\credentials\README.md","Token Decryption β†’","./token-decryption.md" +"docs\authentication\credentials\README.md","Web Applications","../../scenarios/web-apps/README.md" +"docs\authentication\credentials\README.md","Web APIs","../../scenarios/web-apis/README.md" +"docs\authentication\credentials\README.md","Daemon Applications","../../scenarios/daemon/README.md" +"docs\authentication\credentials\README.md","Agent Identities","../../scenarios/agent-identities/README.md" +"docs\authentication\credentials\README.md","Calling Downstream APIs","../../calling-downstream-apis/README.md" +"docs\authentication\credentials\README.md","Token Cache","../token-cache/README.md" +"docs\authentication\credentials\README.md","Migration Guides","../../migration/README.md" +"docs\authentication\credentials\README.md","Web Apps","../../scenarios/web-apps/troubleshooting.md" +"docs\authentication\credentials\README.md","Web APIs","../../scenarios/web-apis/troubleshooting.md" +"docs\authentication\credentials\README.md","Daemon Apps","../../scenarios/daemon/README.md" +"docs\authentication\credentials\README.md","troubleshooting guide","../../scenarios/web-apps/troubleshooting.md" +"docs\authentication\credentials\token-decryption.md","Certificates Guide","./certificates.md" +"docs\authentication\credentials\token-decryption.md","Certificate-Based Authentication","./certificates.md" +"docs\authentication\credentials\token-decryption.md","Security Best Practices","../../advanced/security-best-practices.md" +"docs\authentication\credentials\token-decryption.md","Back to Credentials Overview","./README.md" +"docs\authentication\credentials\token-decryption.md","Certificates Guide","./certificates.md" +"docs\authentication\credentials\token-decryption.md","Calling Downstream APIs","../../calling-downstream-apis/README.md" +"docs\authentication\credentials\token-decryption.md","troubleshooting guides","../../scenarios/web-apps/troubleshooting.md" +"docs\authentication\token-cache\README.md","Overview","#overview" +"docs\authentication\token-cache\README.md","Quick Start","#quick-start" +"docs\authentication\token-cache\README.md","Choosing a Cache Strategy","#choosing-a-cache-strategy" +"docs\authentication\token-cache\README.md","Cache Implementations","#cache-implementations" +"docs\authentication\token-cache\README.md","Advanced Configuration","#advanced-configuration" +"docs\authentication\token-cache\README.md","Next Steps","#next-steps" +"docs\authentication\token-cache\README.md","Web apps calling APIs","../../calling-downstream-apis/from-web-apps.md" +"docs\authentication\token-cache\README.md","Web APIs calling downstream APIs","../../calling-downstream-apis/from-web-apis.md" +"docs\authentication\token-cache\README.md","β†’ Learn more about in-memory cache configuration","in-memory.md" +"docs\authentication\token-cache\README.md","β†’ Learn more about distributed cache configuration","distributed.md" +"docs\authentication\token-cache\README.md","β†’ Learn more about L1/L2 cache architecture","l1-l2-cache.md" +"docs\authentication\token-cache\README.md","β†’ Learn more about cache eviction strategies","eviction.md" +"docs\authentication\token-cache\README.md","β†’ Learn more about encryption and data protection","encryption.md" +"docs\authentication\token-cache\README.md","Distributed Cache Deep Dive","distributed.md" +"docs\authentication\token-cache\README.md","Cache Eviction Strategies","eviction.md" +"docs\authentication\token-cache\README.md","Troubleshooting Guide","troubleshooting.md" +"docs\authentication\token-cache\README.md","Encryption Guide","encryption.md" +"docs\authentication\token-cache\README.md","Calling Downstream APIs from Web Apps","../../calling-downstream-apis/from-web-apps.md" +"docs\authentication\token-cache\README.md","Calling Downstream APIs from Web APIs","../../calling-downstream-apis/from-web-apis.md" +"docs\authentication\token-cache\README.md","Web App Quickstart","../../getting-started/quickstart-webapp.md" +"docs\authentication\token-cache\README.md","Configuring Redis for Production","distributed.md#redis-production-setup" +"docs\authentication\token-cache\README.md","Handling L2 Cache Failures","troubleshooting.md#l2-cache-failures" +"docs\authentication\token-cache\README.md","Optimizing Cache Performance","distributed.md#performance-optimization" +"docs\authentication\token-cache\README.md","Multi-Region Cache Deployment","distributed.md#multi-region" +"docs\authentication\token-cache\troubleshooting.md","L2 Cache Not Being Written","#l2-cache-not-being-written" +"docs\authentication\token-cache\troubleshooting.md","Deserialization Errors with Encryption","#deserialization-errors-with-encryption" +"docs\authentication\token-cache\troubleshooting.md","Memory Cache Growing Too Large","#memory-cache-growing-too-large" +"docs\authentication\token-cache\troubleshooting.md","Frequent MFA Prompts","#frequent-mfa-prompts" +"docs\authentication\token-cache\troubleshooting.md","Cache Connection Failures","#cache-connection-failures" +"docs\authentication\token-cache\troubleshooting.md","Token Cache Empty After Restart","#token-cache-empty-after-restart" +"docs\authentication\token-cache\troubleshooting.md","Session Cache Cookie Too Large","#session-cache-cookie-too-large" +"docs\authentication\token-cache\troubleshooting.md","Token Cache Overview","README.md" +"docs\authentication\token-cache\troubleshooting.md","Token Cache Overview","README.md" +"docs\authentication\token-cache\troubleshooting.md","Distributed Cache Configuration","distributed.md" +"docs\authentication\token-cache\troubleshooting.md","Cache Eviction Strategies","eviction.md" +"docs\authentication\authorization.md","Overview","#overview" +"docs\authentication\authorization.md","Authorization Concepts","#authorization-concepts" +"docs\authentication\authorization.md","Scope Validation with RequiredScope","#scope-validation-with-requiredscope" +"docs\authentication\authorization.md","App Permissions with RequiredScopeOrAppPermission","#app-permissions-with-requiredscopeorapppermission" +"docs\authentication\authorization.md","Authorization Policies","#authorization-policies" +"docs\authentication\authorization.md","Tenant Filtering","#tenant-filtering" +"docs\authentication\authorization.md","Best Practices","#best-practices" +"docs\authentication\authorization.md","Customization Guide","../advanced/customization.md" +"docs\authentication\authorization.md","Logging & Diagnostics","../advanced/logging.md" +"docs\authentication\authorization.md","Quickstart: Web API","../getting-started/quickstart-webapi.md" +"docs\authentication\authorization.md","Token Cache","token-cache/README.md" +"docs\blog-posts\downstreamwebapi-to-downstreamapi.md","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" +"docs\calling-downstream-apis\azure-sdks.md","Agent Identities documentation","../scenarios/agent-identities/README.md" +"docs\calling-downstream-apis\azure-sdks.md","Credentials Configuration","../authentication/credentials/README.md" +"docs\calling-downstream-apis\azure-sdks.md","Agent Identities","../scenarios/agent-identities/README.md" +"docs\calling-downstream-apis\azure-sdks.md","Calling Downstream APIs Overview","README.md" +"docs\calling-downstream-apis\azure-sdks.md","calling custom APIs","custom-apis.md" +"docs\calling-downstream-apis\custom-apis.md","IDownstreamApi Reference","../api-reference/idownstreamapi.md" +"docs\calling-downstream-apis\custom-apis.md","Calling from Web Apps","from-web-apps.md" +"docs\calling-downstream-apis\custom-apis.md","Calling from Web APIs","from-web-apis.md" +"docs\calling-downstream-apis\custom-apis.md","Microsoft Graph Integration","microsoft-graph.md" +"docs\calling-downstream-apis\custom-apis.md","Agent Identities","../scenarios/agent-identities/README.md" +"docs\calling-downstream-apis\custom-apis.md","main documentation","README.md" +"docs\calling-downstream-apis\from-web-apis.md","Logging & Diagnostics Guide","../advanced/logging.md" +"docs\calling-downstream-apis\from-web-apis.md","Long-Running Processes","../advanced/long-running-processes.md" +"docs\calling-downstream-apis\from-web-apis.md","Token Caching","../authentication/token-cache/README.md" +"docs\calling-downstream-apis\from-web-apis.md","Calling from Web Apps","from-web-apps.md" +"docs\calling-downstream-apis\from-web-apis.md","Web API Scenarios","../scenarios/web-apis/README.md" +"docs\calling-downstream-apis\from-web-apis.md","API Behind Gateways","../advanced/api-gateways.md" +"docs\calling-downstream-apis\from-web-apis.md","Logging & Diagnostics","../advanced/logging.md" +"docs\calling-downstream-apis\from-web-apis.md","Authorization Guide","../authentication/authorization.md" +"docs\calling-downstream-apis\from-web-apis.md","Customization Guide","../advanced/customization.md" +"docs\calling-downstream-apis\from-web-apis.md","calling Microsoft Graph","microsoft-graph.md" +"docs\calling-downstream-apis\from-web-apis.md","custom APIs","custom-apis.md" +"docs\calling-downstream-apis\from-web-apps.md","πŸ“– Learn more about Microsoft Graph integration","microsoft-graph.md" +"docs\calling-downstream-apis\from-web-apps.md","πŸ“– Learn more about Azure SDK integration","azure-sdks.md" +"docs\calling-downstream-apis\from-web-apps.md","πŸ“– Learn more about custom API calls","custom-apis.md" +"docs\calling-downstream-apis\from-web-apps.md","πŸ“– Learn more about custom HTTP logic","custom-apis.md#using-iauthorizationheaderprovider" +"docs\calling-downstream-apis\from-web-apps.md","OWIN documentation","../../scenarios/web-apps/owin.md" +"docs\calling-downstream-apis\from-web-apps.md","Logging & Diagnostics Guide","../advanced/logging.md" +"docs\calling-downstream-apis\from-web-apps.md","Back to Downstream APIs Overview","./README.md" +"docs\calling-downstream-apis\from-web-apps.md","Token Cache Configuration","../authentication/token-cache/README.md" +"docs\calling-downstream-apis\from-web-apps.md","Microsoft Graph Integration","./microsoft-graph.md" +"docs\calling-downstream-apis\from-web-apps.md","Azure SDK Integration","./azure-sdks.md" +"docs\calling-downstream-apis\from-web-apps.md","Custom API Calls","./custom-apis.md" +"docs\calling-downstream-apis\from-web-apps.md","Calling from Web APIs (OBO)","./from-web-apis.md" +"docs\calling-downstream-apis\from-web-apps.md","Configure distributed cache","../authentication/token-cache/README.md" +"docs\calling-downstream-apis\from-web-apps.md","troubleshooting guide","../../scenarios/web-apps/troubleshooting.md" +"docs\calling-downstream-apis\microsoft-graph.md","Calling Downstream APIs Overview","README.md" +"docs\calling-downstream-apis\microsoft-graph.md","Calling from Web Apps","from-web-apps.md" +"docs\calling-downstream-apis\microsoft-graph.md","Calling from Web APIs","from-web-apis.md" +"docs\calling-downstream-apis\microsoft-graph.md","calling Azure SDKs","azure-sdks.md" +"docs\calling-downstream-apis\microsoft-graph.md","custom APIs","custom-apis.md" +"docs\calling-downstream-apis\README.md","πŸ“– Learn more about Microsoft Graph integration","microsoft-graph.md" +"docs\calling-downstream-apis\README.md","πŸ“– Learn more about Azure SDK integration","azure-sdks.md" +"docs\calling-downstream-apis\README.md","πŸ“– Learn more about IDownstreamApi","custom-apis.md" +"docs\calling-downstream-apis\README.md","πŸ“– Learn more about MicrosoftIdentityMessageHandler","custom-apis.md#microsoftidentitymessagehandler" +"docs\calling-downstream-apis\README.md","πŸ“– Learn more about IAuthorizationHeaderProvider","custom-apis.md#iauthorizationheaderprovider" +"docs\calling-downstream-apis\README.md","πŸ“– Learn more about credentials configuration","../authentication/credentials/README.md" +"docs\calling-downstream-apis\README.md","πŸ“– Read the Web Apps guide","from-web-apps.md" +"docs\calling-downstream-apis\README.md","πŸ“– Read the Web APIs guide","from-web-apis.md" +"docs\calling-downstream-apis\README.md","πŸ“– Read the Daemon Applications guide","../scenarios/daemon/README.md" +"docs\calling-downstream-apis\README.md","Credentials Configuration","../authentication/credentials/README.md" +"docs\calling-downstream-apis\README.md","Web App Scenarios","../scenarios/web-apps/README.md" +"docs\calling-downstream-apis\README.md","Web API Scenarios","../scenarios/web-apis/README.md" +"docs\calling-downstream-apis\README.md","Agent Identities","../scenarios/agent-identities/README.md" +"docs\calling-downstream-apis\README.md","credentials guide","../authentication/credentials/README.md" +"docs\design\managed_identity_capabilities_devex.md","alt text","capab1.png" +"docs\frameworks\aspnet-framework.md","MSAL.NET with Microsoft.Identity.Web Guide","msal-dotnet-framework.md" +"docs\frameworks\aspnet-framework.md","OWIN Integration Guide","owin.md" +"docs\frameworks\aspnet-framework.md","MSAL.NET with Microsoft.Identity.Web","msal-dotnet-framework.md" +"docs\frameworks\aspnet-framework.md","OWIN Integration","owin.md" +"docs\frameworks\msal-dotnet-framework.md","Overview","#overview" +"docs\frameworks\msal-dotnet-framework.md","Package Options","#package-options" +"docs\frameworks\msal-dotnet-framework.md","Token Cache Serialization","#token-cache-serialization" +"docs\frameworks\msal-dotnet-framework.md","Certificate Management","#certificate-management" +"docs\frameworks\msal-dotnet-framework.md","Sample Applications","#sample-applications" +"docs\frameworks\msal-dotnet-framework.md","Best Practices","#best-practices" +"docs\frameworks\msal-dotnet-framework.md","OWIN Integration","owin.md" +"docs\frameworks\msal-dotnet-framework.md","Daemon Applications Guide","../scenarios/daemon/README.md" +"docs\frameworks\msal-dotnet-framework.md","OWIN Integration","owin.md" +"docs\frameworks\msal-dotnet-framework.md","ASP.NET Framework Overview","aspnet-framework.md" +"docs\frameworks\msal-dotnet-framework.md","Credentials Guide","../authentication/credentials/README.md" +"docs\frameworks\msal-dotnet-framework.md","Logging & Diagnostics","../advanced/logging.md" +"docs\frameworks\owin.md","Overview","#overview" +"docs\frameworks\owin.md","Installation","#installation" +"docs\frameworks\owin.md","Configuration","#configuration" +"docs\frameworks\owin.md","Startup Setup","#startup-setup" +"docs\frameworks\owin.md","Controller Integration","#controller-integration" +"docs\frameworks\owin.md","Calling Microsoft Graph","#calling-microsoft-graph" +"docs\frameworks\owin.md","Calling Downstream APIs","#calling-downstream-apis" +"docs\frameworks\owin.md","Sample Applications","#sample-applications" +"docs\frameworks\owin.md","Best Practices","#best-practices" +"docs\frameworks\owin.md","MSAL.NET with Microsoft.Identity.Web","msal-dotnet-framework.md" +"docs\frameworks\owin.md","ASP.NET Framework Overview","aspnet-framework.md" +"docs\frameworks\owin.md","Authorization Guide","../authentication/authorization.md" +"docs\frameworks\owin.md","Customization Guide","../advanced/customization.md" +"docs\frameworks\owin.md","Logging & Diagnostics","../advanced/logging.md" +"docs\frameworks\owin.md","Token Cache Serialization","../authentication/token-cache/README.md" +"docs\getting-started\quickstart-webapi.md","Authorization Guide","../authentication/authorization.md" +"docs\getting-started\quickstart-webapi.md","Customization Guide","../advanced/customization.md" +"docs\getting-started\quickstart-webapi.md","Logging & Diagnostics","../advanced/logging.md" +"docs\getting-started\quickstart-webapi.md","Call downstream APIs","../calling-downstream-apis/from-web-apis.md" +"docs\getting-started\quickstart-webapi.md","Configure token cache","../authentication/token-cache/README.md" +"docs\getting-started\quickstart-webapi.md","Long-running processes","../scenarios/web-apis/long-running-processes.md" +"docs\getting-started\quickstart-webapi.md","Deploy behind API Gateway","../advanced/api-gateways.md" +"docs\getting-started\quickstart-webapi.md","Logging & Diagnostics","../advanced/logging.md" +"docs\getting-started\quickstart-webapi.md","Authorization Guide","../authentication/authorization.md" +"docs\getting-started\quickstart-webapi.md","Web API Troubleshooting Guide","../scenarios/web-apis/troubleshooting.md" +"docs\getting-started\quickstart-webapi.md","Web API Scenario Documentation","../scenarios/web-apis/README.md" +"docs\getting-started\quickstart-webapp.md","Authorization Guide","../authentication/authorization.md" +"docs\getting-started\quickstart-webapp.md","Customization Guide","../advanced/customization.md" +"docs\getting-started\quickstart-webapp.md","Logging & Diagnostics","../advanced/logging.md" +"docs\getting-started\quickstart-webapp.md","Call downstream APIs","../calling-downstream-apis/from-web-apps.md" +"docs\getting-started\quickstart-webapp.md","Configure token cache","../authentication/token-cache/README.md" +"docs\getting-started\quickstart-webapp.md","Handle incremental consent","../advanced/incremental-consent-ca.md" +"docs\getting-started\quickstart-webapp.md","Deploy to Azure","../deployment/azure-app-service.md" +"docs\getting-started\quickstart-webapp.md","Logging & Diagnostics","../advanced/logging.md" +"docs\getting-started\quickstart-webapp.md","Web App Troubleshooting Guide","../scenarios/web-apps/troubleshooting.md" +"docs\getting-started\quickstart-webapp.md","Web Apps Scenario Documentation","../scenarios/web-apps/README.md" +"docs\scenarios\daemon\README.md","Quick Start","#quick-start" +"docs\scenarios\daemon\README.md","Standard Daemon Applications","#standard-daemon-applications" +"docs\scenarios\daemon\README.md","Autonomous Agents (Agent Identity)","#autonomous-agents-agent-identity" +"docs\scenarios\daemon\README.md","Agent User Identity","#agent-user-identity" +"docs\scenarios\daemon\README.md","Service Configuration","#service-configuration" +"docs\scenarios\daemon\README.md","Calling APIs","#calling-apis" +"docs\scenarios\daemon\README.md","Token Caching","#token-caching" +"docs\scenarios\daemon\README.md","Azure Samples","#azure-samples" +"docs\scenarios\daemon\README.md","Troubleshooting","#troubleshooting" +"docs\scenarios\daemon\README.md","Calling downstream APIs","../../calling-downstream-apis/README.md" +"docs\scenarios\daemon\README.md","Token Cache Configuration","../../authentication/token-cache/README.md" +"docs\scenarios\daemon\README.md","Certificate Configuration Guide","../../frameworks/msal-dotnet-framework.md#certificate-loading" +"docs\scenarios\daemon\README.md","Logging & Diagnostics Guide","../../advanced/logging.md" +"docs\scenarios\daemon\README.md","Calling Downstream APIs from Web APIs","../../calling-downstream-apis/from-web-apis.md" +"docs\scenarios\daemon\README.md","MSAL.NET Framework Guide","../../frameworks/msal-dotnet-framework.md" +"docs\scenarios\daemon\README.md","Certificate Configuration","../../authentication/credentials.md" +"docs\scenarios\daemon\README.md","Token Cache Configuration","../../authentication/token-cache/README.md" +"docs\scenarios\daemon\README.md","Logging & Diagnostics","../../advanced/logging.md" +"docs\scenarios\daemon\README.md","Customization Guide","../../advanced/customization.md" +"docs\sidecar\scenarios\agent-autonomous-batch.md","Agent Identities","../agent-identities.md" +"docs\sidecar\scenarios\agent-autonomous-batch.md","Configuration Reference","../configuration.md" +"docs\sidecar\scenarios\agent-autonomous-batch.md","Security Best Practices","../security.md" +"docs\sidecar\scenarios\call-downstream-api.md","Obtain Authorization Header","obtain-authorization-header.md" +"docs\sidecar\scenarios\call-downstream-api.md","Using from TypeScript","using-from-typescript.md" +"docs\sidecar\scenarios\call-downstream-api.md","Using from Python","using-from-python.md" +"docs\sidecar\scenarios\call-downstream-api.md","Endpoints Reference","../endpoints.md" +"docs\sidecar\scenarios\long-running-obo.md","Configuration Reference","../configuration.md" +"docs\sidecar\scenarios\long-running-obo.md","Security Best Practices","../security.md" +"docs\sidecar\scenarios\long-running-obo.md","Troubleshooting","../troubleshooting.md" +"docs\sidecar\scenarios\long-running-obo.md","Agent Identities","../agent-identities.md" +"docs\sidecar\scenarios\managed-identity.md","Installation Guide","../installation.md#azure-kubernetes-service-aks-with-managed-identity" +"docs\sidecar\scenarios\managed-identity.md","Security Best Practices","../security.md" +"docs\sidecar\scenarios\managed-identity.md","Configuration Reference","../configuration.md" +"docs\sidecar\scenarios\managed-identity.md","Troubleshooting","../troubleshooting.md" +"docs\sidecar\scenarios\obtain-authorization-header.md","Call a Downstream API","call-downstream-api.md" +"docs\sidecar\scenarios\obtain-authorization-header.md","Agent Identities","../agent-identities.md" +"docs\sidecar\scenarios\obtain-authorization-header.md","Endpoints Reference","../endpoints.md" +"docs\sidecar\scenarios\obtain-authorization-header.md","Troubleshooting","../troubleshooting.md" +"docs\sidecar\scenarios\signed-http-request.md","Security Best Practices","../security.md#signed-http-requests-shr" +"docs\sidecar\scenarios\signed-http-request.md","Configuration Reference","../configuration.md#signed-http-request-shr-configuration" +"docs\sidecar\scenarios\signed-http-request.md","Endpoints Reference","../endpoints.md" +"docs\sidecar\scenarios\using-from-python.md","Call Downstream API","call-downstream-api.md" +"docs\sidecar\scenarios\using-from-python.md","Obtain Authorization Header","obtain-authorization-header.md" +"docs\sidecar\scenarios\using-from-python.md","Agent Identities","../agent-identities.md" +"docs\sidecar\scenarios\using-from-typescript.md","Call Downstream API","call-downstream-api.md" +"docs\sidecar\scenarios\using-from-typescript.md","Obtain Authorization Header","obtain-authorization-header.md" +"docs\sidecar\scenarios\using-from-typescript.md","Agent Identities","../agent-identities.md" +"docs\sidecar\scenarios\validate-authorization-header.md","Obtain Authorization Header","obtain-authorization-header.md" +"docs\sidecar\scenarios\validate-authorization-header.md","Security Best Practices","../security.md" +"docs\sidecar\scenarios\validate-authorization-header.md","Endpoints Reference","../endpoints.md" +"docs\sidecar\scenarios\validate-authorization-header.md","Troubleshooting","../troubleshooting.md" +"docs\sidecar\agent-identities.md","Configuration Reference","configuration.md" +"docs\sidecar\agent-identities.md","Endpoints Reference","endpoints.md" +"docs\sidecar\agent-identities.md","Scenarios: Agent Autonomous Batch","scenarios/agent-autonomous-batch.md" +"docs\sidecar\agent-identities.md","FAQ","faq.md" +"docs\sidecar\agent-identities.md","Troubleshooting","troubleshooting.md" +"docs\sidecar\comparison.md","Installation Guide","installation.md" +"docs\sidecar\comparison.md","Configuration Reference","configuration.md" +"docs\sidecar\comparison.md","Scenarios","scenarios/README.md" +"docs\sidecar\configuration.md","Agent Identities","agent-identities.md" +"docs\sidecar\configuration.md","Agent Identities","agent-identities.md" +"docs\sidecar\configuration.md","Endpoints Reference","endpoints.md" +"docs\sidecar\configuration.md","Security Best Practices","security.md" +"docs\sidecar\configuration.md","Troubleshooting","troubleshooting.md" +"docs\sidecar\endpoints.md","Configuration","configuration.md" +"docs\sidecar\endpoints.md","Agent Identities","agent-identities.md" +"docs\sidecar\endpoints.md","Security","security.md" +"docs\sidecar\endpoints.md","Scenarios","README.md#scenario-guides" +"docs\sidecar\endpoints.md","Troubleshooting","troubleshooting.md" +"docs\sidecar\faq.md","Comparison with Microsoft.Identity.Web","comparison.md" +"docs\sidecar\faq.md","Installation Guide","installation.md" +"docs\sidecar\faq.md","Agent Identities","agent-identities.md" +"docs\sidecar\faq.md","Agent Identities configuration section","agent-identities.md#microsoft-entra-id-configuration" +"docs\sidecar\faq.md","Security Best Practices","security.md" +"docs\sidecar\faq.md","Configuration Reference","configuration.md#configuration-overrides" +"docs\sidecar\faq.md","Security Best Practices","security.md#signed-http-requests-shr" +"docs\sidecar\faq.md","Security Best Practices","security.md#network-security" +"docs\sidecar\faq.md","Installation Guide","installation.md#azure-kubernetes-service-aks-with-managed-identity" +"docs\sidecar\faq.md","Troubleshooting Guide","troubleshooting.md" +"docs\sidecar\faq.md","Troubleshooting - Agent Identity Validation","troubleshooting.md#3-400-bad-request---agent-identity-validation" +"docs\sidecar\faq.md","Comparison - Migration Guidance","comparison.md#migration-guidance" +"docs\sidecar\faq.md","Security Best Practices","security.md" +"docs\sidecar\faq.md","Security - Incident Response","security.md#incident-response" +"docs\sidecar\faq.md","Installation Guide","installation.md" +"docs\sidecar\faq.md","Configuration Reference","configuration.md" +"docs\sidecar\faq.md","Agent Identities","agent-identities.md" +"docs\sidecar\faq.md","Scenarios","scenarios/README.md" +"docs\sidecar\faq.md","Security Best Practices","security.md" +"docs\sidecar\faq.md","Troubleshooting","troubleshooting.md" +"docs\sidecar\index.md","Comparison Guide","comparison.md" +"docs\sidecar\index.md","Installation","installation.md" +"docs\sidecar\index.md","Configuration","configuration.md" +"docs\sidecar\index.md","Endpoints","endpoints.md" +"docs\sidecar\index.md","README navigation","README.md" +"docs\sidecar\index.md","Installation","installation.md" +"docs\sidecar\installation.md","Configuration Reference","configuration.md" +"docs\sidecar\installation.md","Security Best Practices","security.md" +"docs\sidecar\installation.md","Endpoints Reference","endpoints.md" +"docs\sidecar\README.md","Overview","index.md" +"docs\sidecar\README.md","Overview","index.md" +"docs\sidecar\README.md","Installation","installation.md" +"docs\sidecar\README.md","Installation","installation.md" +"docs\sidecar\README.md","Configuration","configuration.md" +"docs\sidecar\README.md","Security","security.md" +"docs\sidecar\README.md","Scenario Guides","#scenario-guides" +"docs\sidecar\README.md","Endpoints","endpoints.md" +"docs\sidecar\README.md","Comparison","comparison.md" +"docs\sidecar\README.md","Troubleshooting","troubleshooting.md" +"docs\sidecar\README.md","FAQ","faq.md" +"docs\sidecar\README.md","Agent Identities","agent-identities.md" +"docs\sidecar\README.md","index.md","index.md" +"docs\sidecar\README.md","installation.md","installation.md" +"docs\sidecar\README.md","configuration.md","configuration.md" +"docs\sidecar\README.md","agent-identities.md","agent-identities.md" +"docs\sidecar\README.md","endpoints.md","endpoints.md" +"docs\sidecar\README.md","security.md","security.md" +"docs\sidecar\README.md","comparison.md","comparison.md" +"docs\sidecar\README.md","troubleshooting.md","troubleshooting.md" +"docs\sidecar\README.md","faq.md","faq.md" +"docs\sidecar\README.md","Validate an Authorization Header","scenarios/validate-authorization-header.md" +"docs\sidecar\README.md","Obtain an Authorization Header","scenarios/obtain-authorization-header.md" +"docs\sidecar\README.md","Call a Downstream API","scenarios/call-downstream-api.md" +"docs\sidecar\README.md","Use Managed Identity","scenarios/managed-identity.md" +"docs\sidecar\README.md","Implement Long-Running OBO","scenarios/long-running-obo.md" +"docs\sidecar\README.md","Use Signed HTTP Requests","scenarios/signed-http-request.md" +"docs\sidecar\README.md","Agent Autonomous Batch Processing","scenarios/agent-autonomous-batch.md" +"docs\sidecar\README.md","Integration from TypeScript","scenarios/using-from-typescript.md" +"docs\sidecar\README.md","Integration from Python","scenarios/using-from-python.md" +"docs\sidecar\security.md","Configuration Reference","configuration.md" +"docs\sidecar\security.md","Agent Identities","agent-identities.md" +"docs\sidecar\security.md","Troubleshooting","troubleshooting.md" +"docs\sidecar\security.md","Installation Guide","installation.md" +"docs\sidecar\troubleshooting.md","Configuration Reference","configuration.md" +"docs\sidecar\troubleshooting.md","Agent Identities","agent-identities.md" +"docs\sidecar\troubleshooting.md","Security Best Practices","security.md" +"docs\sidecar\troubleshooting.md","FAQ","faq.md" +"docs\README.md","Web App - Sign in users","./getting-started/quickstart-webapp.md" +"docs\README.md","Web API - Protect your API","./getting-started/quickstart-webapi.md" +"docs\README.md","Daemon App - Call APIs","./scenarios/daemon/README.md" +"docs\README.md","Microsoft.Identity.Web.DownstreamApi documentation","./packages/downstream-api.md" +"docs\README.md","Agent Identities guide","./packages/agent-identities.md" +"docs\README.md","Web Apps Scenario","./scenarios/web-apps/README.md" +"docs\README.md","Web APIs Scenario","./scenarios/web-apis/README.md" +"docs\README.md","Credentials Guide","./authentication/credentials/README.md" +"docs\README.md","Daemon Applications & Agent Identities","./scenarios/daemon/README.md" +"docs\README.md","Package Reference Guide","./packages/README.md" +"docs\README.md","Certificateless (FIC + Managed Identity)","./authentication/credentials/certificateless.md" +"docs\README.md","Certificates from Key Vault","./authentication/credentials/certificates.md#key-vault" +"docs\README.md","Client Secrets","./authentication/credentials/client-secrets.md" +"docs\README.md","Certificates from Files","./authentication/credentials/certificates.md#file-path" +"docs\README.md","Credential Decision Guide","./authentication/credentials/README.md" +"docs\README.md","Quickstart: Web App","./getting-started/quickstart-webapp.md" +"docs\README.md","Quickstart: Web API","./getting-started/quickstart-webapi.md" +"docs\README.md","Why Microsoft.Identity.Web?","./getting-started/why-microsoft-identity-web.md" +"docs\README.md","Web Applications","./scenarios/web-apps/README.md" +"docs\README.md","Web APIs","./scenarios/web-apis/README.md" +"docs\README.md","Daemon Applications and Agent Identities","./scenarios/daemon/README.md" +"docs\README.md","Azure Functions","./scenarios/azure-functions/README.md" +"docs\README.md","Credentials Guide","./authentication/credentials/README.md" +"docs\README.md","Token Cache","./authentication/token-cache/README.md" +"docs\README.md","Token Decryption","./authentication/token-cache/token-decryption.md" +"docs\README.md","Authorization","./authentication/authorization.md" +"docs\README.md","Customization","./advanced/customization.md" +"docs\README.md","Logging & Diagnostics","./advanced/logging.md" +"docs\README.md","Multiple Authentication Schemes","./advanced/multiple-auth-schemes.md" +"docs\README.md","Incremental Consent & Conditional Access","./advanced/incremental-consent-ca.md" +"docs\README.md","Long-Running Processes","./advanced/long-running-processes.md" +"docs\README.md","APIs Behind Gateways","./advanced/api-gateways.md" +"docs\README.md","Performance Optimization","./advanced/performance.md" +"docs\README.md","ASP.NET Framework & .NET Standard","./frameworks/aspnet-framework.md" +"docs\README.md","MSAL.NET with Microsoft.Identity.Web","./frameworks/msal-dotnet-framework.md" +"docs\README.md","OWIN Integration","./frameworks/owin.md" +"docs\README.md","Azure App Service","./deployment/azure-app-service.md" +"docs\README.md","Containers & Docker","./deployment/containers.md" +"docs\README.md","Migrating from 1.x to 2.x","./migration/v1-to-v2.md" +"docs\README.md","Migrating from 2.x to 3.x","./migration/v2-to-v3.md" +"docs\README.md","Quickstart Guides","./getting-started/" +"docs\README.md","Scenarios","./scenarios/" diff --git a/src/Microsoft.Identity.Web.AgentIdentities/README.AgentIdentities.md b/src/Microsoft.Identity.Web.AgentIdentities/README.AgentIdentities.md index 0c2726ef7..834dbc91a 100644 --- a/src/Microsoft.Identity.Web.AgentIdentities/README.AgentIdentities.md +++ b/src/Microsoft.Identity.Web.AgentIdentities/README.AgentIdentities.md @@ -1,523 +1,3 @@ # 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 - -## 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 [Readme-azure](../../README-Azure.md) - -### 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"); -}) -.AddHttpMessageHandler(serviceProvider => new MicrosoftIdentityMessageHandler( - serviceProvider.GetRequiredService(), - new MicrosoftIdentityMessageHandlerOptions - { - 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) +See [Agent identities](../../docs/calling-downstream-apis/AgentIdentities-Readme.md) \ No newline at end of file diff --git a/src/Microsoft.Identity.Web.GraphServiceClient/GraphServiceClient-Readme.md b/src/Microsoft.Identity.Web.GraphServiceClient/GraphServiceClient-Readme.md new file mode 100644 index 000000000..852f202aa --- /dev/null +++ b/src/Microsoft.Identity.Web.GraphServiceClient/GraphServiceClient-Readme.md @@ -0,0 +1,282 @@ +ο»Ώ# Microsoft.Identity.Web.GraphServiceClient + +With the introduction of Microsoft.Identity.Web.GraphServiceClient and Microsoft.Identity.Web.GraphServiceClientBeta +libraries in version Microsoft.Identity.Web 2.12, you now have the choice to use either the legacy +Microsoft.Identity.Web.MicrosoftGraph and Microsoft.Identity.Web.MicrosoftGraphBeta NuGet packages +based on Microsoft Graph SDK 4.x or the new libraries based on Microsoft Graph SDK 5. +By keeping both options available, you can choose to migrate to the latest version of the SDK at your own pace +and with minimal disruption to your existing code. + +By migrating to Microsoft.Identity.Web.GraphServiceClient, you'll benefit from the latest features of the Microsoft Graph SDK, +including a simplified fluent API and the ability to use both Microsoft Graph and Microsoft Graph Beta APIs in the same application. +However, migrating from Microsoft.Identity.Web.MicrosoftGraph 2.x to Microsoft.Identity.Web.GraphServiceClient requires moving some of your code, +as discussed in the [migration guide](#migrate-from-microsoftidentitywebmicrosoftgraph-2x-to-microsoftidentitywebgraphserviceclient). + +## Usage + +1. Reference Microsoft.Identity.Web.GraphServiceClient in your project. + ```shell + dotnet add package Microsoft.Identity.Web.GraphServiceClient + ``` + +1. In the startup method, add Microsoft Graph support to the service collection. + By default, the scopes are set to `User.Read` and the BaseUrl is "https://graph.microsoft.com/v1.0". + You can change them by passing a delegate to the `AddMicrosoftGraph` method (See below). + + Use the following namespace. + ```csharp + using Microsoft.Identity.Web; + ``` + + Add the Microsoft Graph + + ```csharp + services.AddMicrosoftGraph(); + ``` + + or, if you have described Microsoft Graph options in your configuration file: + ```json + "AzureAd": + { + // more here + }, + + "DownstreamApis": + { + "MicrosoftGraph": + { + // Specify BaseUrl if you want to use Microsoft graph in a national cloud. + // See https://learn.microsoft.com/graph/deployments#microsoft-graph-and-graph-explorer-service-root-endpoints + // "BaseUrl": "https://graph.microsoft.com/v1.0", + + // Set RequestAppToken this to "true" if you want to request an application token (to call graph on + // behalf of the application). The scopes will then automatically + // be ['https://graph.microsoft.com/.default']. + // "RequestAppToken": false + + // Set Scopes to request (unless you request an app token). + "Scopes": ["User.Read", "User.ReadBasic.All"] + + // See https://aka.ms/ms-id-web/downstreamApiOptions for all the properties you can set. + } + } + ``` + + The code to add Microsoft Graph based on the configuration is: + + ```csharp + services.AddMicrosoftGraph(); + services.Configure(options => + services.Configuration.GetSection("DownstreamApis:MicrosoftGraph")); + ``` + + or + + ```csharp + services.AddMicrosoftGraph(options => + services.Configuration.GetSection("DownstreamApis:MicrosoftGraph").Bind(options) ); + ``` + +2. Inject the GraphServiceClient from the constructor of controllers. + ```csharp + using Microsoft.Graph; + + public class HomeController : Controller + { + private readonly GraphServiceClient _graphServiceClient; + public HomeController(GraphServiceClient graphServiceClient) + { + _graphServiceClient = graphServiceClient; + } + } + ``` + +3. Use Microsoft Graph SDK to call Microsoft Graph. For example, to get the current user's profile: + ```csharp + var user = await _graphServiceClient.Me.GetAsync(); + ``` + +4. You can override the default options in the GetAsync(), PostAsync() etc.. methods. + For example to get the mail folders of the current user, you'll need to request more scopes ("Mail.Read"). + If your app registered several authentication schemes in ASP.NET Core, you'll also need to specify + which to authentication scheme to apply. + + ```csharp + var mailFolders = await _graphServiceClient.Me.MailFolders.GetAsync(r => + { + r.Options.WithScopes("Mail.Read") + .WithAuthenticationScheme(JwtBearerDefaults.AuthenticationScheme); + }); + ``` + + You could also write the same code as follows, which is more verbose, but enables you to set several options at once: + + ```csharp + var mailFolders = await _graphServiceClient.Me.MailFolders.GetAsync(r => + { + r.Options.WithAuthenticationOptions(o => + { + // Specify scopes for the request + o.Scopes = new string[] { "Mail.Read" }; + + // Specify the ASP.NET Core authentication scheme if needed (in the case + // of multiple authentication schemes) + o.AcquireTokenOptions.AuthenticationOptionsName = JwtBearerDefaults.AuthenticationScheme; + }); + }); + ``` + + If your app calls the Graph API on behalf of itself, you'll need to request an application token. + You do this by setting WithAppOnly. For instance to get the number of applications in the tenant: + + ```charp + int? appsInTenant = await _graphServiceClient.Applications.Count.GetAsync( + r => r.Options.WithAppOnly() ); + ``` + + which is a shortcut for: + + ```charp + int? appsInTenant = await _graphServiceClient.Applications.Count.GetAsync(r => + { + r.Options.WithAuthenticationOptions(o => + { + // Applications require app permissions, hence an app token + o.RequestAppToken = true; + }); + }); + ``` + +## You can now use both Microsoft Graph and Microsoft Graph Beta + +You can now use both Microsoft Graph and Microsoft Graph Beta in the same application: + +1. Reference both Microsoft.Identity.Web.GraphServiceClient and Microsoft.Identity.Web.GraphServiceClientBeta in your project + ```shell + dotnet add package Microsoft.Identity.Web.GraphServiceClient + dotnet add package Microsoft.Identity.Web.GraphServiceClientBeta + ``` + +1. In the startup method, add Microsoft Graph and Graph Beta to the service collection: + + ```csharp + services.AddMicrosoftGraph(); + services.AddMicrosoftGraphBeta(); + ``` + +1. In the controller or wherever you want to use them declare both GraphServiceClient and GraphServiceClientBeta + and inject them in the constructor: + + ```csharp + using GraphServiceClient = Microsoft.Graph.GraphServiceClient; + using GraphBetaServiceClient = Microsoft.Graph.GraphBetaServiceClient; + ``` + + ```csharp + MyController(GraphServiceClient graphServiceClient, GraphBetaServiceClient graphServiceClient) + { + // more here + } + ``` + +## Migrate from Microsoft.Identity.Web.MicrosoftGraph 2.x to Microsoft.Identity.Web.GraphServiceClient + +Microsoft.Identity.Web.GraphServiceClient is based on Microsoft.GraphSDK 5.x, which introduces breaking changes. +The Request() method has disappeared, and the extension methods it enabled in Microsoft.Identity.Web.MicrosoftGraph +are now moved to the GetAsync(), GetPost(), etc methods. + +The Microsoft Graph 4.x code: + +```csharp +var user = await _graphServiceClient.Me.Request().GetAsync(); +``` + +becomes with Microsoft.Graph 5.x: + +```csharp +var user = await _graphServiceClient.Me.GetAsync(); +``` + +The following paragraphs help you migrate from Microsoft.Identity.Web.MicrosoftGraph to Microsoft.Identity.Web.GraphServiceClient. + +### Replace the nuget packages + +1. Reference Microsoft.Identity.Web.GraphServiceClient in your project. + ```shell + dotnet remove package Microsoft.Identity.Web.MicrosoftGraph + dotnet add package Microsoft.Identity.Web.GraphServiceClient + ``` + +### Update the code + +In addition to the changes to the code due to the migration from Microsoft.Graph 4.x to Microsoft.Graph 5.x, you need to change the location of the +modifiers `.WithScopes()`, `.WithAppOnly()`, `WithAuthenticationScheme()` and `.WithAuthenticationOptions()`. + +#### WithScopes() + +In Microsoft.Identity.Web.MicrosoftGraph, you used to use `.WithScopes()` on the request to specify scopes to use to authenticate to Microsoft Graph: +```csharp +var messages = await _graphServiceClient.Users + .Request() + .WithScopes("User.Read.All") + .GetAsync(); +int NumberOfUsers = messages.Count; +``` + +With Microsoft.Identity.Web.GraphServiceClient, you need to call `.WithScopes()` on the options of the builder. + +```csharp +var messages = await _graphServiceClient.Users + .GetAsync(b => b.Options.WithScopes("User.Read.All")); +int NumberOfUsers = messages.Value.Count; +``` + +#### WithAppOnly() + +In Microsoft.Identity.Web.MicrosoftGraph 2.x, you could specify using app permissions (which require an app-only-token) by calling `.WithAppOnly()`. + +```csharp +var messages = await _graphServiceClient.Users + .Request() + .WithAppOnly() + .GetAsync(); +int NumberOfUsers = messages.Count; +``` + +With Microsoft.Identity.Web.GraphServiceClient, you need to call `.WithAppOnly()` on the options of the builder. + +```csharp +var messages = await _graphServiceClient.Users + .GetAsync(b => b.Options.WithAppOnly() )); +int NumberOfUsers = messages.Value.Count; +``` + +Note that this will use, under the hood, the scopes **["https://graph.microsoft.com/.default"]** which means all the pre-authorized scopes. You don't need +to specify these scopes, as this is the only possible when calling a Microsof Graph API requiring app permissions. + +#### WithAuthenticationScheme() in ASP.NET Core applications. + +If you are using Microsoft.Identity.Web.MicrosoftGraph in an ASP.NET Core application, you can specify the authentication scheme +to use by calling `WithAuthenticationScheme()`. + +```csharp +var messages = await _graphServiceClient.Users + .Request() + .WithAuthenticationScheme(JwtBearerDefaults.AuthenticationScheme) + .GetAsync(); +int NumberOfUsers = messages.Count; +``` + +With Microsoft.Identity.Web.GraphServiceClient, this becomes: + +```csharp +var messages = await _graphServiceClient.Users + .GetAsync(b => b.Options.WithAuthenticationScheme(JwtBearerDefaults.AuthenticationScheme) )); +int NumberOfUsers = messages.Value.Count; +``` + +More information about the migration from Microsoft Graph SDK 4.x to 5.x can be found in [Microsoft Graph .NET SDK v5 changelog and upgrade guide](https://github.com/microsoftgraph/msgraph-sdk-dotnet/blob/dev/docs/upgrade-to-v5.md) + +#### Other authentication options + +You can use .WithAuthenticationOptions() on the builder options. diff --git a/tools/Check-BrokenLinks.ps1 b/tools/Check-BrokenLinks.ps1 new file mode 100644 index 000000000..2f3b956c6 --- /dev/null +++ b/tools/Check-BrokenLinks.ps1 @@ -0,0 +1,210 @@ +<# +.SYNOPSIS + Checks for broken internal links in markdown documentation files. + +.DESCRIPTION + This script scans all markdown (.md) files in a directory and its subdirectories, + extracts internal links, and verifies that the target files exist. + External links (http/https), anchor-only links (#), and mailto links are skipped. + +.PARAMETER Path + The root directory to scan for markdown files. Defaults to the script's parent directory. + +.PARAMETER IncludeExternal + If specified, also checks external HTTP/HTTPS links for validity (slower). + +.PARAMETER OutputFormat + Output format: 'Table' (default), 'List', 'Json', or 'Csv' + +.EXAMPLE + .\Check-BrokenLinks.ps1 + Scans the docs folder for broken links and displays results in a table. + +.EXAMPLE + .\Check-BrokenLinks.ps1 -Path "C:\MyDocs" -OutputFormat Json + Scans a custom path and outputs results as JSON. + +.EXAMPLE + .\Check-BrokenLinks.ps1 | Export-Csv -Path "broken-links.csv" -NoTypeInformation + Exports broken links to a CSV file. +#> + +[CmdletBinding()] +param( + [Parameter(Position = 0)] + [string]$Path = (Split-Path $PSScriptRoot -Parent), + + [switch]$IncludeExternal, + + [ValidateSet('Table', 'List', 'Json', 'Csv')] + [string]$OutputFormat = 'Table' +) + +$ErrorActionPreference = 'Stop' + +function Test-MarkdownLink { + param( + [string]$SourceFile, + [string]$LinkTarget + ) + + # Skip external links, anchors, and mailto + if ($LinkTarget -match '^https?://' -or + $LinkTarget -match '^#' -or + $LinkTarget -match '^mailto:') { + return $null + } + + # Handle anchor links (file.md#section) + $targetPath = ($LinkTarget -split '#')[0] + + # Skip empty paths (pure anchor links handled above) + if (-not $targetPath.Trim()) { + return $null + } + + # Resolve the full path relative to the source file + $sourceDir = Split-Path $SourceFile -Parent + $fullPath = Join-Path $sourceDir $targetPath + + # Normalize the path + try { + $normalizedPath = [System.IO.Path]::GetFullPath($fullPath) + } + catch { + return [PSCustomObject]@{ + SourceFile = $SourceFile + LinkTarget = $LinkTarget + Status = 'Invalid Path' + Exists = $false + } + } + + # Check if file/directory exists + $exists = Test-Path $normalizedPath + + if (-not $exists) { + return [PSCustomObject]@{ + SourceFile = $SourceFile + LinkTarget = $LinkTarget + Status = 'Not Found' + Exists = $false + } + } + + return $null +} + +function Test-ExternalLink { + param([string]$Url) + + try { + $response = Invoke-WebRequest -Uri $Url -Method Head -TimeoutSec 10 -UseBasicParsing -ErrorAction Stop + return $response.StatusCode -eq 200 + } + catch { + return $false + } +} + +# Main script +Write-Host "`nπŸ“‚ Scanning for broken links in: $Path`n" -ForegroundColor Cyan + +$brokenLinks = @() +$fileCount = 0 +$linkCount = 0 + +# Get all markdown files +$mdFiles = Get-ChildItem -Path $Path -Recurse -Filter "*.md" -File + +foreach ($file in $mdFiles) { + $fileCount++ + $content = Get-Content $file.FullName -Raw -ErrorAction SilentlyContinue + + if (-not $content) { continue } + + # Extract all markdown links: [text](target) + $linkMatches = [regex]::Matches($content, '\[([^\]]+)\]\(([^)]+)\)') + + foreach ($match in $linkMatches) { + $linkText = $match.Groups[1].Value + $linkTarget = $match.Groups[2].Value + $linkCount++ + + # Check internal links + $result = Test-MarkdownLink -SourceFile $file.FullName -LinkTarget $linkTarget + + if ($result) { + $result | Add-Member -NotePropertyName 'LinkText' -NotePropertyValue $linkText + $result | Add-Member -NotePropertyName 'RelativeSource' -NotePropertyValue ($file.FullName -replace [regex]::Escape("$Path\"), '') + $brokenLinks += $result + } + + # Optionally check external links + if ($IncludeExternal -and $linkTarget -match '^https?://') { + Write-Host " Checking external: $linkTarget" -ForegroundColor Gray + if (-not (Test-ExternalLink -Url $linkTarget)) { + $brokenLinks += [PSCustomObject]@{ + SourceFile = $file.FullName + RelativeSource = ($file.FullName -replace [regex]::Escape("$Path\"), '') + LinkTarget = $linkTarget + LinkText = $linkText + Status = 'External Link Failed' + Exists = $false + } + } + } + } +} + +# Output results +Write-Host "πŸ“Š Scan complete!" -ForegroundColor Green +Write-Host " Files scanned: $fileCount" +Write-Host " Links checked: $linkCount" +Write-Host " Broken links: $($brokenLinks.Count)`n" -ForegroundColor $(if ($brokenLinks.Count -gt 0) { 'Yellow' } else { 'Green' }) + +if ($brokenLinks.Count -eq 0) { + Write-Host "βœ… No broken links found!" -ForegroundColor Green + return +} + +# Format output +$output = $brokenLinks | Select-Object RelativeSource, LinkTarget, Status | Sort-Object RelativeSource, LinkTarget + +switch ($OutputFormat) { + 'Table' { + $output | Format-Table -AutoSize -Wrap + } + 'List' { + $output | Format-List + } + 'Json' { + $output | ConvertTo-Json -Depth 3 + } + 'Csv' { + $output | ConvertTo-Csv -NoTypeInformation + } +} + +# Group by missing target pattern for summary +Write-Host "`nπŸ“‹ Summary by missing path pattern:" -ForegroundColor Cyan +$brokenLinks | + Group-Object { + $target = $_.LinkTarget + if ($target -match '^\.\./scenarios/') { 'scenarios/*' } + elseif ($target -match '^\.\./deployment/') { 'deployment/*' } + elseif ($target -match '^\.\./migration/') { 'migration/*' } + elseif ($target -match '^\.\./packages/') { 'packages/*' } + elseif ($target -match 'README\.md$') { '*/README.md references' } + elseif ($target -match '^\./') { 'Same-directory files' } + else { 'Other' } + } | + Sort-Object Count -Descending | + ForEach-Object { + Write-Host " $($_.Count) - $($_.Name)" -ForegroundColor Yellow + } + +Write-Host "" + +# Return the broken links for pipeline usage +return $output