diff --git a/.github/skills/README.md b/.github/skills/README.md new file mode 100644 index 000000000..75a75019a --- /dev/null +++ b/.github/skills/README.md @@ -0,0 +1,124 @@ +# AI Coding Assistant Skills for Microsoft.Identity.Web + +This folder contains **Skills** - specialized knowledge modules that help AI coding assistants provide better assistance for specific scenarios. + +## What Are Skills? + +Skills are an **open standard** for sharing domain-specific knowledge with AI coding assistants. They are markdown files with structured guidance that AI assistants use when helping with specific tasks. Unlike general instructions, skills are **scenario-specific** and activated when the assistant detects relevant context (keywords, file patterns, or explicit requests). + +### Supported AI Assistants + +Skills work with multiple AI coding assistants that support the open skills format: + +- **GitHub Copilot** - Native support in VS Code, Visual Studio, GitHub Copilot CLI, and other IDEs +- **Claude** (Anthropic) - Via Claude for VS Code extension and Claude Code +- **Other assistants** - Any AI tool that follows the skills convention + +## Available Skills + +| Skill | Description | Full Guide | +|-------|-------------|------------| +| [entra-id-aspire-authentication](./entra-id-aspire-authentication/SKILL.md) | Adding Microsoft Entra ID authentication to .NET Aspire applications | [Aspire Integration Guide](../../docs/frameworks/aspire.md) | +| [entra-id-aspire-provisioning](./entra-id-aspire-provisioning/SKILL.md) | Provisioning Entra ID app registrations for Aspire apps using Microsoft Graph PowerShell | [Aspire Integration Guide](../../docs/frameworks/aspire.md) | + +> **šŸ’” Tip:** Skills are condensed versions optimized for AI assistants. For comprehensive documentation with detailed explanations, diagrams, and troubleshooting, see the linked full guides. +> +> **šŸ”„ Two-phase workflow:** Use the **authentication skill** first to add code (Phase 1), then the **provisioning skill** to create app registrations (Phase 2). + +## How to Use Skills + +### Option 1: Repository-Level (Recommended for Teams) + +Copy the skill folder to your project's `.github/skills/` directory: + +``` +your-repo/ +ā”œā”€ā”€ .github/ +│ └── skills/ +│ └── entra-id-aspire-authentication/ +│ └── SKILL.md +``` + +Copilot will automatically use this skill when working in your repository. + +### Option 2: User-Level (Personal Setup) + +Install skills globally so they're available across all your projects: + +**Windows:** +```powershell +# Create the skills directory +mkdir "$env:USERPROFILE\.github\skills\entra-id-aspire-authentication" -Force + +# Copy the skill (or download from this repo) +Copy-Item "SKILL.md" "$env:USERPROFILE\.github\skills\entra-id-aspire-authentication\" +``` + +Location: `%USERPROFILE%\.github\skills\` + +**Linux / macOS:** +```bash +# Create the skills directory +mkdir -p ~/.github/skills/entra-id-aspire-authentication + +# Copy the skill (or download from this repo) +cp SKILL.md ~/.github/skills/entra-id-aspire-authentication/ +``` + +Location: `~/.github/skills/` + +### Option 3: Reference in Chat + +You can also explicitly tell Copilot to use a skill: + +> "Using the entra-id-aspire-authentication skill, add authentication to my Aspire app" + +## Skill File Structure + +Each skill follows this structure: + +```markdown +--- +name: skill-name +description: When Copilot should use this skill +license: MIT +--- + +# Skill Title + +## When to Use This Skill +- Trigger condition 1 +- Trigger condition 2 + +## Implementation Guide +... +``` + +The YAML frontmatter helps AI assistants understand when to apply the skill. + +## Creating New Skills + +1. Create a folder under `.github/skills/` with your skill name +2. Add a `SKILL.md` file with: + - YAML frontmatter (`name`, `description`, `license`) + - Clear "When to Use" section + - Step-by-step implementation guidance + - Code examples and configuration snippets + - Troubleshooting tips + +## Skills vs. Instructions + +| Aspect | Instructions file | Skills | +|--------|-------------------|--------| +| Scope | Always active for the repo | Activated by context/keywords | +| Purpose | General coding standards | Specific implementation scenarios | +| Location | `.github/copilot-instructions.md` | `.github/skills//SKILL.md` | +| Content | Style guides, conventions | Step-by-step tutorials, patterns | +| Standard | Varies by AI assistant | Open standard across assistants | + +## Resources + +- [Microsoft.Identity.Web Documentation](../../docs/README.md) +- [Aspire Integration Guide](../../docs/frameworks/aspire.md) +- [GitHub copilot skills](https://docs.github.com/en/copilot/concepts/agents/about-agent-skills) +- [GitHub Copilot Documentation](https://docs.github.com/copilot) diff --git a/.github/skills/entra-id-aspire-authentication/BlazorAuthenticationChallengeHandler.cs b/.github/skills/entra-id-aspire-authentication/BlazorAuthenticationChallengeHandler.cs new file mode 100644 index 000000000..cce77251d --- /dev/null +++ b/.github/skills/entra-id-aspire-authentication/BlazorAuthenticationChallengeHandler.cs @@ -0,0 +1,120 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Authorization; + +namespace Microsoft.Identity.Web; + +/// +/// Handles authentication challenges for Blazor Server components. +/// Provides functionality for incremental consent and Conditional Access scenarios. +/// +public class BlazorAuthenticationChallengeHandler( + NavigationManager navigation, + AuthenticationStateProvider authenticationStateProvider, + IConfiguration configuration) +{ + private const string MsaTenantId = "9188040d-6c67-4c5b-b112-36a304b66dad"; + + /// + /// Gets the current user's authentication state. + /// + public async Task GetUserAsync() + { + var authState = await authenticationStateProvider.GetAuthenticationStateAsync(); + return authState.User; + } + + /// + /// Checks if the current user is authenticated. + /// + public async Task IsAuthenticatedAsync() + { + var user = await GetUserAsync(); + return user.Identity?.IsAuthenticated == true; + } + + /// + /// Handles exceptions that may require user re-authentication. + /// Returns true if a challenge was initiated, false otherwise. + /// + public async Task HandleExceptionAsync(Exception exception) + { + var challengeException = exception as MicrosoftIdentityWebChallengeUserException + ?? exception.InnerException as MicrosoftIdentityWebChallengeUserException; + + if (challengeException != null) + { + var user = await GetUserAsync(); + ChallengeUser(user, challengeException.Scopes, challengeException.MsalUiRequiredException?.Claims); + return true; + } + + return false; + } + + /// + /// Initiates a challenge to authenticate the user or request additional consent. + /// + public void ChallengeUser(ClaimsPrincipal user, string[]? scopes = null, string? claims = null) + { + var currentUri = navigation.Uri; + + // Build scopes string (add OIDC scopes) + var allScopes = (scopes ?? []) + .Union(["openid", "offline_access", "profile"]) + .Distinct(); + var scopeString = Uri.EscapeDataString(string.Join(" ", allScopes)); + + // Get login hint from user claims + var loginHint = Uri.EscapeDataString(GetLoginHint(user)); + + // Get domain hint + var domainHint = Uri.EscapeDataString(GetDomainHint(user)); + + // Build the challenge URL + var challengeUrl = $"/authentication/login?returnUrl={Uri.EscapeDataString(currentUri)}" + + $"&scope={scopeString}" + + $"&loginHint={loginHint}" + + $"&domainHint={domainHint}"; + + // Add claims if present (for Conditional Access) + if (!string.IsNullOrEmpty(claims)) + { + challengeUrl += $"&claims={Uri.EscapeDataString(claims)}"; + } + + navigation.NavigateTo(challengeUrl, forceLoad: true); + } + + /// + /// Initiates a challenge with scopes from configuration. + /// + public async Task ChallengeUserWithConfiguredScopesAsync(string configurationSection) + { + var user = await GetUserAsync(); + var scopes = configuration.GetSection(configurationSection).Get(); + ChallengeUser(user, scopes); + } + + private static string GetLoginHint(ClaimsPrincipal user) + { + return user.FindFirst("preferred_username")?.Value ?? + user.FindFirst("login_hint")?.Value ?? + string.Empty; + } + + private static string GetDomainHint(ClaimsPrincipal user) + { + var tenantId = user.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid")?.Value ?? + user.FindFirst("tid")?.Value; + + if (string.IsNullOrEmpty(tenantId)) + return "organizations"; + + // MSA tenant + if (tenantId == MsaTenantId) + return "consumers"; + + return "organizations"; + } +} diff --git a/.github/skills/entra-id-aspire-authentication/LoginLogoutEndpointRouteBuilderExtensions.cs b/.github/skills/entra-id-aspire-authentication/LoginLogoutEndpointRouteBuilderExtensions.cs new file mode 100644 index 000000000..e2b447896 --- /dev/null +++ b/.github/skills/entra-id-aspire-authentication/LoginLogoutEndpointRouteBuilderExtensions.cs @@ -0,0 +1,89 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.Identity.Web; + +/// +/// Extension methods for mapping login and logout endpoints that support +/// incremental consent and Conditional Access scenarios. +/// +public static class LoginLogoutEndpointRouteBuilderExtensions +{ + /// + /// Maps login and logout endpoints under the current route group. + /// The login endpoint supports incremental consent via scope, loginHint, domainHint, and claims parameters. + /// + /// The endpoint route builder. + /// The endpoint convention builder for further configuration. + public static IEndpointConventionBuilder MapLoginAndLogout(this IEndpointRouteBuilder endpoints) + { + var group = endpoints.MapGroup(""); + + // Enhanced login endpoint that supports incremental consent and Conditional Access + group.MapGet("/login", ( + string? returnUrl, + string? scope, + string? loginHint, + string? domainHint, + string? claims) => + { + var properties = GetAuthProperties(returnUrl); + + // Add scopes if provided (for incremental consent) + if (!string.IsNullOrEmpty(scope)) + { + var scopes = scope.Split(' ', StringSplitOptions.RemoveEmptyEntries); + properties.SetParameter(OpenIdConnectParameterNames.Scope, scopes); + } + + // Add login hint (pre-fills username) + if (!string.IsNullOrEmpty(loginHint)) + { + properties.SetParameter(OpenIdConnectParameterNames.LoginHint, loginHint); + } + + // Add domain hint (skips home realm discovery) + if (!string.IsNullOrEmpty(domainHint)) + { + properties.SetParameter(OpenIdConnectParameterNames.DomainHint, domainHint); + } + + // Add claims challenge (for Conditional Access / step-up auth) + if (!string.IsNullOrEmpty(claims)) + { + properties.Items["claims"] = claims; + } + + return TypedResults.Challenge(properties, [OpenIdConnectDefaults.AuthenticationScheme]); + }) + .AllowAnonymous(); + + group.MapPost("/logout", async (HttpContext context) => + { + string? returnUrl = null; + if (context.Request.HasFormContentType) + { + var form = await context.Request.ReadFormAsync(); + returnUrl = form["ReturnUrl"]; + } + + return TypedResults.SignOut(GetAuthProperties(returnUrl), + [CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectDefaults.AuthenticationScheme]); + }) + .DisableAntiforgery(); + + return group; + } + + private static AuthenticationProperties GetAuthProperties(string? returnUrl) + { + const string pathBase = "/"; + if (string.IsNullOrEmpty(returnUrl)) returnUrl = pathBase; + else if (returnUrl.StartsWith("//", StringComparison.Ordinal)) returnUrl = pathBase; // Prevent protocol-relative redirects + else if (!Uri.IsWellFormedUriString(returnUrl, UriKind.Relative)) returnUrl = new Uri(returnUrl, UriKind.Absolute).PathAndQuery; + else if (returnUrl[0] != '/') returnUrl = $"{pathBase}{returnUrl}"; + return new AuthenticationProperties { RedirectUri = returnUrl }; + } +} diff --git a/.github/skills/entra-id-aspire-authentication/SKILL.md b/.github/skills/entra-id-aspire-authentication/SKILL.md new file mode 100644 index 000000000..11d3d16c1 --- /dev/null +++ b/.github/skills/entra-id-aspire-authentication/SKILL.md @@ -0,0 +1,550 @@ +--- +name: entra-id-aspire-authentication +description: | + Guide for adding Microsoft Entra ID (Azure AD) authentication to .NET Aspire applications. + Use this when asked to add authentication, Entra ID, Azure AD, OIDC, or identity to an Aspire app, + or when working with Microsoft.Identity.Web in Aspire projects. +license: MIT +--- + +# Entra ID Authentication for .NET Aspire Applications + +This skill helps you integrate **Microsoft Entra ID** (Azure AD) authentication into **.NET Aspire** distributed applications using **Microsoft.Identity.Web**. + +## When to Use This Skill + +- Adding user authentication to Aspire apps +- Protecting APIs with JWT Bearer authentication +- Configuring OIDC sign-in for Blazor Server +- Setting up token acquisition for downstream API calls +- Implementing service-to-service authentication + +## Architecture Overview + +``` +User Browser → Blazor Server (OIDC) → Entra ID → Access Token → Protected API (JWT) +``` + +**Key Components:** +- **Blazor Frontend**: Uses `AddMicrosoftIdentityWebApp` for OIDC + `MicrosoftIdentityMessageHandler` for token attachment +- **API Backend**: Uses `AddMicrosoftIdentityWebApi` for JWT validation +- **Aspire**: Service discovery with `https+http://servicename` URLs + +--- + +## Pre-Implementation Checklist + +Before starting, the agent MUST: + +### 1. Detect Project Types + +Scan each project's `Program.cs` to identify its type: + +```powershell +# Find all Program.cs files in solution +Get-ChildItem -Recurse -Filter "Program.cs" | ForEach-Object { + $content = Get-Content $_.FullName -Raw + $projectDir = Split-Path $_.FullName -Parent + $projectName = Split-Path $projectDir -Leaf + + # Skip AppHost and ServiceDefaults + if ($projectName -match "AppHost|ServiceDefaults") { return } + + $isWebApp = $content -match "AddRazorComponents|MapRazorComponents|AddServerSideBlazor" + $isApi = $content -match "MapGet|MapPost|MapPut|MapDelete|AddControllers" + + if ($isWebApp) { + Write-Host "WEB APP: $projectName (has Razor/Blazor components)" + } elseif ($isApi) { + Write-Host "API: $projectName (exposes endpoints)" + } +} +``` + +**Detection rules:** +| Pattern in `Program.cs` | Project Type | +|------------------------|--------------| +| `AddRazorComponents` / `MapRazorComponents` / `AddServerSideBlazor` | **Blazor Web App** | +| `MapGet` / `MapPost` / `AddControllers` (without Razor) | **Web API** | + +> **Note:** APIs can call other APIs (downstream). The Aspire `.WithReference()` shows service dependencies, not necessarily web-to-API relationships. + +### 2. Confirm with User + +**AGENT: Show detected topology and ask for confirmation:** +> "I detected: +> - **Web App** (Blazor): `{webProjectName}` +> - **API**: `{apiProjectName}` +> +> The web app will authenticate users and call the API. Is this correct?" + +### 3. Establish Workflow + +**AGENT: Explain the two-phase approach:** +> "I'll implement authentication in two phases: +> +> **Phase 1 (now):** Add authentication code with placeholder values. The app will **build** but won't **run** until app registrations are configured. +> +> **Phase 2 (after):** Use the `entra-id-aspire-provisioning` skill to create Entra ID app registrations and update the configuration with real values. +> +> Ready to proceed with Phase 1?" + +--- + +## Implementation Checklist + +**CRITICAL: Complete ALL steps in order. Do not skip any step.** + +### API Project Steps +- [ ] Step 1.1: Add Microsoft.Identity.Web package +- [ ] Step 1.2: Update appsettings.json with AzureAd section +- [ ] Step 1.3: Update Program.cs with JWT Bearer authentication +- [ ] Step 1.4: Add RequireAuthorization() to protected endpoints + +### Web/Blazor Project Steps +- [ ] Step 2.1: Add Microsoft.Identity.Web package +- [ ] Step 2.2: Update appsettings.json with AzureAd and scopes +- [ ] Step 2.3: Update Program.cs with OIDC, token acquisition, and **BlazorAuthenticationChallengeHandler** +- [ ] Step 2.4: Copy LoginLogoutEndpointRouteBuilderExtensions.cs from skill folder (adds incremental consent support) +- [ ] Step 2.5: Copy BlazorAuthenticationChallengeHandler.cs from skill folder +- [ ] Step 2.6: Create UserInfo.razor component (LOGIN BUTTON) +- [ ] Step 2.7: Update MainLayout.razor to include UserInfo +- [ ] Step 2.8: Update Routes.razor with AuthorizeRouteView +- [ ] Step 2.9: Store client secret in user-secrets +- [ ] Step 2.10: Add try/catch with ChallengeHandler on **every page calling APIs** + +--- + +## Step-by-Step Implementation + +### Prerequisites + +1. .NET Aspire solution with API and Web (Blazor) projects +2. Azure AD tenant + +> **Two-phase workflow:** +> - **Phase 1**: Add authentication code with placeholder values → App will **build** but **not run** +> - **Phase 2**: Run `entra-id-aspire-provisioning` skill to create app registrations → App will **run** + +### Part 1: Protect the API with JWT Bearer + +**1.1 Add Package:** +```powershell +cd MyService.ApiService +dotnet add package Microsoft.Identity.Web +``` + +**1.2 Configure `appsettings.json`:** +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "", + "ClientId": "", + "Audiences": ["api://"] + } +} +``` + +**1.3 Update `Program.cs`:** +```csharp +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Identity.Web; + +var builder = WebApplication.CreateBuilder(args); +builder.AddServiceDefaults(); + +// Add JWT Bearer authentication +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")); + +builder.Services.AddAuthorization(); + +var app = builder.Build(); + +app.UseAuthentication(); +app.UseAuthorization(); + +// Protect endpoints +app.MapGet("/weatherforecast", () => { /* ... */ }) + .RequireAuthorization(); + +app.Run(); +``` + +### Part 2: Configure Blazor Frontend + +**2.1 Add Package:** +```powershell +cd MyService.Web +dotnet add package Microsoft.Identity.Web +``` + +**2.2 Configure `appsettings.json`:** +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "Domain": ".onmicrosoft.com", + "TenantId": "", + "ClientId": "", + "CallbackPath": "/signin-oidc", + "ClientCredentials": [ + { + "SourceType": "ClientSecret", + "ClientSecret": "" + } + ] + }, + "WeatherApi": { + "Scopes": ["api:///.default"] + } +} +``` + +**2.3 Update `Program.cs`:** +```csharp +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.Identity.Web; + +var builder = WebApplication.CreateBuilder(args); +builder.AddServiceDefaults(); + +// Authentication + token acquisition +builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd")) + .EnableTokenAcquisitionToCallDownstreamApi() + .AddInMemoryTokenCaches(); + +builder.Services.AddCascadingAuthenticationState(); +builder.Services.AddRazorComponents().AddInteractiveServerComponents(); + +// Add Blazor authentication challenge handler for incremental consent and Conditional Access +builder.Services.AddScoped(); + +// HttpClient with automatic token attachment +builder.Services.AddHttpClient(client => +{ + client.BaseAddress = new("https+http://apiservice"); // Aspire service discovery +}) +.AddMicrosoftIdentityMessageHandler(builder.Configuration.GetSection("WeatherApi").Bind); + +var app = builder.Build(); + +app.UseAuthentication(); +app.UseAuthorization(); +app.UseAntiforgery(); + +app.MapRazorComponents().AddInteractiveServerRenderMode(); +app.MapGroup("/authentication").MapLoginAndLogout(); + +app.Run(); +``` + +**2.4 Copy `LoginLogoutEndpointRouteBuilderExtensions.cs` from skill folder:** + +Copy the `LoginLogoutEndpointRouteBuilderExtensions.cs` file from this skill's folder to the Web project root. + +> **AGENT:** The file is located at `./LoginLogoutEndpointRouteBuilderExtensions.cs`. The namespace is `Microsoft.Identity.Web` so no changes are needed — it provides the `MapLoginAndLogout()` extension method with support for incremental consent and Conditional Access. + +**2.5 Copy `BlazorAuthenticationChallengeHandler.cs` from skill folder:** + +Copy the `BlazorAuthenticationChallengeHandler.cs` file from this skill's folder to the Web project root. + +> **AGENT:** The file is located at `./BlazorAuthenticationChallengeHandler.cs`. The namespace is `Microsoft.Identity.Web` so no changes are needed — it will be available once Microsoft.Identity.Web is referenced. + +**2.6 Create UserInfo Component (`Components/UserInfo.razor`) — THE LOGIN BUTTON:** + +> **CRITICAL: This step is frequently forgotten. Without this, users have no way to log in!** + +```razor +@using Microsoft.AspNetCore.Components.Authorization + + + + Hello, @context.User.Identity?.Name + + + Login + + +``` + +**2.7 Update MainLayout.razor to include UserInfo:** + +Find the `
` or navigation section in `Components/Layout/MainLayout.razor` and add the UserInfo component: + +```razor +@inherits LayoutComponentBase + +
+ + +
+
+ @* <-- ADD THIS LINE *@ +
+ +
+ @Body +
+
+
+``` + +**2.8 Update Routes.razor for AuthorizeRouteView:** + +Replace `RouteView` with `AuthorizeRouteView` in `Components/Routes.razor`: + +```razor +@using Microsoft.AspNetCore.Components.Authorization + + + + + +

You are not authorized to view this page.

+ Login +
+
+ +
+
+``` + +**2.9 Store Client Secret in User Secrets:** + +> **Never commit secrets to source control!** + +```powershell +cd MyService.Web +dotnet user-secrets init +dotnet user-secrets set "AzureAd:ClientCredentials:0:ClientSecret" "" +``` + +Then update `appsettings.json` to reference user secrets (remove the hardcoded secret): +```jsonc +{ + "AzureAd": { + "ClientCredentials": [ + { + // For more options see https://aka.ms/ms-id-web/credentials + "SourceType": "ClientSecret" + } + ] + } +} +``` + +--- + +## Common Patterns + +### Protect Blazor Pages +```razor +@page "/weather" +@attribute [Authorize] +``` + +### Scope Validation in API +```csharp +app.MapGet("/weatherforecast", () => { /* ... */ }) + .RequireAuthorization() + .RequireScope("access_as_user"); +``` + +### App-Only Tokens (Service-to-Service) +```csharp +.AddMicrosoftIdentityMessageHandler(options => +{ + options.Scopes.Add("api:///.default"); + options.RequestAppToken = true; +}); +``` + +### Override Scopes Per Request +```csharp +var request = new HttpRequestMessage(HttpMethod.Get, "/endpoint") + .WithAuthenticationOptions(options => + { + options.Scopes.Clear(); + options.Scopes.Add("api:///specific.scope"); + }); +``` + +### Production: Use Managed Identity +```json +{ + "AzureAd": { + "ClientCredentials": [ + { + "SourceType": "SignedAssertionFromManagedIdentity", + "ManagedIdentityClientId": "" + } + ] + } +} +``` + +### On-Behalf-Of (API calling downstream APIs) +```csharp +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")) + .EnableTokenAcquisitionToCallDownstreamApi() + .AddInMemoryTokenCaches(); + +builder.Services.AddDownstreamApi("GraphApi", builder.Configuration.GetSection("GraphApi")); +``` + +### Handle Conditional Access / MFA / Incremental Consent + +> **This is NOT optional** — Blazor Server requires explicit exception handling for Conditional Access and consent. + +When calling APIs, Conditional Access policies or consent requirements can trigger `MicrosoftIdentityWebChallengeUserException`. You MUST handle this on **every page that calls a downstream API**. + +**Step 2.3 registers the handler** — `AddScoped()` makes the service available. + +**Each page calling APIs needs this pattern:** + +```razor +@page "/weather" +@attribute [Authorize] + +@using Microsoft.AspNetCore.Authorization +@using Microsoft.Identity.Web + +@inject WeatherApiClient WeatherApi +@inject BlazorAuthenticationChallengeHandler ChallengeHandler + +Weather + +

Weather

+ +@if (!string.IsNullOrEmpty(errorMessage)) +{ +
@errorMessage
+} +else if (forecasts == null) +{ +

Loading...

+} +else +{ + @* Display your data *@ +} + +@code { + private WeatherForecast[]? forecasts; + private string? errorMessage; + + protected override async Task OnInitializedAsync() + { + if (!await ChallengeHandler.IsAuthenticatedAsync()) + { + // Not authenticated - redirect to login with required scopes + await ChallengeHandler.ChallengeUserWithConfiguredScopesAsync("WeatherApi:Scopes"); + return; + } + + try + { + forecasts = await WeatherApi.GetWeatherAsync(); + } + catch (Exception ex) + { + // Handle incremental consent / Conditional Access + if (!await ChallengeHandler.HandleExceptionAsync(ex)) + { + errorMessage = $"Error loading data: {ex.Message}"; + } + } + } +} +``` + +> **Why this pattern?** +> 1. `IsAuthenticatedAsync()` checks if user is signed in before making API calls +> 2. `HandleExceptionAsync()` catches `MicrosoftIdentityWebChallengeUserException` (or as InnerException) +> 3. If it is a challenge exception → redirects user to re-authenticate with required claims/scopes +> 4. If it is NOT a challenge exception → returns false so you can handle the error + +> **Why is this not automatic?** Blazor Server's circuit-based architecture requires explicit handling. The handler re-challenges the user by navigating to the login endpoint with the required claims/scopes. + +--- + +## Troubleshooting + +| Issue | Solution | +|-------|----------| +| 401 on API calls | Verify scopes match the API's App ID URI | +| OIDC redirect fails | Add `/signin-oidc` to Azure AD redirect URIs | +| Token not attached | Ensure `AddMicrosoftIdentityMessageHandler` is configured | +| AADSTS65001 | Admin consent required - grant in Azure Portal | +| 404 on `/MicrosoftIdentity/Account/Challenge` | Use `BlazorAuthenticationChallengeHandler` instead of `MicrosoftIdentityConsentHandler` | + +--- + +## Key Files to Modify + +| Project | File | Purpose | +|---------|------|---------| +| ApiService | `Program.cs` | JWT auth + `RequireAuthorization()` | +| ApiService | `appsettings.json` | AzureAd config (ClientId, TenantId) | +| Web | `Program.cs` | OIDC + token acquisition + challenge handler registration | +| Web | `appsettings.json` | AzureAd config + downstream API scopes | +| Web | `LoginLogoutEndpointRouteBuilderExtensions.cs` | Login/logout with incremental consent support (**copy from skill**) | +| Web | `BlazorAuthenticationChallengeHandler.cs` | Reusable auth challenge handler (**copy from skill**) | +| Web | `Components/UserInfo.razor` | **Login/logout button UI** | +| Web | `Components/Layout/MainLayout.razor` | Include UserInfo in layout | +| Web | `Components/Routes.razor` | AuthorizeRouteView for protected pages | + +--- + +## Post-Implementation Verification + +**AGENT: After completing all steps, verify:** + +1. **Build succeeds:** + ```powershell + dotnet build + ``` + +2. **Check all files were created/modified:** + - [ ] API `Program.cs` has `AddMicrosoftIdentityWebApi` + - [ ] API `appsettings.json` has `AzureAd` section + - [ ] Web `Program.cs` has `AddMicrosoftIdentityWebApp` and `AddMicrosoftIdentityMessageHandler` + - [ ] Web `Program.cs` has `AddScoped()` + - [ ] Web `appsettings.json` has `AzureAd` and scope configuration + - [ ] Web has `LoginLogoutEndpointRouteBuilderExtensions.cs` (with incremental consent params) + - [ ] Web has `BlazorAuthenticationChallengeHandler.cs` + - [ ] Web has `Components/UserInfo.razor` (**LOGIN BUTTON**) + - [ ] Web `MainLayout.razor` includes `` + - [ ] Web `Routes.razor` uses `AuthorizeRouteView` + - [ ] **Every page calling protected APIs** has try/catch with `ChallengeHandler.HandleExceptionAsync(ex)` + +3. **AGENT: Inform user of next step:** + > "āœ… **Phase 1 complete!** Authentication code is in place. The app will **build** but **won't run** until app registrations are configured. + > + > **Next:** Run the `entra-id-aspire-provisioning` skill to: + > - Create Entra ID app registrations + > - Update `appsettings.json` with real ClientIds + > - Store client secret securely + > + > Ready to proceed with provisioning?" + +--- + +## Resources + +- šŸ“– **[Full Aspire Integration Guide](https://github.com/AzureAD/microsoft-identity-web/blob/master/docs/frameworks/aspire.md)** - Comprehensive documentation with diagrams, detailed explanations, and advanced scenarios +- [Microsoft.Identity.Web Documentation](https://github.com/AzureAD/microsoft-identity-web/tree/master/docs) +- [MicrosoftIdentityMessageHandler Guide](https://github.com/AzureAD/microsoft-identity-web/blob/master/docs/calling-downstream-apis/custom-apis.md#microsoftidentitymessagehandler---for-httpclient-integration) +- [.NET Aspire Service Discovery](https://learn.microsoft.com/dotnet/aspire/service-discovery/overview) +- [Credentials Guide](https://github.com/AzureAD/microsoft-identity-web/blob/master/docs/authentication/credentials/credentials-README.md) diff --git a/.github/skills/entra-id-aspire-provisioning/SKILL.md b/.github/skills/entra-id-aspire-provisioning/SKILL.md new file mode 100644 index 000000000..1f7051d86 --- /dev/null +++ b/.github/skills/entra-id-aspire-provisioning/SKILL.md @@ -0,0 +1,811 @@ +--- +name: entra-id-aspire-provisioning +description: | + Provision Entra ID (Azure AD) app registrations for .NET Aspire applications and update configuration. + Use after adding Microsoft.Identity.Web authentication code to create or update app registrations, + configure scopes, credentials, and update appsettings.json files. + Triggers: "provision entra id", "create app registration", "register azure ad app", + "configure entra id apps", "set up authentication apps". +--- + +# Entra ID Provisioning for .NET Aspire + +Provision Entra ID app registrations for Aspire solutions and update `appsettings.json` configuration. + +## Prerequisites + +### Install Microsoft Graph PowerShell + +```powershell +# Install the required modules (only if needed, one-time setup) +Install-Module Microsoft.Graph.Applications -Scope CurrentUser -Force +Install-Module Microsoft.Graph.Identity.SignIns -Scope CurrentUser -Force + +# Note: Microsoft.Graph.Users is NOT required - this skill uses Invoke-MgGraphRequest +# to get current user info, which avoids module version compatibility issues. +``` + +### Connect to Microsoft Graph + +```powershell +# Connect with required scopes +Connect-MgGraph -Scopes "Application.ReadWrite.All", "Directory.ReadWrite.All" + +# Verify connection +Get-MgContext +``` + +> **Note**: You may be prompted to consent to permissions on first use. + +## Provisioning Checklist + +Use this checklist to verify all provisioning steps are complete: + +### For Each Web API Project +- [ ] App registration created (or existing one found or user-provided) +- [ ] App ID URI set (`api://{clientId}`) +- [ ] `access_as_user` scope configured +- [ ] Service principal created +- [ ] Current user added as owner +- [ ] `appsettings.json` updated with `TenantId` and `ClientId` + +### For Each Web App Project +- [ ] App registration created (or existing one found or user-provided) +- [ ] Redirect URIs configured (from `launchSettings.json`) +- [ ] Client secret generated and stored in user-secrets +- [ ] API permission added (to call the web API) +- [ ] Admin consent granted (or manual steps provided) +- [ ] Service principal created +- [ ] Current user added as owner +- [ ] `appsettings.json` updated with `TenantId`, `ClientId`, and `Scopes` + +### Final Verification +- [ ] API provisioned **before** web app (web app needs API's ClientId and ScopeId) +- [ ] All `appsettings.json` files have real GUIDs (no placeholders) +- [ ] Client secret stored in user-secrets (not in `appsettings.json`) +- [ ] `Disconnect-MgGraph` called when done + +## When to Use This Skill + +Use this skill **after** the `entra-id-aspire-authentication` skill has added authentication code. This skill: +- Creates or updates Entra ID app registrations +- Configures App ID URIs and scopes for APIs +- Sets up redirect URIs for web apps +- Generates client secrets and stores them securely +- Updates `appsettings.json` with `TenantId`, `ClientId`, and scopes + +## Workflow + +### Step 1: Detect Project Types + +Scan `Program.cs` files to identify which projects need app registrations: + +```powershell +# Detect projects with Microsoft.Identity.Web +Get-ChildItem -Recurse -Filter "Program.cs" | ForEach-Object { + $content = Get-Content $_.FullName -Raw + $projectDir = Split-Path $_.FullName -Parent + $projectName = Split-Path $projectDir -Leaf + + if ($content -match "AddMicrosoftIdentityWebApi") { + Write-Host "API: $projectName" + } elseif ($content -match "AddMicrosoftIdentityWebApp") { + Write-Host "WebApp: $projectName" + } +} +``` + +### Step 2: Gather Configuration + +Before provisioning, the agent MUST gather required information interactively. + +#### 2a. Get Tenant ID + +First, detect the default tenant from the current connection if Microsoft Graph powershell is connected: + +```powershell +$context = Get-MgContext +if ($context) { + $defaultTenant = $context.TenantId + Write-Host "Connected to tenant: $defaultTenant" +} else { + Write-Host "Not connected. Run: Connect-MgGraph -TenantId '' -Scopes 'Application.ReadWrite.All'" +} +``` + +**AGENT: Ask the user:** +> "I detected tenant ID `{defaultTenant}`. Should I use this tenant, or would you like to specify a different one?" + +- If user confirms → use `$defaultTenant` +- If user provides different ID → use that value +- If not connected → instruct user to run `Connect-MgGraph` first + +#### 2b. Check for Existing ClientIds in appsettings.json + +Before asking about new vs. existing apps, scan `appsettings.json` files: + +```powershell +# === Detect existing ClientIds from appsettings.json === + +$projects = @() + +Get-ChildItem -Recurse -Filter "Program.cs" | ForEach-Object { + $content = Get-Content $_.FullName -Raw + $projectDir = Split-Path $_.FullName -Parent + $projectName = Split-Path $projectDir -Leaf + + # Skip AppHost and ServiceDefaults + if ($projectName -match "AppHost|ServiceDefaults") { return } + + $appSettingsPath = Join-Path $projectDir "appsettings.json" + $existingClientId = $null + $isPlaceholder = $false + + if (Test-Path $appSettingsPath) { + $appSettings = Get-Content $appSettingsPath -Raw | ConvertFrom-Json + if ($appSettings.AzureAd.ClientId) { + $clientId = $appSettings.AzureAd.ClientId + # Check if it's a placeholder value + if ($clientId -match "^<.*>$" -or $clientId -match "YOUR_" -or $clientId -eq "") { + $isPlaceholder = $true + } else { + $existingClientId = $clientId + } + } + } + + $projectType = $null + if ($content -match "AddMicrosoftIdentityWebApi") { + $projectType = "API" + } elseif ($content -match "AddMicrosoftIdentityWebApp") { + $projectType = "WebApp" + } + + if ($projectType) { + $projects += @{ + Name = $projectName + Path = $projectDir + Type = $projectType + ExistingClientId = $existingClientId + IsPlaceholder = $isPlaceholder + } + } +} + +# Output findings +$projects | ForEach-Object { + if ($_.ExistingClientId) { + Write-Host "$($_.Type): $($_.Name) - EXISTING ClientId: $($_.ExistingClientId)" -ForegroundColor Yellow + } elseif ($_.IsPlaceholder) { + Write-Host "$($_.Type): $($_.Name) - Placeholder ClientId (needs provisioning)" -ForegroundColor Cyan + } else { + Write-Host "$($_.Type): $($_.Name) - No ClientId configured" -ForegroundColor Cyan + } +} +``` + +**AGENT: Based on findings, ask the user:** + +**If existing ClientIds found:** +> "I found existing app registrations in your configuration: +> - **API** (`{apiProjectName}`): ClientId `{apiClientId}` +> - **Web App** (`{webProjectName}`): ClientId `{webClientId}` +> +> Should I: +> 1. **Use these existing apps** and complement them if needed (add missing scopes, redirect URIs)? +> 2. **Create new app registrations** and update the configuration?" + +**If only placeholders or no ClientIds:** +> "No existing app registrations found in `appsettings.json`. I'll create new ones." + +- If user chooses **existing** → use the "Existing App Flow" section with detected ClientIds +- If user chooses **new** → proceed to Step 3 + +#### 2c. Confirm or Provide ClientIds + +Based on the detection results, present options to the user: + +**AGENT: Ask the user:** +> "I found the following configuration: +> - **API** (`{apiProjectName}`): {`ClientId: {id}` OR `No ClientId configured`} +> - **Web App** (`{webProjectName}`): {`ClientId: {id}` OR `No ClientId configured`} +> +> What would you like to do? +> 1. **Create new app registrations** for projects without valid ClientIds +> 2. **Use existing app registrations** — provide ClientIds if not detected +> 3. **Replace all** — create new apps even if ClientIds exist" + +**If user provides ClientIds manually:** +> "Please provide the ClientIds: +> - API ClientId: ___ +> - Web App ClientId: ___" + +Store the final decision: +```powershell +# Final configuration after user input +$apiConfig = @{ + ProjectName = "MyService.ApiService" + ProjectPath = "path/to/api" + ClientId = $null # Or user-provided/detected GUID + Action = "Create" # Or "UseExisting" +} + +$webConfig = @{ + ProjectName = "MyService.Web" + ProjectPath = "path/to/web" + ClientId = $null # Or user-provided/detected GUID + Action = "Create" # Or "UseExisting" +} +``` + +**Decision logic:** +- If `Action = "Create"` → proceed to Step 3 (provision new app) +- If `Action = "UseExisting"` → use the "Existing App Flow" section with the ClientId (detected or user-provided) + +> **Important for existing apps:** +> - **Web APIs**: The Existing App Flow checks for and adds `access_as_user` scope if missing +> - **Web Apps**: Run Step 5 (Discover Redirect URIs) first, then pass URIs to Existing App Flow to add any missing redirect URIs +> - **Both**: App ID URI and service principal are created if missing + +### Step 3: Provision API App Registration + +For each project with `AddMicrosoftIdentityWebApi`: + +```powershell +# === Provision API App Registration === + +param( + [Parameter(Mandatory=$true)][string]$TenantId, + [Parameter(Mandatory=$true)][string]$DisplayName, + [string]$SignInAudience = "AzureADMyOrg" +) + +Write-Host "Creating API app registration: $DisplayName" -ForegroundColor Cyan + +# Create the app registration +$apiApp = New-MgApplication -DisplayName $DisplayName -SignInAudience $SignInAudience + +$apiClientId = $apiApp.AppId +$apiObjectId = $apiApp.Id + +Write-Host "Created app: $apiClientId" + +# Set App ID URI +$appIdUri = "api://$apiClientId" +Update-MgApplication -ApplicationId $apiObjectId -IdentifierUris @($appIdUri) +Write-Host "Set App ID URI: $appIdUri" + +# Expose scope: access_as_user +$scopeId = [guid]::NewGuid().ToString() +$scope = @{ + Id = $scopeId + AdminConsentDescription = "Allow the application to access $DisplayName on behalf of the signed-in user." + AdminConsentDisplayName = "Access $DisplayName" + IsEnabled = $true + Type = "User" + UserConsentDescription = "Allow the application to access $DisplayName on your behalf." + UserConsentDisplayName = "Access $DisplayName" + Value = "access_as_user" +} + +$api = @{ + Oauth2PermissionScopes = @($scope) +} + +Update-MgApplication -ApplicationId $apiObjectId -Api $api +Write-Host "Added scope: access_as_user (id: $scopeId)" + +# Create service principal +New-MgServicePrincipal -AppId $apiClientId | Out-Null +Write-Host "Created service principal" + +# Add current user as owner (using Invoke-MgGraphRequest for robustness - avoids module version issues) +$currentUser = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/me" +if ($currentUser) { + $ownerRef = @{ + "@odata.id" = "https://graph.microsoft.com/v1.0/directoryObjects/$($currentUser.id)" + } + New-MgApplicationOwnerByRef -ApplicationId $apiObjectId -BodyParameter $ownerRef + Write-Host "Added owner: $($currentUser.userPrincipalName)" +} + +# Output for next steps +Write-Host "" +Write-Host "=== API Provisioning Complete ===" -ForegroundColor Green +Write-Host "ClientId: $apiClientId" +Write-Host "AppIdUri: $appIdUri" +Write-Host "ScopeId: $scopeId" +Write-Host "Owner: $($currentUser.userPrincipalName)" +``` + +### Step 4: Update API appsettings.json + +Update the API project's `appsettings.json`: + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "", + "ClientId": "", + "Audiences": ["api://"] + } +} +``` + +### Step 5: Discover Redirect URIs + +Parse `Properties/launchSettings.json` for the web project: + +```powershell +# === Discover Redirect URIs === + +param( + [Parameter(Mandatory=$true)][string]$ProjectPath +) + +$launchSettingsPath = Join-Path $ProjectPath "Properties/launchSettings.json" +$launchSettings = Get-Content $launchSettingsPath | ConvertFrom-Json + +$redirectUris = @() + +foreach ($profile in $launchSettings.profiles.PSObject.Properties) { + $appUrl = $profile.Value.applicationUrl + if ($appUrl) { + $urls = $appUrl -split ";" + foreach ($url in $urls) { + if ($url -match "^https://") { + $redirectUris += "$url/signin-oidc" + } + } + } +} + +Write-Host "Redirect URIs: $($redirectUris -join ', ')" +$redirectUris +``` + +### Step 6: Provision Web App Registration + +For each project with `AddMicrosoftIdentityWebApp`: + +```powershell +# === Provision Web App Registration === + +param( + [Parameter(Mandatory=$true)][string]$TenantId, + [Parameter(Mandatory=$true)][string]$DisplayName, + [Parameter(Mandatory=$true)][string]$ApiClientId, + [Parameter(Mandatory=$true)][string]$ApiScopeId, + [Parameter(Mandatory=$true)][string[]]$RedirectUris, + [string]$SignInAudience = "AzureADMyOrg" +) + +Write-Host "Creating Web app registration: $DisplayName" -ForegroundColor Cyan + +# Configure web platform with redirect URIs and enable ID tokens +$webConfig = @{ + RedirectUris = $RedirectUris + ImplicitGrantSettings = @{ + EnableIdTokenIssuance = $true + } +} + +# Create the app registration +$webApp = New-MgApplication ` + -DisplayName $DisplayName ` + -SignInAudience $SignInAudience ` + -Web $webConfig + +$webClientId = $webApp.AppId +$webObjectId = $webApp.Id + +Write-Host "Created app: $webClientId" + +# Add API permission for access_as_user scope +# First, get the Microsoft Graph resource ID for the API +$apiServicePrincipal = Get-MgServicePrincipal -Filter "appId eq '$ApiClientId'" + +$requiredResourceAccess = @{ + ResourceAppId = $ApiClientId + ResourceAccess = @( + @{ + Id = $ApiScopeId + Type = "Scope" + } + ) +} + +Update-MgApplication -ApplicationId $webObjectId -RequiredResourceAccess @($requiredResourceAccess) +Write-Host "Added API permission for $ApiClientId" + +# Create client secret +$passwordCredential = @{ + DisplayName = "dev-secret" + EndDateTime = (Get-Date).AddYears(1) +} + +$secret = Add-MgApplicationPassword -ApplicationId $webObjectId -PasswordCredential $passwordCredential +$secretValue = $secret.SecretText + +Write-Host "Created client secret" + +# Create service principal for the web app +New-MgServicePrincipal -AppId $webClientId | Out-Null +Write-Host "Created service principal" + +# Add current user as owner (using Invoke-MgGraphRequest for robustness - avoids module version issues) +$currentUser = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/me" +if ($currentUser) { + $ownerRef = @{ + "@odata.id" = "https://graph.microsoft.com/v1.0/directoryObjects/$($currentUser.id)" + } + New-MgApplicationOwnerByRef -ApplicationId $webObjectId -BodyParameter $ownerRef + Write-Host "Added owner: $($currentUser.userPrincipalName)" +} + +# Output for next steps +Write-Host "" +Write-Host "=== Web App Provisioning Complete ===" -ForegroundColor Green +Write-Host "ClientId: $webClientId" +Write-Host "Secret: $secretValue" +Write-Host "Owner: $($currentUser.userPrincipalName)" +Write-Host "" +Write-Host "IMPORTANT: Store this secret securely. It will not be shown again." +``` + +### Step 7: Store Secret in User Secrets + +```powershell +# === Store secret in dotnet user-secrets === + +param( + [Parameter(Mandatory=$true)][string]$ProjectPath, + [Parameter(Mandatory=$true)][string]$Secret +) + +Push-Location $ProjectPath + +# Initialize user-secrets if needed +$csproj = Get-ChildItem -Filter "*.csproj" | Select-Object -First 1 +$csprojContent = Get-Content $csproj.FullName -Raw + +if ($csprojContent -notmatch "UserSecretsId") { + dotnet user-secrets init + Write-Host "Initialized user-secrets" +} + +# Set the secret +dotnet user-secrets set "AzureAd:ClientSecret" $Secret +Write-Host "Stored ClientSecret in user-secrets" + +Pop-Location +``` + +### Step 8: Update Web App appsettings.json + +Update the web project's `appsettings.json`: + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "", + "ClientId": "", + "CallbackPath": "/signin-oidc" + }, + "DownstreamApi": { + "Scopes": ["api:///.default"] + } +} +``` + +> **Note**: The `ClientSecret` is stored in user-secrets, not in `appsettings.json`. + +## Existing App Flow + +When using an existing app registration (detected from `appsettings.json` or provided by user), this flow **complements** it by adding any missing configuration: + +| Check | API | Web App | +|-------|-----|---------| +| App ID URI (`api://{clientId}`) | āœ… Add if missing | — | +| `access_as_user` scope | āœ… Add if missing | — | +| Redirect URIs | — | āœ… Add missing URIs | +| API Permission to call API | — | āœ… Add if missing | +| Service Principal | āœ… Create if missing | āœ… Create if missing || Owner (current user) | āœ… Add if not owner | āœ… Add if not owner | +### Complement Existing API App + +```powershell +# === Complement Existing API App Registration === + +param( + [Parameter(Mandatory=$true)][string]$ClientId +) + +Write-Host "Fetching existing API app: $ClientId" -ForegroundColor Cyan + +# Get the application by AppId +$app = Get-MgApplication -Filter "appId eq '$ClientId'" +$objectId = $app.Id + +# Check App ID URI +if (-not $app.IdentifierUris -or $app.IdentifierUris.Count -eq 0) { + Write-Host "Adding App ID URI..." + Update-MgApplication -ApplicationId $objectId -IdentifierUris @("api://$ClientId") +} + +# Check for access_as_user scope +$existingScope = $app.Api.Oauth2PermissionScopes | Where-Object { $_.Value -eq "access_as_user" } +$scopeId = $null + +if (-not $existingScope) { + Write-Host "Adding access_as_user scope..." + $scopeId = [guid]::NewGuid().ToString() + $displayName = $app.DisplayName ?? "API" + + # Get existing scopes and add new one + $existingScopes = @($app.Api.Oauth2PermissionScopes) + $newScope = @{ + Id = $scopeId + AdminConsentDescription = "Allow access on behalf of signed-in user" + AdminConsentDisplayName = "Access $displayName" + IsEnabled = $true + Type = "User" + UserConsentDescription = "Allow access on your behalf" + UserConsentDisplayName = "Access $displayName" + Value = "access_as_user" + } + + $api = @{ + Oauth2PermissionScopes = $existingScopes + $newScope + } + + Update-MgApplication -ApplicationId $objectId -Api $api +} else { + $scopeId = $existingScope.Id + Write-Host "access_as_user scope already exists (id: $scopeId)" +} + +# Check service principal +$sp = Get-MgServicePrincipal -Filter "appId eq '$ClientId'" -ErrorAction SilentlyContinue +if (-not $sp) { + New-MgServicePrincipal -AppId $ClientId | Out-Null + Write-Host "Created service principal" +} + +# Check and add current user as owner if not already (using Invoke-MgGraphRequest for robustness) +$currentUser = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/me" +if ($currentUser) { + $existingOwners = Get-MgApplicationOwner -ApplicationId $objectId + $isOwner = $existingOwners | Where-Object { $_.Id -eq $currentUser.id } + if (-not $isOwner) { + $ownerRef = @{ + "@odata.id" = "https://graph.microsoft.com/v1.0/directoryObjects/$($currentUser.id)" + } + New-MgApplicationOwnerByRef -ApplicationId $objectId -BodyParameter $ownerRef + Write-Host "Added owner: $($currentUser.userPrincipalName)" + } else { + Write-Host "Current user is already an owner" + } +} + +Write-Host "API app registration updated" -ForegroundColor Green +Write-Host "ScopeId: $scopeId" + +# Return scope ID for web app configuration +$scopeId +``` + +### Complement Existing Web App + +```powershell +# === Complement Existing Web App Registration === + +param( + [Parameter(Mandatory=$true)][string]$ClientId, + [Parameter(Mandatory=$true)][string]$ApiClientId, + [Parameter(Mandatory=$true)][string]$ApiScopeId, + [string[]]$RequiredRedirectUris = @() +) + +Write-Host "Fetching existing Web app: $ClientId" -ForegroundColor Cyan + +# Get the application by AppId +$app = Get-MgApplication -Filter "appId eq '$ClientId'" +$objectId = $app.Id + +# Check redirect URIs +if ($RequiredRedirectUris.Count -gt 0) { + $existingUris = @($app.Web.RedirectUris) + $missingUris = $RequiredRedirectUris | Where-Object { $_ -notin $existingUris } + if ($missingUris.Count -gt 0) { + Write-Host "Adding missing redirect URIs: $($missingUris -join ', ')" + $allUris = $existingUris + $missingUris + + $webConfig = @{ + RedirectUris = $allUris + ImplicitGrantSettings = @{ + EnableIdTokenIssuance = $true + } + } + + Update-MgApplication -ApplicationId $objectId -Web $webConfig + } else { + Write-Host "All redirect URIs already configured" + } +} + +# Check API permission +$existingPermission = $app.RequiredResourceAccess | Where-Object { $_.ResourceAppId -eq $ApiClientId } +if (-not $existingPermission) { + Write-Host "Adding API permission for $ApiClientId..." + + $requiredResourceAccess = @{ + ResourceAppId = $ApiClientId + ResourceAccess = @( + @{ + Id = $ApiScopeId + Type = "Scope" + } + ) + } + + # Preserve existing permissions and add new one + $allPermissions = @($app.RequiredResourceAccess) + $requiredResourceAccess + Update-MgApplication -ApplicationId $objectId -RequiredResourceAccess $allPermissions + Write-Host "Added API permission" +} else { + Write-Host "API permission already configured" +} + +# Check service principal +$sp = Get-MgServicePrincipal -Filter "appId eq '$ClientId'" -ErrorAction SilentlyContinue +if (-not $sp) { + $sp = New-MgServicePrincipal -AppId $ClientId + Write-Host "Created service principal" +} + +# Check and add current user as owner if not already (using Invoke-MgGraphRequest for robustness) +$currentUser = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/me" +if ($currentUser) { + $existingOwners = Get-MgApplicationOwner -ApplicationId $objectId + $isOwner = $existingOwners | Where-Object { $_.Id -eq $currentUser.id } + if (-not $isOwner) { + $ownerRef = @{ + "@odata.id" = "https://graph.microsoft.com/v1.0/directoryObjects/$($currentUser.id)" + } + New-MgApplicationOwnerByRef -ApplicationId $objectId -BodyParameter $ownerRef + Write-Host "Added owner: $($currentUser.userPrincipalName)" + } else { + Write-Host "Current user is already an owner" + } +} + +# Grant admin consent for the web app to call the API +Write-Host "Attempting to grant admin consent for API access..." +try { + $apiSp = Get-MgServicePrincipal -Filter "appId eq '$ApiClientId'" + + # Check if consent already exists + $existingGrant = Get-MgOauth2PermissionGrant -Filter "clientId eq '$($sp.Id)' and resourceId eq '$($apiSp.Id)'" -ErrorAction SilentlyContinue + + if (-not $existingGrant) { + $grant = @{ + ClientId = $sp.Id + ConsentType = "AllPrincipals" + ResourceId = $apiSp.Id + Scope = "access_as_user" + } + New-MgOauth2PermissionGrant -BodyParameter $grant | Out-Null + Write-Host "Admin consent granted successfully" -ForegroundColor Green + } else { + Write-Host "Admin consent already exists" + } +} catch { + Write-Host "" + Write-Host "āš ļø Could not grant admin consent automatically." -ForegroundColor Yellow + Write-Host " This requires DelegatedPermissionGrant.ReadWrite.All permission." -ForegroundColor Yellow + Write-Host "" + Write-Host " To grant consent manually:" -ForegroundColor Cyan + Write-Host " 1. Go to Azure Portal > Entra ID > App registrations" -ForegroundColor Cyan + Write-Host " 2. Select the web app: $($app.DisplayName)" -ForegroundColor Cyan + Write-Host " 3. Go to 'API permissions'" -ForegroundColor Cyan + Write-Host " 4. Click 'Grant admin consent for [tenant]'" -ForegroundColor Cyan + Write-Host "" + Write-Host " Alternatively, users will be prompted for consent on first sign-in." -ForegroundColor Cyan + Write-Host "" +} + +Write-Host "Web app registration updated" -ForegroundColor Green +``` + +## Error Handling: Admin Script Fallback + +If the user lacks permissions, generate a script for an admin: + +```powershell +# === Generate Admin Script === + +$scriptContent = @" +# ============================================================ +# Admin Script: Entra ID App Provisioning +# ============================================================ +# This script requires Application Administrator or Global Administrator role. +# Generated: $(Get-Date -Format "yyyy-MM-dd HH:mm") +# Solution: $SolutionName +# Tenant: $TenantId +# ============================================================ + +# Prerequisites - run once +# Install-Module Microsoft.Graph.Applications -Scope CurrentUser -Force + +# Connect with admin privileges +Connect-MgGraph -Scopes "Application.ReadWrite.All", "Directory.ReadWrite.All" + +Write-Host "Provisioning Entra ID apps..." -ForegroundColor Cyan + +# [Full provisioning script content here] + +Write-Host "" +Write-Host "=== PROVISIONING COMPLETE ===" -ForegroundColor Green +Write-Host "API ClientId: `$apiClientId" +Write-Host "Web ClientId: `$webClientId" +Write-Host "" +Write-Host "Please provide these values to the developer." + +# Cleanup +Disconnect-MgGraph +"@ + +$scriptPath = "entra-provision-admin.ps1" +$scriptContent | Out-File -FilePath $scriptPath -Encoding UTF8 +Write-Host "Admin script saved to: $scriptPath" -ForegroundColor Yellow +``` + +## Configuration Reference + +### API appsettings.json + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "YOUR_TENANT_ID", + "ClientId": "YOUR_API_CLIENT_ID", + "Audiences": ["api://YOUR_API_CLIENT_ID"] + } +} +``` + +### Web App appsettings.json + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "YOUR_TENANT_ID", + "ClientId": "YOUR_WEB_CLIENT_ID", + "CallbackPath": "/signin-oidc" + }, + "DownstreamApi": { + "Scopes": ["api://YOUR_API_CLIENT_ID/.default"] + } +} +``` + +## Best Practices + +1. **Provision API first** — Web app needs the API's Client ID and scope ID +2. **Use `.default` scope** — Safer for downstream API calls in composed scenarios +3. **Store secrets in user-secrets** — Never commit secrets to source control +4. **Single tenant by default** — Use `AzureADMyOrg`; switch to `AzureADMultipleOrgs` only when needed +5. **Parse launchSettings.json** — Get accurate redirect URIs for all launch profiles +6. **Complement, don't duplicate** — When using existing apps, only add what's missing +7. **Disconnect when done** — Run `Disconnect-MgGraph` after provisioning + +## Related + +- [Entra ID Aspire Authentication Skill](../entra-id-aspire-authentication/SKILL.md) — Code wiring (run first) +- [Aspire Framework Docs](../../docs/frameworks/aspire.md) — Full integration guide +- [Microsoft Graph PowerShell SDK](https://learn.microsoft.com/powershell/microsoftgraph/) — Reference +- [New-MgApplication](https://learn.microsoft.com/powershell/module/microsoft.graph.applications/new-mgapplication) — App registration cmdlet diff --git a/.github/workflows/aot-check.yml b/.github/workflows/aot-check.yml deleted file mode 100644 index ed77ce7ab..000000000 --- a/.github/workflows/aot-check.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: "AOT Check" - -on: - push: - branches: [ "master", "rel/v2" ] - pull_request: - types: - - opened - - synchronize - - reopened - - ready_for_review - branches: [ "master", "rel/v2" ] - -env: - TargetNetNext: False - -jobs: - analyze: - runs-on: windows-latest - name: AOT check - - steps: - - name: Checkout repository - uses: actions/checkout@v5 - with: - fetch-depth: 1 - - - name: Setup .NET 9.0.x - uses: actions/setup-dotnet@v5 - with: - dotnet-version: 9.0.x - - - name: Setup .NET 10.0.x - uses: actions/setup-dotnet@v5 - with: - dotnet-version: 10.0.x - - - name: Runs AOT check with .NET 9.0 - id: aot-powershell-net9 - run: build\test-aot.ps1 'net9.0' - - - name: Runs AOT check with .NET 10.0 - id: aot-powershell-net10 - run: build\test-aot.ps1 'net10.0' - diff --git a/Directory.Build.props b/Directory.Build.props index 423b9e4a7..f7aae31c8 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,13 +4,11 @@ - 4.1.0 + 4.4.1 $(MicrosoftIdentityWebVersion) - true - 4.0.0 - + 4.2.0 $(MSBuildThisFileDirectory)/build $(BuildDirectory)/35MSSharedLib1024.snk git @@ -37,11 +35,10 @@ enable true true - 13 + 14 true - true @@ -53,7 +50,7 @@ - + @@ -78,13 +75,13 @@ - 13 + 14 8.15.0 - 4.79.2 - 9.6.0 + 4.82.0 + 11.0.0 3.3.0 4.7.2 4.6.0 @@ -148,10 +145,6 @@ 8.0.0 - - - - 6.0.0-* 6.0.0-* @@ -185,7 +178,7 @@ 3.1.3 2.1.0 2.1.0 - 2.2.4 + 2.1.0 diff --git a/Microsoft.Identity.Web.sln b/Microsoft.Identity.Web.sln index 934728fad..8e68b4d1e 100644 --- a/Microsoft.Identity.Web.sln +++ b/Microsoft.Identity.Web.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 +# Visual Studio Version 18 +VisualStudioVersion = 18.0.11217.181 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{1DDE1AAC-5AE6-4725-94B6-A26C58D3423F}" EndProject @@ -9,6 +9,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig BannedSymbols.txt = BannedSymbols.txt + changelog.md = changelog.md Directory.Build.props = Directory.Build.props build\Microsoft.Identity.Web-Source-Assemblies.dgml = build\Microsoft.Identity.Web-Source-Assemblies.dgml EndProjectSection @@ -174,6 +175,36 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Identity.Web.Side EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sidecar.Tests", "tests\E2E Tests\Sidecar.Tests\Sidecar.Tests.csproj", "{946E6BED-2A06-4FF4-3E39-22ACEB44A984}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MtlsPop", "MtlsPop", "{06818CF6-16AD-4184-9264-B593B8F2AA25}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MtlsPopClient", "tests\DevApps\MtlsPop\MtlsPopClient\MtlsPopClient.csproj", "{3ECC1B78-A458-726F-D7B8-AB74733CCCDC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MtlsPopWebApi", "tests\DevApps\MtlsPop\MtlsPopWebApi\MtlsPopWebApi.csproj", "{A61CEEDE-6F2C-0710-E008-B5F6F25D87D7}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{FF3B93A1-B8A4-4120-AF3D-7B5C43C0735C}" + ProjectSection(SolutionItems) = preProject + build\build.md = build\build.md + build\CodeCoverage.runsettings = build\CodeCoverage.runsettings + build\credscan-exclusion.json = build\credscan-exclusion.json + build\GenerateDocFx.ps1 = build\GenerateDocFx.ps1 + build\Microsoft.Identity.Web-Source-Assemblies.dgml = build\Microsoft.Identity.Web-Source-Assemblies.dgml + build\MSAL.snk = build\MSAL.snk + build\pipeline-releasebuild.yaml = build\pipeline-releasebuild.yaml + build\policheck_exclusions.xml = build\policheck_exclusions.xml + build\policheck_filetypes.xml = build\policheck_filetypes.xml + build\release-provisioning-tool.yml = build\release-provisioning-tool.yml + build\template-install-dependencies.yaml = build\template-install-dependencies.yaml + build\template-onebranch-release-build.yaml = build\template-onebranch-release-build.yaml + build\template-pack-and-sign-all-nugets.yaml = build\template-pack-and-sign-all-nugets.yaml + build\template-pack-nuget.yaml = build\template-pack-nuget.yaml + build\template-postbuild-code-analysis.yaml = build\template-postbuild-code-analysis.yaml + build\template-prebuild-code-analysis.yaml = build\template-prebuild-code-analysis.yaml + build\template-publish-and-cleanup.yaml = build\template-publish-and-cleanup.yaml + build\template-restore-build-MSIdentityWeb.yaml = build\template-restore-build-MSIdentityWeb.yaml + build\template-run-unit-tests.yaml = build\template-run-unit-tests.yaml + build\tsaConfig.json = build\tsaConfig.json + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -417,6 +448,14 @@ Global {946E6BED-2A06-4FF4-3E39-22ACEB44A984}.Debug|Any CPU.Build.0 = Debug|Any CPU {946E6BED-2A06-4FF4-3E39-22ACEB44A984}.Release|Any CPU.ActiveCfg = Release|Any CPU {946E6BED-2A06-4FF4-3E39-22ACEB44A984}.Release|Any CPU.Build.0 = Release|Any CPU + {3ECC1B78-A458-726F-D7B8-AB74733CCCDC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3ECC1B78-A458-726F-D7B8-AB74733CCCDC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3ECC1B78-A458-726F-D7B8-AB74733CCCDC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3ECC1B78-A458-726F-D7B8-AB74733CCCDC}.Release|Any CPU.Build.0 = Release|Any CPU + {A61CEEDE-6F2C-0710-E008-B5F6F25D87D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A61CEEDE-6F2C-0710-E008-B5F6F25D87D7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A61CEEDE-6F2C-0710-E008-B5F6F25D87D7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A61CEEDE-6F2C-0710-E008-B5F6F25D87D7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -496,6 +535,9 @@ Global {A8181404-23E0-D38B-454C-D16ECDB18B9F} = {E37CDBC1-18F6-4C06-A3EE-532C9106721F} {55C81F88-0FFA-491C-A1D7-0ACA7212B59C} = {1DDE1AAC-5AE6-4725-94B6-A26C58D3423F} {946E6BED-2A06-4FF4-3E39-22ACEB44A984} = {45B20A78-91F8-4DD2-B9AD-F12D3A93536C} + {06818CF6-16AD-4184-9264-B593B8F2AA25} = {7786D2DD-9EE4-42E1-B587-740A2E15C41D} + {3ECC1B78-A458-726F-D7B8-AB74733CCCDC} = {06818CF6-16AD-4184-9264-B593B8F2AA25} + {A61CEEDE-6F2C-0710-E008-B5F6F25D87D7} = {06818CF6-16AD-4184-9264-B593B8F2AA25} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {104367F1-CE75-4F40-B32F-F14853973187} diff --git a/README.md b/README.md index faeea5b61..fd6ecbcc5 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,16 @@ The Microsoft Identity Web roadmap is available from [Roadmap](https://github.co - Code samples are available for [web app samples](https://github.com/AzureAD/microsoft-identity-web/wiki/web-app-samples) and [web API samples](https://github.com/AzureAD/microsoft-identity-web/wiki#web-api-samples) +### Authority Configuration + +Understanding how to properly configure authentication authorities is crucial for Azure AD, B2C, and CIAM applications: + +- **[Authority Configuration & Precedence Guide](docs/authority-configuration.md)** - Comprehensive guide explaining how Authority, Instance, TenantId, and PreserveAuthority work together +- **[B2C Authority Examples](docs/b2c-authority-examples.md)** - Azure AD B2C-specific configuration patterns and best practices +- **[CIAM Authority Examples](docs/ciam-authority-examples.md)** - Customer Identity Access Management (CIAM) scenarios with custom domains +- **[Migration Guide](docs/migration-authority-vs-instance.md)** - Upgrade path for existing applications and resolving configuration conflicts +- **[Authority FAQ](docs/faq-authority-precedence.md)** - Common questions and troubleshooting tips + ## Where do I file issues diff --git a/benchmark/Directory.Build.props b/benchmark/Directory.Build.props index 6949dd8ed..156005f8d 100644 --- a/benchmark/Directory.Build.props +++ b/benchmark/Directory.Build.props @@ -13,7 +13,7 @@ - 1.0.0 + 2.0.0 0.13.12 0.13.12 4.3.4 diff --git a/benchmark/appsettings.json b/benchmark/appsettings.json index e98f45f30..4a41ca20a 100644 --- a/benchmark/appsettings.json +++ b/benchmark/appsettings.json @@ -1,8 +1,8 @@ { "AzureAd": { "Instance": "https://login.microsoftonline.com/", - "TenantId": "msidlab4.onmicrosoft.com", - "ClientId": "f6b698c0-140c-448f-8155-4aa9bf77ceba", + "TenantId": "id4slab1.onmicrosoft.com", + "ClientId": "4ebc2cfc-14bf-4c88-9678-26543ec1c59d", "ClientCredentials": [ { "SourceType": "StoreWithDistinguishedName", diff --git a/build/test-aot.ps1 b/build/test-aot.ps1 deleted file mode 100644 index 2a59dfa57..000000000 --- a/build/test-aot.ps1 +++ /dev/null @@ -1,57 +0,0 @@ -param([string]$targetNetFramework) - -$projectName='Microsoft.Identity.Web.AotCompatibility.TestApp' -$rootDirectory = Split-Path $PSScriptRoot -Parent - -# Add TargetNetNext parameter if targeting .NET 10 -$additionalParams = "" -if ($targetNetFramework -eq "net10.0") { - $additionalParams = "/p:TargetNetNext=True" -} - -$publishOutput = dotnet publish $rootDirectory/tests/$projectName/$projectName.csproj --framework $targetNetFramework -nodeReuse:false /p:UseSharedCompilation=false $additionalParams - -$actualWarningCount = 0 - -foreach ($line in $($publishOutput -split "`r`n")) -{ - if (($line -like "*analysis warning IL*") -or ($line -like "*analysis error IL*")) - { - Write-Host $line - $actualWarningCount += 1 - } -} - -Write-Host "Actual warning count is: ", $actualWarningCount -$expectedWarningCount = 50 - -if ($LastExitCode -ne 0) -{ - Write-Host "There was an error while publishing AotCompatibility Test App. LastExitCode is:", $LastExitCode - Write-Host $publishOutput -} - -$runtime = if ($IsWindows) { "win-x64" } elseif ($IsMacOS) { "macos-x64"} else {"linux-x64"} -$app = if ($IsWindows ) {"./$projectName.exe" } else {"./$projectName" } - -Push-Location $rootDirectory/tests/$projectName/bin/Release/$targetNetFramework/$runtime - -Write-Host "Executing test App..." -$app -Write-Host "Finished executing test App" - -if ($LastExitCode -ne 0) -{ - Write-Host "There was an error while executing AotCompatibility Test App. LastExitCode is:", $LastExitCode -} - -Pop-Location - -$testPassed = 0 -if ($expectedWarningCount -ne $actualWarningCount) -{ - $testPassed = 1 - Write-Host "Actual warning count:", $actualWarningCount, "is not as expected. Expected warning count is:", $expectedWarningCount -} - -Exit $testPassed diff --git a/changelog.md b/changelog.md index 6b8e4abbe..eeffb5917 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,92 @@ +## 4.4.0 + +### New features +- Add AOT-compatible web API authentication for .NET 10+. See [#3705](https://github.com/AzureAD/microsoft-identity-web/pull/3705) and [#3664](https://github.com/AzureAD/microsoft-identity-web/pull/3664). +- Propagate long-running web API session key back to callers in user token acquisition. See [#3728](https://github.com/AzureAD/microsoft-identity-web/pull/3728). +- Add OBO event initialization for OBO APIs. See [#3724](https://github.com/AzureAD/microsoft-identity-web/pull/3724). +- Add support for calling `WithClientClaims` flow for token acquisition. See [#3623](https://github.com/AzureAD/microsoft-identity-web/pull/3623). +- Add `OnBeforeTokenAcquisitionForOnBehalfOf` event. See [#3680](https://github.com/AzureAD/microsoft-identity-web/pull/3680). + +### Bug fixes +- Throw `InvalidOperationException` with actionable message when a custom credential is not registered. See [#3626](https://github.com/AzureAD/microsoft-identity-web/pull/3626). +- Fix event firing for `InvokeOnBeforeTokenAcquisitionForOnBehalfOfAsync`. See [#3717](https://github.com/AzureAD/microsoft-identity-web/pull/3717). +- Update `OnBeforeTokenAcquisitionForOnBehalfOf` to construct `ClaimsPrincipal` from token. See [#3714](https://github.com/AzureAD/microsoft-identity-web/pull/3714). +- Add a retry counter for acquire token and updated tests with a fake secret. See [#3682](https://github.com/AzureAD/microsoft-identity-web/pull/3682). +- Fix OBO user error handling. See [#3712](https://github.com/AzureAD/microsoft-identity-web/pull/3712). +- Fix override merging for app token (and others). See [#3644](https://github.com/AzureAD/microsoft-identity-web/pull/3644). +- Fix certificate reload logic to only trigger on certificate-specific errors. See [#3653](https://github.com/AzureAD/microsoft-identity-web/pull/3653). +- Update ROPC flow CCA to pass `SendX5C` to MSAL. See [#3671](https://github.com/AzureAD/microsoft-identity-web/pull/3671). + +### Dependencies updates +- Bump `qs` in `/tests/DevApps/SidecarAdapter/typescript`. See [#3725](https://github.com/AzureAD/microsoft-identity-web/pull/3725). +- Downgrade Microsoft.Extensions.Configuration.Binder to 2.1.0 on .NET Framework. See [#3730](https://github.com/AzureAD/microsoft-identity-web/pull/3730). +- Update .NET SDK to 10.0.103 to address DOTNET-Security-10.0 vulnerability. See [#3726](https://github.com/AzureAD/microsoft-identity-web/pull/3726). +- Upgrade to Microsoft.Identity.Abstractions 11 for AoT compatibility. See [#3699](https://github.com/AzureAD/microsoft-identity-web/pull/3699). +- Update to MSAL 4.81.0. See [#3665](https://github.com/AzureAD/microsoft-identity-web/pull/3665). + +### Documentation +- Added comprehensive authority configuration and precedence documentation, including guides for Azure AD, B2C, and CIAM scenarios with migration examples and FAQ. See [#3613](https://github.com/AzureAD/microsoft-identity-web/issues/3613). +- Add documentation for auto-generated session key for long-running OBO session. See [#3729](https://github.com/AzureAD/microsoft-identity-web/pull/3729). +- Improve the Aspire doc article and skills. See [#3695](https://github.com/AzureAD/microsoft-identity-web/pull/3695). +- Add an article and agent skill to add Entra ID to an Aspire app. See [#3689](https://github.com/AzureAD/microsoft-identity-web/pull/3689). +- Fix misleading comment in `CertificatelessOptions.ManagedIdentityClientId`. See [#3667](https://github.com/AzureAD/microsoft-identity-web/pull/3667). +- Add Copilot explore tool functionality. See [#3694](https://github.com/AzureAD/microsoft-identity-web/pull/3694). + +### Fundamentals +- Remove unnecessary warning suppression. See [#3715](https://github.com/AzureAD/microsoft-identity-web/pull/3715). +- Migrate labs to Lab.API 2.x (first pass). See [#3710](https://github.com/AzureAD/microsoft-identity-web/pull/3710). +- Update Sidecar E2E test constants. See [#3693](https://github.com/AzureAD/microsoft-identity-web/pull/3693). +- Fix intermittent failures in `CertificatesObserverTests`. See [#3687](https://github.com/AzureAD/microsoft-identity-web/pull/3687). +- Add validation baseline exclusions. See [#3684](https://github.com/AzureAD/microsoft-identity-web/pull/3684). +- Add dSTS integration tests. See [#3677](https://github.com/AzureAD/microsoft-identity-web/pull/3677). +- Fix FIC test. See [#3663](https://github.com/AzureAD/microsoft-identity-web/pull/3663). +- Update IdentityWeb version, build logic, and validation. See [#3659](https://github.com/AzureAD/microsoft-identity-web/pull/3659). + +## 4.3.0 + +### New features +- Added token binding (mTLS PoP) scenario for confidential client (app-only) token acquisition and downstream API calls. See [#3622](https://github.com/AzureAD/microsoft-identity-web/pull/3622). + +### Dependencies updates +- Bumped **qs** from 6.14.0 to 6.14.1 in /tests/DevApps/SidecarAdapter/typescript. See [#3660]( https://github.com/AzureAD/microsoft-identity-web/pull/3660). + +### Documentation +- Modernized Identity Web documentation, which is now can be found in [docs](https://github.com/AzureAD/microsoft-identity-web/tree/master/docs). See [#3566](https://github.com/AzureAD/microsoft-identity-web/pull/3566). +- Added token binding (mTLS PoP) documentation. See [#3661](https://github.com/AzureAD/microsoft-identity-web/pull/3661). + +## 4.2.0 + +### New features +- Added CAE claims support for FIC + Managed Identity. See [#3647](https://github.com/AzureAD/microsoft-identity-web/pull/3647) for details. +- Added `AddMicrosoftIdentityMessageHandler` extension methods for `IHttpClientBuilder`. See [#3649](https://github.com/AzureAD/microsoft-identity-web/pull/3649) for details. + +### Bug fixes +- Fixed tenant not being propagated in credential FIC acquisition. See [#3633](https://github.com/AzureAD/microsoft-identity-web/pull/3633) for details. +- Fixed `ForAgentIdentity` hardcoded 'AzureAd' `ConfigurationSection` to respect `AuthenticationOptionsName`. See [#3635](https://github.com/AzureAD/microsoft-identity-web/pull/3635) for details. +- Fixed `GetTokenAcquirer` to propagate `MicrosoftEntraApplicationOptions` properties. See [#3651](https://github.com/AzureAD/microsoft-identity-web/pull/3651) for details. +- Added meaningful error message when identity configuration is missing. See [#3637](https://github.com/AzureAD/microsoft-identity-web/pull/3637) for details. + +### Dependencies updates +- Update Microsoft.Identity.Abstractions to version 10.0.0. +- Bump express from 5.1.0 to 5.2.0 in /tests/DevApps/SidecarAdapter/typescript. [#3636](https://github.com/AzureAD/microsoft-identity-web/pull/3636) +- Bump jws from 3.2.2 to 3.2.3 in /tests/DevApps/SidecarAdapter/typescript. [#3641](https://github.com/AzureAD/microsoft-identity-web/pull/3641) + +### Fundamentals +- Update support policy. [#3656](https://github.com/AzureAD/microsoft-identity-web/pull/3656) +- Update agent identity coordinates in E2E tests after deauth. [#3640](https://github.com/AzureAD/microsoft-identity-web/pull/3640) +- Update E2E agent identity configuration to new tenant. [#3646](https://github.com/AzureAD/microsoft-identity-web/pull/3646) + +## 4.1.1 + +### Bug fixes +- Authority-only configuration parsing improvements: Early parsing of Authority into Instance/TenantId and defensive fallback in PrepareAuthorityInstanceForMsal. Behavior is backward compatible; Authority is still ignored when Instance/TenantId explicitly provided—now surfaced via a warning. See [#3612](https://github.com/AzureAD/microsoft-identity-web/issues/3612). + +### New features +- Added warning diagnostics for conflicting Authority vs Instance/TenantId: Emitting a single structured warning when both styles are provided. See [#3611](https://github.com/AzureAD/microsoft-identity-web/issues/3611). + +### Fundamentals +- Expanded authority test matrix: Coverage for AAD (v1/v2), B2C (/tfp/ normalization, policy path), CIAM (PreserveAuthority), query parameters, scheme-less forms, and conflict scenarios. See [#3610](https://github.com/AzureAD/microsoft-identity-web/issues/3610). + 4.1.0 ========= ### New features 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..b21f2f925 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,289 @@ +# 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: + +- **[.NET Aspire](./frameworks/aspire.md)** distributed applications ⭐ *Recommended for new ASP.NET Core projects* +- **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 + +### Framework Integration +- [.NET Aspire](./frameworks/aspire.md) ⭐ - **Recommended** for new ASP.NET Core distributed applications +- [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/authority-configuration.md b/docs/authority-configuration.md new file mode 100644 index 000000000..c331f947b --- /dev/null +++ b/docs/authority-configuration.md @@ -0,0 +1,237 @@ +# Authority Configuration & Precedence in Microsoft.Identity.Web + +## Overview + +Microsoft.Identity.Web provides flexible options for configuring authentication authority URLs. Understanding how these configuration options interact is crucial for proper application setup, especially when working with Azure Active Directory (AAD), Azure AD B2C, and Customer Identity and Access Management (CIAM). + +This guide explains: +- How authority-related configuration properties work together +- The precedence rules when multiple properties are set +- Best practices for different authentication scenarios +- How to interpret and resolve configuration warnings + +## Terminology + +### Core Configuration Properties + +- **Authority**: A complete URL to the authentication endpoint, including the instance and tenant identifier. Examples: + - `https://login.microsoftonline.com/common` + - `https://login.microsoftonline.com/contoso.onmicrosoft.com` + - `https://contoso.b2clogin.com/contoso.onmicrosoft.com/B2C_1_susi` + +- **Instance**: The base URL of the authentication service without tenant information. Examples: + - `https://login.microsoftonline.com/` (Azure Commercial) + - `https://login.microsoftonline.us/` (Azure Government) + - `https://login.chinacloudapi.cn/` (Azure China) + - `https://contoso.b2clogin.com/` (B2C) + + See [Azure AD authentication endpoints](https://learn.microsoft.com/azure/active-directory/develop/authentication-national-cloud) for sovereign cloud instances. + +- **TenantId**: The tenant identifier, which can be: + - A GUID (e.g., `12345678-1234-1234-1234-123456789012`) + - A tenant domain (e.g., `contoso.onmicrosoft.com`) + - Special values (`common`, `organizations`, `consumers`) + +- **Domain**: The primary domain of your tenant (e.g., `contoso.onmicrosoft.com`). Used primarily with B2C configurations. + +- **Policy IDs**: B2C-specific identifiers for user flows: + - `SignUpSignInPolicyId` (e.g., `B2C_1_susi`) + - `ResetPasswordPolicyId` (e.g., `B2C_1_reset`) + - `EditProfilePolicyId` (e.g., `B2C_1_edit_profile`) + +## Authority Resolution Decision Tree + +The following flowchart illustrates how Microsoft.Identity.Web resolves the authority configuration: + +```mermaid +flowchart TD + A[Configuration Provided] --> B{Instance & TenantId set?} + B -- Yes --> C[Use Instance & TenantId
Ignore Authority if present] + B -- No --> D{Authority Provided?} + D -- No --> E[Configuration Invalid
Requires Instance+TenantId] + D -- Yes --> H[Parse Authority
Extract Instance + TenantId] + H --> I{Is B2C?} + I -- Yes --> J[Normalize /tfp/ if present
Derive PreparedInstance] + I -- No --> K[Derive PreparedInstance] + C --> K + J --> K + K --> L[Pass PreparedInstance to MSAL] +``` + +## Precedence Rules + +The following table summarizes how different configuration combinations are resolved: + +| Authority Set | Instance Set | TenantId Set | Result | Warning | +|---------------|--------------|--------------|--------|---------| +| āœ… | āŒ | āŒ | Authority is parsed → Instance + TenantId | No | +| āœ… | āœ… | āŒ | Instance used, Authority **ignored** | āš ļø Yes (EventId 408) | +| āœ… | āŒ | āœ… | TenantId used, Authority **ignored** | āš ļø Yes (EventId 408) | +| āœ… | āœ… | āœ… | Instance + TenantId used, Authority **ignored** | āš ļø Yes (EventId 408) | +| āŒ | āœ… | āœ… | Instance + TenantId used | No | +| āŒ | āœ… | āŒ | Instance used, tenant resolved at runtime | No* | +| āŒ | āŒ | āœ… | Invalid configuration | Error | + +\* For single-tenant apps, always specify TenantId when using Instance. + +## Recommended Configuration Patterns + +### AAD Single-Tenant Application + +**Recommended**: Use `Instance` and `TenantId` separately for clarity and flexibility. + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "12345678-1234-1234-1234-123456789012", + "ClientId": "11111111-1111-1111-1111-111111111111" + } +} +``` + +**Alternative**: Use `Authority` (will be parsed automatically). + +```json +{ + "AzureAd": { + "Authority": "https://login.microsoftonline.com/12345678-1234-1234-1234-123456789012", + "ClientId": "11111111-1111-1111-1111-111111111111" + } +} +``` + +### AAD Multi-Tenant Application + +**Option 1**: Use `Instance` with special tenant value. + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "organizations", + "ClientId": "11111111-1111-1111-1111-111111111111" + } +} +``` + +**Option 2**: Use complete `Authority`. + +```json +{ + "AzureAd": { + "Authority": "https://login.microsoftonline.com/organizations", + "ClientId": "11111111-1111-1111-1111-111111111111" + } +} +``` + +### Azure AD B2C + +**Recommended**: Use `Authority` including the policy path. Do NOT mix with `Instance`/`TenantId`. + +```json +{ + "AzureAdB2C": { + "Authority": "https://contoso.b2clogin.com/contoso.onmicrosoft.com/B2C_1_susi", + "ClientId": "11111111-1111-1111-1111-111111111111", + "Domain": "contoso.onmicrosoft.com" + } +} +``` + +**Note**: The legacy `/tfp/` path segment is automatically normalized by Microsoft.Identity.Web: +- `https://contoso.b2clogin.com/tfp/contoso.onmicrosoft.com/B2C_1_susi` +- Becomes: `https://contoso.b2clogin.com/contoso.onmicrosoft.com/B2C_1_susi` + +See [B2C Authority Examples](b2c-authority-examples.md) for more details. + +### CIAM (Customer Identity and Access Management) + +**Recommended**: Use `Authority` for CIAM configurations. The library automatically handles CIAM authorities correctly. + +```json +{ + "AzureAd": { + "Authority": "https://contoso.ciamlogin.com/contoso.onmicrosoft.com", + "ClientId": "11111111-1111-1111-1111-111111111111" + } +} +``` + +See [CIAM Authority Examples](ciam-authority-examples.md) for more details. + +## Understanding the Warning Log Message + +When both `Authority` and (`Instance` and/or `TenantId`) are configured, you'll see a warning like: + +``` +[Warning] [MsIdWeb] Authority 'https://login.microsoftonline.com/common' is being ignored +because Instance 'https://login.microsoftonline.com/' and/or TenantId 'contoso.onmicrosoft.com' +are already configured. To use Authority, remove Instance and TenantId from the configuration. +``` + +**What it means**: Microsoft.Identity.Web detected conflicting configuration. The `Instance` and `TenantId` properties take precedence, and the `Authority` value is completely ignored. + +**How to fix**: +1. **Option 1 (Recommended)**: Remove `Authority` from your configuration, keep `Instance` and `TenantId`. +2. **Option 2**: Remove both `Instance` and `TenantId`, keep only `Authority`. + +**Event ID**: 408 (AuthorityConflict) + +## Edge Cases and Special Scenarios + +### Scheme-less Authority + +If you provide an authority without the `https://` scheme, you may encounter parsing errors. Always include the full URL: + +āŒ Wrong: `"Authority": "login.microsoftonline.com/common"` +āœ… Correct: `"Authority": "https://login.microsoftonline.com/common"` + +### Trailing Slashes + +Trailing slashes are automatically normalized. Both forms work identically: +- `https://login.microsoftonline.com/` +- `https://login.microsoftonline.com` + +### Query Parameters in Authority + +Query parameters in the Authority URL are preserved during parsing but generally not recommended. Use `ExtraQueryParameters` configuration option instead. + +### Missing v2.0 Endpoint Suffix + +Microsoft.Identity.Web and MSAL.NET use the v2.0 endpoint by default. You do NOT need to append `/v2.0` to your authority: + +āŒ Avoid: `https://login.microsoftonline.com/common/v2.0` +āœ… Correct: `https://login.microsoftonline.com/common` + +### Custom Domains with CIAM + +When using custom domains with CIAM, use the full Authority URL. The library handles custom CIAM domains automatically: + +```json +{ + "AzureAd": { + "Authority": "https://login.contoso.com/contoso.onmicrosoft.com", + "ClientId": "11111111-1111-1111-1111-111111111111" + } +} +``` + +**Note**: Ensure your custom domain is properly configured in your CIAM tenant before using it. + +## Migration Guidance + +If you're upgrading from older configurations or mixing authority properties, see the [Migration Guide](migration-authority-vs-instance.md) for detailed upgrade paths. + +## Frequently Asked Questions + +For answers to common configuration questions and troubleshooting tips, see the [Authority Precedence FAQ](faq-authority-precedence.md). + +## Additional Resources + +- [Azure AD B2C Authority Examples](b2c-authority-examples.md) +- [CIAM Authority Examples](ciam-authority-examples.md) +- [Migration Guide: Authority vs Instance/TenantId](migration-authority-vs-instance.md) +- [Microsoft identity platform documentation](https://learn.microsoft.com/azure/active-directory/develop/) +- [Azure AD B2C documentation](https://learn.microsoft.com/azure/active-directory-b2c/) diff --git a/docs/b2c-authority-examples.md b/docs/b2c-authority-examples.md new file mode 100644 index 000000000..9740bf21a --- /dev/null +++ b/docs/b2c-authority-examples.md @@ -0,0 +1,361 @@ +# Azure AD B2C Authority Configuration Examples + +This guide provides detailed examples and best practices for configuring authentication authorities in Azure AD B2C scenarios using Microsoft.Identity.Web. + +## Overview + +Azure AD B2C (Business to Consumer) is a cloud identity service that enables custom sign-up, sign-in, and profile management for consumer-facing applications. B2C authority configuration has unique characteristics compared to standard Azure AD due to its policy-based architecture. + +## Key B2C Concepts + +### User Flows (Policies) + +B2C uses user flows (policies) to define authentication experiences: +- **Sign-up and Sign-in (SUSI)**: Combined registration and authentication flow +- **Password Reset**: Self-service password recovery +- **Profile Editing**: User profile modification + +Each policy has a unique identifier like `B2C_1_susi`, `B2C_1_reset`, or `B2C_1_edit_profile`. + +### B2C Authority Structure + +A complete B2C authority URL includes: +1. **B2C Login Instance**: `https://{tenant-name}.b2clogin.com/` +2. **Tenant Domain**: `{tenant-name}.onmicrosoft.com` +3. **Policy Path**: The user flow identifier + +Example: `https://contoso.b2clogin.com/contoso.onmicrosoft.com/B2C_1_susi` + +## Recommended Configuration Pattern + +### Primary Configuration (appsettings.json) + +Always use the complete `Authority` URL including the policy path. Do NOT split into `Instance` and `TenantId` for B2C. + +```json +{ + "AzureAdB2C": { + "Authority": "https://contoso.b2clogin.com/contoso.onmicrosoft.com/B2C_1_susi", + "ClientId": "11111111-1111-1111-1111-111111111111", + "Domain": "contoso.onmicrosoft.com", + "SignUpSignInPolicyId": "B2C_1_susi", + "ResetPasswordPolicyId": "B2C_1_reset", + "EditProfilePolicyId": "B2C_1_edit_profile" + } +} +``` + +### Multiple Policies + +When your application supports multiple user flows, configure all policy IDs: + +```json +{ + "AzureAdB2C": { + "Authority": "https://fabrikam.b2clogin.com/fabrikam.onmicrosoft.com/B2C_1_signupsignin1", + "ClientId": "22222222-2222-2222-2222-222222222222", + "Domain": "fabrikam.onmicrosoft.com", + "SignUpSignInPolicyId": "B2C_1_signupsignin1", + "ResetPasswordPolicyId": "B2C_1_passwordreset1", + "EditProfilePolicyId": "B2C_1_profileediting1", + "CallbackPath": "/signin-oidc" + } +} +``` + +### Code Configuration (Program.cs or Startup.cs) + +```csharp +builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(options => + { + builder.Configuration.Bind("AzureAdB2C", options); + }); +``` + +## Legacy /tfp/ Path Normalization + +### Historical Context + +Older B2C implementations used the `/tfp/` (Trust Framework Policy) path segment: + +``` +https://contoso.b2clogin.com/tfp/contoso.onmicrosoft.com/B2C_1_susi +``` + +### Automatic Normalization + +Microsoft.Identity.Web **automatically removes** the `/tfp/` segment during authority preparation. Both formats work identically: + +**Legacy format** (auto-normalized): +```json +{ + "AzureAdB2C": { + "Authority": "https://contoso.b2clogin.com/tfp/contoso.onmicrosoft.com/B2C_1_susi", + "ClientId": "11111111-1111-1111-1111-111111111111", + "Domain": "contoso.onmicrosoft.com" + } +} +``` + +**Modern format** (recommended): +```json +{ + "AzureAdB2C": { + "Authority": "https://contoso.b2clogin.com/contoso.onmicrosoft.com/B2C_1_susi", + "ClientId": "11111111-1111-1111-1111-111111111111", + "Domain": "contoso.onmicrosoft.com" + } +} +``` + +Both configurations result in the same prepared instance: `https://contoso.b2clogin.com/contoso.onmicrosoft.com/` + +### Migration from /tfp/ + +If you're migrating from legacy `/tfp/` URLs: + +1. **No action required**: Your existing configuration continues to work +2. **Optional cleanup**: Remove `/tfp/` from your configuration for clarity +3. **No breaking changes**: The normalization is transparent to your application + +## Custom Domains in B2C + +### Standard b2clogin.com Domain + +Most B2C tenants use the standard `{tenant}.b2clogin.com` domain: + +```json +{ + "AzureAdB2C": { + "Authority": "https://contoso.b2clogin.com/contoso.onmicrosoft.com/B2C_1_susi", + "ClientId": "11111111-1111-1111-1111-111111111111", + "Domain": "contoso.onmicrosoft.com" + } +} +``` + +### Custom Domain Configuration + +For B2C custom domains (e.g., `login.contoso.com`): + +```json +{ + "AzureAdB2C": { + "Authority": "https://login.contoso.com/contoso.onmicrosoft.com/B2C_1_susi", + "ClientId": "11111111-1111-1111-1111-111111111111", + "Domain": "contoso.onmicrosoft.com" + } +} +``` + +**Note**: Custom domains require additional B2C tenant configuration. See [Azure AD B2C custom domains documentation](https://learn.microsoft.com/azure/active-directory-b2c/custom-domain). + +## Policy Switching at Runtime + +### Handling Password Reset Flow + +B2C password reset is typically invoked from the sign-in page. Configure error handling: + +```csharp +builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(options => + { + builder.Configuration.Bind("AzureAdB2C", options); + + options.Events = new OpenIdConnectEvents + { + OnRemoteFailure = async context => + { + // Handle password reset error + if (context.Failure?.Message?.Contains("AADB2C90118") == true) + { + context.Response.Redirect($"/AzureAdB2C/Account/ResetPassword"); + context.HandleResponse(); + } + } + }; + }); +``` + +### Profile Edit Flow + +```csharp +[Authorize] +public IActionResult EditProfile() +{ + var editProfileUrl = $"{Configuration["AzureAdB2C:Instance"]}" + + $"{Configuration["AzureAdB2C:Domain"]}/" + + $"{Configuration["AzureAdB2C:EditProfilePolicyId"]}" + + "/oauth2/v2.0/authorize" + + $"?client_id={Configuration["AzureAdB2C:ClientId"]}" + + $"&redirect_uri={Request.Scheme}://{Request.Host}/signin-oidc" + + "&response_type=id_token" + + "&scope=openid profile" + + "&response_mode=form_post" + + $"&nonce={Guid.NewGuid()}"; + + return Redirect(editProfileUrl); +} +``` + +## Common B2C Configuration Mistakes + +### āŒ Mistake 1: Mixing Authority with Instance/TenantId + +**Wrong**: +```json +{ + "AzureAdB2C": { + "Authority": "https://contoso.b2clogin.com/contoso.onmicrosoft.com/B2C_1_susi", + "Instance": "https://contoso.b2clogin.com/", + "TenantId": "contoso.onmicrosoft.com", + "ClientId": "11111111-1111-1111-1111-111111111111" + } +} +``` + +**Correct**: Choose ONE approach: +```json +{ + "AzureAdB2C": { + "Authority": "https://contoso.b2clogin.com/contoso.onmicrosoft.com/B2C_1_susi", + "ClientId": "11111111-1111-1111-1111-111111111111", + "Domain": "contoso.onmicrosoft.com" + } +} +``` + +### āŒ Mistake 2: Omitting Policy from Authority + +**Wrong**: +```json +{ + "AzureAdB2C": { + "Authority": "https://contoso.b2clogin.com/contoso.onmicrosoft.com", + "ClientId": "11111111-1111-1111-1111-111111111111" + } +} +``` + +**Correct**: Include the policy in the Authority: +```json +{ + "AzureAdB2C": { + "Authority": "https://contoso.b2clogin.com/contoso.onmicrosoft.com/B2C_1_susi", + "ClientId": "11111111-1111-1111-1111-111111111111", + "Domain": "contoso.onmicrosoft.com", + "SignUpSignInPolicyId": "B2C_1_susi" + } +} +``` + +### āŒ Mistake 3: Using login.microsoftonline.com for B2C + +**Wrong**: +```json +{ + "AzureAdB2C": { + "Authority": "https://login.microsoftonline.com/contoso.onmicrosoft.com/B2C_1_susi", + "ClientId": "11111111-1111-1111-1111-111111111111" + } +} +``` + +**Correct**: Use the B2C-specific domain: +```json +{ + "AzureAdB2C": { + "Authority": "https://contoso.b2clogin.com/contoso.onmicrosoft.com/B2C_1_susi", + "ClientId": "11111111-1111-1111-1111-111111111111", + "Domain": "contoso.onmicrosoft.com" + } +} +``` + +## Multi-Region B2C + +For applications deployed across regions with B2C instances in multiple geographies: + +### Configuration per Region + +**US Region**: +```json +{ + "AzureAdB2C": { + "Authority": "https://contoso.b2clogin.com/contoso.onmicrosoft.com/B2C_1_susi", + "ClientId": "11111111-1111-1111-1111-111111111111", + "Domain": "contoso.onmicrosoft.com" + } +} +``` + +**EU Region** (using custom domain for GDPR compliance): +```json +{ + "AzureAdB2C": { + "Authority": "https://login.contoso.eu/contoso.onmicrosoft.com/B2C_1_susi", + "ClientId": "11111111-1111-1111-1111-111111111111", + "Domain": "contoso.onmicrosoft.com" + } +} +``` + +Use environment-specific configuration files (`appsettings.Production.json`, `appsettings.Development.json`) to manage regional variations. + +## Testing and Validation + +### Verify B2C Configuration + +1. **Check Authority Format**: Ensure it includes instance, tenant domain, and policy +2. **Validate Policy IDs**: Confirm they match your B2C tenant configuration +3. **Test All Flows**: Sign-up, sign-in, password reset, and profile edit +4. **Monitor Logs**: Watch for EventId 408 warnings indicating configuration conflicts + +### Common Validation Errors + +**Error**: "AADB2C90008: The provided grant has not been issued for this endpoint" +- **Cause**: Mismatch between configured authority and actual policy +- **Fix**: Verify the policy ID in your Authority matches the B2C tenant + +**Error**: "AADB2C90091: The user has cancelled entering self-asserted information" +- **Cause**: User canceled the flow (not a configuration error) +- **Fix**: Handle gracefully in your application + +## Migration from Microsoft.Identity.Web v1.x + +If upgrading from earlier versions: + +### Before (v1.x) +```json +{ + "AzureAdB2C": { + "Instance": "https://contoso.b2clogin.com/tfp/", + "ClientId": "11111111-1111-1111-1111-111111111111", + "Domain": "contoso.onmicrosoft.com", + "SignUpSignInPolicyId": "B2C_1_susi" + } +} +``` + +### After (v2.x and later) +```json +{ + "AzureAdB2C": { + "Authority": "https://contoso.b2clogin.com/contoso.onmicrosoft.com/B2C_1_susi", + "ClientId": "11111111-1111-1111-1111-111111111111", + "Domain": "contoso.onmicrosoft.com", + "SignUpSignInPolicyId": "B2C_1_susi" + } +} +``` + +**Note**: The v1.x pattern still works due to backward compatibility, but the Authority-based approach is recommended for clarity. + +## Additional Resources + +- [Authority Configuration & Precedence Guide](authority-configuration.md) +- [CIAM Authority Examples](ciam-authority-examples.md) +- [Migration Guide](migration-authority-vs-instance.md) +- [Azure AD B2C documentation](https://learn.microsoft.com/azure/active-directory-b2c/) +- [B2C custom policies](https://learn.microsoft.com/azure/active-directory-b2c/custom-policy-overview) 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..10867244c --- /dev/null +++ b/docs/calling-downstream-apis/calling-downstream-apis-README.md @@ -0,0 +1,498 @@ +# 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 with Token Binding | Enhanced security with certificate binding (mTLS PoP) | IDownstreamApi with `ProtocolScheme: "MTLS_POP"` | +| 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 + +### Token Binding (mTLS PoP) +- **Scenario**: Enhanced security where tokens are cryptographically bound to certificates as per [RFC 8705](https://datatracker.ietf.org/doc/html/rfc8705) +- **Token type**: Access token with certificate binding (`cnf` claim) +- **Methods**: `GetForAppAsync()` with `ProtocolScheme: "MTLS_POP"` +- **Security**: Prevents token theft by binding tokens to specific certificates + +[šŸ“– Learn more about Token Binding with mTLS PoP](token-binding.md) + +## šŸš€ 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) + +### Token Binding with mTLS PoP (Enhanced Security) + +Token binding provides enhanced security by cryptographically binding access tokens to X.509 certificates. Even if a token is intercepted, it cannot be used without the corresponding certificate. + +```csharp +// Installation +// dotnet add package Microsoft.Identity.Web.DownstreamApi + +// appsettings.json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + "ClientCredentials": [ + { + "SourceType": "StoreWithDistinguishedName", + "CertificateStorePath": "CurrentUser/My", + "CertificateDistinguishedName": "CN=YourCertificate" + } + ], + "SendX5c": true + }, + "SecureApi": { + "BaseUrl": "https://api.contoso.com/", + "RelativePath": "api/data", + "ProtocolScheme": "MTLS_POP", + "RequestAppToken": true, + "Scopes": [ "api://your-api/.default" ] + } +} + +// Startup configuration +builder.Services.AddDownstreamApi( + "SecureApi", + builder.Configuration.GetSection("SecureApi")); + +// Usage +public class SecureApiService +{ + private readonly IDownstreamApi _api; + + public SecureApiService(IDownstreamApi api) + { + _api = api; + } + + public async Task GetSecureDataAsync() + { + // Token is bound to certificate - enhanced security + return await _api.GetForAppAsync("SecureApi"); + } +} +``` + +**Key Benefits:** +- šŸ”’ **Token theft protection**: Stolen tokens are useless without the certificate +- šŸ›”ļø **Replay attack prevention**: Tokens cannot be replayed from different clients +- āœ… **Zero trust alignment**: Strong cryptographic binding between client and token + +[šŸ“– Learn more about Token Binding (mTLS PoP)](token-binding.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 + +- **[Token Binding (mTLS PoP)](token-binding.md)** - Enhanced security with certificate-bound tokens +- **[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 new file mode 100644 index 000000000..3d4f17638 --- /dev/null +++ b/docs/calling-downstream-apis/custom-apis.md @@ -0,0 +1,948 @@ +# Calling Custom APIs + +This guide explains the different approaches for calling your own protected APIs using Microsoft.Identity.Web: IDownstreamApi, IAuthorizationHeaderProvider, and MicrosoftIdentityMessageHandler. + +## Overview + +When calling custom REST APIs, you have three main options depending on your needs: + +| 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 | + +## IDownstreamApi - Recommended for Most Scenarios + +`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); + +// 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(); +``` + +### Basic Usage + +```csharp +using Microsoft.Identity.Abstractions; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +[Authorize] +public class ProductsController : Controller +{ + 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(); + } +} +``` + +### Advanced IDownstreamApi Options + +#### 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) + +Use this when you want to configure authentication options on a per-request basis: + +```csharp +services.AddHttpClient("FlexibleClient") + .AddMicrosoftIdentityMessageHandler(); + +// Later, in a service: +var request = new HttpRequestMessage(HttpMethod.Get, "/api/data") + .WithAuthenticationOptions(options => + { + options.Scopes.Add("https://api.example.com/.default"); + }); + +var response = await httpClient.SendAsync(request); +``` + +#### 2. Options Instance Overload + +Use this when you have a pre-configured options object: + +```csharp +var options = new MicrosoftIdentityMessageHandlerOptions +{ + Scopes = { "https://graph.microsoft.com/.default" } +}; +options.WithAgentIdentity("agent-application-id"); + +services.AddHttpClient("GraphClient", client => +{ + client.BaseAddress = new Uri("https://graph.microsoft.com"); +}) +.AddMicrosoftIdentityMessageHandler(options); +``` + +#### 3. Action Delegate Overload (Inline Configuration) + +Use this for inline configuration - the most common scenario: + +```csharp +services.AddHttpClient("MyApiClient", client => +{ + client.BaseAddress = new Uri("https://api.example.com"); +}) +.AddMicrosoftIdentityMessageHandler(options => +{ + options.Scopes.Add("https://api.example.com/.default"); + options.RequestAppToken = true; +}); +``` + +#### 4. IConfiguration Overload (Configuration from appsettings.json) + +Use this to configure from appsettings.json: + +**appsettings.json:** +```json +{ + "DownstreamApi": { + "Scopes": ["https://api.example.com/.default"] + }, + "GraphApi": { + "Scopes": ["https://graph.microsoft.com/.default", "User.Read"] + } +} +``` + +**Program.cs:** +```csharp +services.AddHttpClient("DownstreamApiClient", client => +{ + client.BaseAddress = new Uri("https://api.example.com"); +}) +.AddMicrosoftIdentityMessageHandler( + configuration.GetSection("DownstreamApi"), + "DownstreamApi"); + +services.AddHttpClient("GraphClient", client => +{ + client.BaseAddress = new Uri("https://graph.microsoft.com"); +}) +.AddMicrosoftIdentityMessageHandler( + configuration.GetSection("GraphApi"), + "GraphApi"); +``` + +### Configuration Examples + +#### Example 1: Simple Web API Client + +```csharp +// Configure in Program.cs +services.AddHttpClient("WeatherApiClient", client => +{ + client.BaseAddress = new Uri("https://api.weather.com"); +}) +.AddMicrosoftIdentityMessageHandler(options => +{ + options.Scopes.Add("https://api.weather.com/.default"); +}); + +// Use in a controller or service +public class WeatherService +{ + private readonly HttpClient _httpClient; + + public WeatherService(IHttpClientFactory factory) + { + _httpClient = factory.CreateClient("WeatherApiClient"); + } + + public async Task GetForecastAsync(string city) + { + var response = await _httpClient.GetAsync($"/forecast/{city}"); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadFromJsonAsync(); + } +} +``` + +#### Example 2: Multiple API Clients + +```csharp +// Configure multiple clients in Program.cs +services.AddHttpClient("ApiClient1") + .AddMicrosoftIdentityMessageHandler(options => + { + options.Scopes.Add("https://api1.example.com/.default"); + }); + +services.AddHttpClient("ApiClient2") + .AddMicrosoftIdentityMessageHandler(options => + { + options.Scopes.Add("https://api2.example.com/.default"); + options.RequestAppToken = true; + }); + +// Use in a service +public class MultiApiService +{ + private readonly HttpClient _client1; + private readonly HttpClient _client2; + + public MultiApiService(IHttpClientFactory factory) + { + _client1 = factory.CreateClient("ApiClient1"); + _client2 = factory.CreateClient("ApiClient2"); + } + + public async Task GetFromBothApisAsync() + { + var data1 = await _client1.GetStringAsync("/data"); + var data2 = await _client2.GetStringAsync("/data"); + return $"{data1} | {data2}"; + } +} +``` + +#### Example 3: Configuration from appsettings.json with Complex Options + +**appsettings.json:** +```json +{ + "DownstreamApis": { + "CustomerApi": { + "Scopes": ["api://customer-api/.default"] + }, + "OrderApi": { + "Scopes": ["api://order-api/.default"] + }, + "InventoryApi": { + "Scopes": ["api://inventory-api/.default"] + } + } +} +``` + +**Program.cs:** +```csharp +var downstreamApis = configuration.GetSection("DownstreamApis"); + +services.AddHttpClient("CustomerApiClient", client => +{ + client.BaseAddress = new Uri("https://customer-api.example.com"); +}) +.AddMicrosoftIdentityMessageHandler( + downstreamApis.GetSection("CustomerApi"), + "CustomerApi"); + +services.AddHttpClient("OrderApiClient", client => +{ + client.BaseAddress = new Uri("https://order-api.example.com"); +}) +.AddMicrosoftIdentityMessageHandler( + downstreamApis.GetSection("OrderApi"), + "OrderApi"); + +services.AddHttpClient("InventoryApiClient", client => +{ + client.BaseAddress = new Uri("https://inventory-api.example.com"); +}) +.AddMicrosoftIdentityMessageHandler( + downstreamApis.GetSection("InventoryApi"), + "InventoryApi"); +``` + +### Per-Request Options + +You can override default options on a per-request basis using the `WithAuthenticationOptions` extension method: + +```csharp +// Configure client with default options +services.AddHttpClient("ApiClient") + .AddMicrosoftIdentityMessageHandler(options => + { + options.Scopes.Add("https://api.example.com/.default"); + }); + +// Override for specific requests +public class MyService +{ + private readonly HttpClient _httpClient; + + public MyService(IHttpClientFactory factory) + { + _httpClient = factory.CreateClient("ApiClient"); + } + + public async Task GetSensitiveDataAsync() + { + // Override scopes for this specific request + var request = new HttpRequestMessage(HttpMethod.Get, "/api/sensitive") + .WithAuthenticationOptions(options => + { + options.Scopes.Clear(); + options.Scopes.Add("https://api.example.com/sensitive.read"); + options.RequestAppToken = true; + }); + + var response = await _httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStringAsync(); + } +} +``` + +### Advanced Scenarios + +#### Agent Identity + +Use agent identity when your application needs to act on behalf of another application: + +```csharp +services.AddHttpClient("AgentClient") + .AddMicrosoftIdentityMessageHandler(options => + { + options.Scopes.Add("https://graph.microsoft.com/.default"); + options.WithAgentIdentity("agent-application-id"); + options.RequestAppToken = true; + }); +``` + +#### Composing with Other Handlers + +You can chain multiple handlers in the pipeline: + +```csharp +services.AddHttpClient("ApiClient") + .AddMicrosoftIdentityMessageHandler(options => + { + options.Scopes.Add("https://api.example.com/.default"); + }) + .AddHttpMessageHandler() + .AddHttpMessageHandler(); +``` + +#### WWW-Authenticate Challenge Handling + +`MicrosoftIdentityMessageHandler` automatically handles WWW-Authenticate challenges for Conditional Access scenarios: + +```csharp +// No additional code needed - automatic handling +services.AddHttpClient("ProtectedApiClient") + .AddMicrosoftIdentityMessageHandler(options => + { + options.Scopes.Add("https://api.example.com/.default"); + }); + +// The handler will automatically: +// 1. Detect 401 responses with WWW-Authenticate challenges +// 2. Extract required claims from the challenge +// 3. Acquire a new token with the additional claims +// 4. Retry the request with the new token +``` + +### Error Handling + +```csharp +public class MyService +{ + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + + public MyService(IHttpClientFactory factory, ILogger logger) + { + _httpClient = factory.CreateClient("ApiClient"); + _logger = logger; + } + + public async Task GetDataWithErrorHandlingAsync() + { + try + { + var response = await _httpClient.GetAsync("/api/data"); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStringAsync(); + } + catch (MicrosoftIdentityAuthenticationException authEx) + { + _logger.LogError(authEx, "Authentication failed: {Message}", authEx.Message); + throw; + } + catch (HttpRequestException httpEx) + { + _logger.LogError(httpEx, "HTTP request failed: {Message}", httpEx.Message); + throw; + } + } +} +``` + +## IAuthorizationHeaderProvider - Maximum Control + +`IAuthorizationHeaderProvider` gives you direct access to authorization headers for complete control over HTTP requests. + +### 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) + +--- + +**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..16bb24222 --- /dev/null +++ b/docs/calling-downstream-apis/from-web-apis.md @@ -0,0 +1,688 @@ +# 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 +``` + +### Session Keys + +Long-running OBO processes use a **session key** to associate a cached OBO token with a particular background workflow. There are two options: + +| Approach | When to use | +|---|---| +| **Explicit key** – you supply your own key (e.g. a `Guid`) | You already have a natural identifier for the work item (process ID, job ID, etc.) | +| **`AllocateForMe`** – the token layer auto-generates a key | You don't have a natural identifier, or you want the identity platform to manage key uniqueness. The SDK will use `hash(client_token)` internally | + +### Long-Running Process Pattern with an Explicit Key + +```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 + } + } +} +``` + +### Long-Running Process Pattern with `AllocateForMe` + +Instead of managing your own key, set `LongRunningWebApiSessionKey` to the special sentinel value **`AcquireTokenOptions.LongRunningWebApiSessionKeyAuto`** (the string `"AllocateForMe"`). On the first call the token acquisition layer will auto-generate a unique session key and write it back to the same `AcquireTokenOptions` instance. You then read the generated key and pass it on all subsequent calls. + +```csharp +[Authorize] +[ApiController] +[Route("api/[controller]")] +public class AutoKeyProcessingController : ControllerBase +{ + private readonly IDownstreamApi _downstreamApi; + private readonly IBackgroundTaskQueue _taskQueue; + + public AutoKeyProcessingController( + IDownstreamApi downstreamApi, + IBackgroundTaskQueue taskQueue) + { + _downstreamApi = downstreamApi; + _taskQueue = taskQueue; + } + + [HttpPost("start")] + public async Task> StartLongProcess([FromBody] ProcessRequest request) + { + // ── First call: let the platform allocate a session key ── + var options = new DownstreamApiOptions + { + RelativePath = "api/process/data", + AcquireTokenOptions = new AcquireTokenOptions + { + // Sentinel value — the platform will replace this with a generated key + LongRunningWebApiSessionKey = AcquireTokenOptions.LongRunningWebApiSessionKeyAuto // "AllocateForMe" + } + }; + + var data = await _downstreamApi.GetForUserAsync( + "PartnerAPI", + optionsOverride => { + optionsOverride.RelativePath = options.RelativePath; + optionsOverride.AcquireTokenOptions.LongRunningWebApiSessionKey = + options.AcquireTokenOptions.LongRunningWebApiSessionKey; + }); + + // After the call, the platform has replaced the sentinel with the generated key. + string generatedSessionKey = options.AcquireTokenOptions.LongRunningWebApiSessionKey; + // generatedSessionKey is now a unique string such as "a1b2c3d4..." — no longer "AllocateForMe". + + // ── Queue background work using the generated key ── + _taskQueue.QueueBackgroundWorkItem(async (cancellationToken) => + { + await ContinueProcessingAsync(generatedSessionKey, data, cancellationToken); + }); + + return Accepted(new ProcessStatus + { + SessionKey = generatedSessionKey, + Status = "Started" + }); + } + + private async Task ContinueProcessingAsync( + string sessionKey, + ProcessData data, + CancellationToken cancellationToken) + { + // Process data... + await Task.Delay(TimeSpan.FromMinutes(5), cancellationToken); + + // ── Subsequent calls: reuse the generated session key ── + await _downstreamApi.PostForUserAsync( + "PartnerAPI", + options => { + options.RelativePath = "api/process/complete"; + options.AcquireTokenOptions.LongRunningWebApiSessionKey = sessionKey; + }, + data, + cancellationToken: cancellationToken); + } +} +``` + +### Important Considerations + +1. **Session Key Lifetime**: Store the generated session key alongside your work item (database, queue message, etc.) so background workers can retrieve it. +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/calling-downstream-apis/token-binding.md b/docs/calling-downstream-apis/token-binding.md new file mode 100644 index 000000000..d4dbf727b --- /dev/null +++ b/docs/calling-downstream-apis/token-binding.md @@ -0,0 +1,612 @@ +# Token Binding with mTLS Proof-of-Possession (mTLS PoP) + +## Overview + +> Note that not all clients are allowed to obtain mTLS PoP certificate as this feature is currently in private preview. + +Certificate token binding (also known as mTLS PoP - Mutual TLS Proof-of-Possession) is an advanced security feature that cryptographically binds access tokens to a specific X.509 certificate. It is described in [RFC 8705](https://datatracker.ietf.org/doc/html/rfc8705). The binding ensures that even if a token is intercepted, it cannot be used by an attacker without possession of the corresponding private key. + +### How It Works + +1. **Token Acquisition**: When requesting an access token with token binding enabled, Microsoft Identity Web includes the certificate's thumbprint in the token request +2. **Token Binding**: The authorization server embeds a `cnf` (confirmation) claim in the issued token containing the certificate's SHA-256 thumbprint (`x5t#S256`) +3. **API Call**: The client presents both the bound token and the certificate when calling the downstream API +4. **Verification**: The API validates that the certificate presented matches the certificate reference in the token's `cnf` claim + +```mermaid +sequenceDiagram + participant Client + participant Azure AD + participant API + + Client->>Azure AD: Token request with certificate thumbprint + Azure AD->>Client: Token with cnf claim (bound to certificate) + Client->>API: MTLS_POP token + Client certificate + API->>API: Validate token and certificate binding + API->>Client: Protected resource +``` + +### Security Benefits + +- **Token Theft Protection**: Stolen tokens are useless without the corresponding certificate +- **Replay Attack Prevention**: Tokens cannot be replayed from different clients +- **Enhanced Authentication**: Combines "something you have" (certificate) with traditional OAuth2 flows +- **Zero Trust Architecture**: Aligns with zero trust principles by binding credentials to specific devices + +## Configuration + +### Client Application Configuration + +#### 1. Configure Azure AD Settings + +In your `appsettings.json`, configure your Azure AD settings including the certificate: + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + "ClientCredentials": [ + { + "SourceType": "StoreWithDistinguishedName", + "CertificateStorePath": "CurrentUser/My", + "CertificateDistinguishedName": "CN=YourCertificate" + } + ], + "SendX5c": true + } +} +``` + +#### 2. Configure Downstream API with Token Binding + +Configure your downstream API section with the `MTLS_POP` protocol scheme: + +```json +{ + "DownstreamApi": { + "BaseUrl": "https://api.contoso.com/", + "RelativePath": "api/data", + "ProtocolScheme": "MTLS_POP", + "RequestAppToken": true, + "Scopes": [ "api://your-api-scope/.default" ] + } +} +``` + +**Important Configuration Properties:** +- `ProtocolScheme`: Must be set to `"MTLS_POP"` to enable token binding +- `RequestAppToken`: Must be `true` (token binding currently supports only application tokens) +- `Scopes`: API scopes required for the downstream API call + +#### 3. Register Services + +```csharp +using Microsoft.Identity.Web; + +var builder = WebApplication.CreateBuilder(args); + +// Option 1: Using TokenAcquirerFactory (for console apps, background services) +var tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance(); + +tokenAcquirerFactory.Services.AddDownstreamApi( + "DownstreamApi", + tokenAcquirerFactory.Configuration.GetSection("DownstreamApi")); + +var serviceProvider = tokenAcquirerFactory.Build(); + +// Option 2: Using ASP.NET Core DI (for web apps, web APIs) +builder.Services.AddAuthentication() + .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd")); + +builder.Services.AddDownstreamApi( + "DownstreamApi", + builder.Configuration.GetSection("DownstreamApi")); +``` + +### API Server Configuration + +The downstream API must validate both the token and the certificate binding. Here's a complete example: + +#### 1. Register Authentication Handlers + +```csharp +using Microsoft.Identity.Web; + +var builder = WebApplication.CreateBuilder(args); + +// Add standard JWT Bearer authentication +builder.Services.AddMicrosoftIdentityWebApiAuthentication(builder.Configuration); + +// Add custom MTLS_POP authentication handler +builder.Services.AddAuthentication() + .AddScheme( + "MTLS_POP", + options => { }); + +var app = builder.Build(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapControllers(); +app.Run(); +``` + +#### 2. Implement mTLS PoP Authentication Handler + +```csharp +using System.Security.Claims; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text.Encodings.Web; +using System.Text.Json; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Tokens; + +public class MtlsPopAuthenticationHandler : AuthenticationHandler +{ + public const string ProtocolScheme = "MTLS_POP"; + + public MtlsPopAuthenticationHandler( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder) + : base(options, logger, encoder) + { + } + + protected override async Task HandleAuthenticateAsync() + { + // 1. Extract the MTLS_POP authorization header + var authHeader = Request.Headers.Authorization.FirstOrDefault(); + if (string.IsNullOrEmpty(authHeader) || + !authHeader.StartsWith($"{ProtocolScheme} ", StringComparison.OrdinalIgnoreCase)) + { + return AuthenticateResult.NoResult(); + } + + var authToken = authHeader.Substring($"{ProtocolScheme} ".Length).Trim(); + + try + { + // 2. Parse the JWT token + var handler = new JsonWebTokenHandler(); + var token = handler.ReadJsonWebToken(authToken); + + // 3. Extract the 'cnf' claim + var cnfClaim = token.Claims.FirstOrDefault(c => c.Type == "cnf"); + if (cnfClaim == null) + { + return AuthenticateResult.Fail("Missing 'cnf' claim in MTLS_POP token"); + } + + // 4. Extract certificate thumbprint from cnf claim + var cnfJson = JsonDocument.Parse(cnfClaim.Value); + if (!cnfJson.RootElement.TryGetProperty("x5t#S256", out var x5tS256Element)) + { + return AuthenticateResult.Fail("Missing 'x5t#S256' in cnf claim"); + } + + var expectedThumbprint = x5tS256Element.GetString(); + + // 5. Get client certificate from TLS connection + var clientCert = Context.Connection.ClientCertificate; + if (clientCert != null) + { + var actualThumbprint = GetCertificateThumbprint(clientCert); + + // 6. Validate certificate binding + if (!string.Equals(actualThumbprint, expectedThumbprint, + StringComparison.OrdinalIgnoreCase)) + { + return AuthenticateResult.Fail( + "Certificate thumbprint mismatch with cnf claim"); + } + } + + // 7. Create claims principal + var claims = token.Claims.Select(c => new Claim(c.Type, c.Value)).ToList(); + var identity = new ClaimsIdentity(claims, ProtocolScheme); + var principal = new ClaimsPrincipal(identity); + var ticket = new AuthenticationTicket(principal, ProtocolScheme); + + return AuthenticateResult.Success(ticket); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error validating mTLS PoP token"); + return AuthenticateResult.Fail($"Validation error: {ex.Message}"); + } + } + + private static string GetCertificateThumbprint(X509Certificate2 certificate) + { + using var sha256 = SHA256.Create(); + var hash = sha256.ComputeHash(certificate.RawData); + return Base64UrlEncoder.Encode(hash); + } +} +``` + +## Usage Examples + +### Console Application / Daemon Service + +```csharp +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Web; + +public class Program +{ + public static async Task Main(string[] args) + { + // Create and configure token acquirer + var tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance(); + + tokenAcquirerFactory.Services.AddDownstreamApi( + "SecureApi", + tokenAcquirerFactory.Configuration.GetSection("SecureApi")); + + var serviceProvider = tokenAcquirerFactory.Build(); + + // Get IDownstreamApi instance + var downstreamApi = serviceProvider.GetRequiredService(); + + // Call API with mTLS PoP token + var response = await downstreamApi.GetForAppAsync("SecureApi"); + + Console.WriteLine($"Result: {response?.Data}"); + } +} + +public class ApiResponse +{ + public string? Data { get; set; } +} +``` + +### ASP.NET Core Web Application + +```csharp +using Microsoft.AspNetCore.Mvc; +using Microsoft.Identity.Abstractions; + +[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] + public async Task GetSecureData() + { + try + { + // Call downstream API with mTLS PoP token binding + var data = await _downstreamApi.GetForAppAsync( + "SecureApi"); + + return Ok(data); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to retrieve secure data"); + return StatusCode(500, "Failed to retrieve data"); + } + } +} + +public class SecureData +{ + public string? Id { get; set; } + public string? Value { get; set; } +} +``` + +### Using DownstreamApiOptions Programmatically + +```csharp +using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Web; + +public class SecureApiService +{ + private readonly IDownstreamApi _downstreamApi; + + public SecureApiService(IDownstreamApi downstreamApi) + { + _downstreamApi = downstreamApi; + } + + public async Task CallSecureApiAsync(string endpoint) where T : class + { + return await _downstreamApi.GetForAppAsync( + serviceName: null, + downstreamApiOptionsOverride: options => + { + options.BaseUrl = "https://api.secure.com"; + options.RelativePath = endpoint; + options.ProtocolScheme = "MTLS_POP"; + options.RequestAppToken = true; + options.Scopes = new[] { "api://secure-api/.default" }; + }); + } +} +``` + +### Custom HttpClient with Authorization Header Provider + +For scenarios requiring more control over HTTP requests: + +```csharp +using Microsoft.Identity.Abstractions; +using System.Net.Http.Headers; + +public class CustomApiClient +{ + private readonly IAuthorizationHeaderProvider _authProvider; + private readonly IHttpClientFactory _httpClientFactory; + + public CustomApiClient( + IAuthorizationHeaderProvider authProvider, + IHttpClientFactory httpClientFactory) + { + _authProvider = authProvider; + _httpClientFactory = httpClientFactory; + } + + public async Task CallApiWithCustomLogicAsync() + { + // Create downstream API options for mTLS PoP + var apiOptions = new DownstreamApiOptions + { + BaseUrl = "https://api.contoso.com", + ProtocolScheme = "MTLS_POP", + RequestAppToken = true, + Scopes = new[] { "api://contoso/.default" } + }; + + // Get authorization header with binding certificate info + var authResult = await (_authProvider as IBoundAuthorizationHeaderProvider) + ?.CreateBoundAuthorizationHeaderAsync(apiOptions)!; + + if (authResult.IsSuccess) + { + // Create HTTP client with certificate binding + var httpClient = authResult.Value.BindingCertificate != null + ? CreateMtlsHttpClient(authResult.Value.BindingCertificate) + : _httpClientFactory.CreateClient(); + + // Set authorization header + httpClient.DefaultRequestHeaders.Authorization = + AuthenticationHeaderValue.Parse(authResult.Value.AuthorizationHeaderValue); + + // Make API call + var response = await httpClient.GetAsync( + $"{apiOptions.BaseUrl}/api/endpoint"); + + return await response.Content.ReadAsStringAsync(); + } + + throw new InvalidOperationException("Failed to acquire token"); + } + + private HttpClient CreateMtlsHttpClient(X509Certificate2 certificate) + { + var handler = new HttpClientHandler(); + handler.ClientCertificates.Add(certificate); + return new HttpClient(handler); + } +} +``` + +## Token Structure + +### Standard OAuth2 Token +```json +{ + "aud": "api://your-api", + "iss": "https://login.microsoftonline.com/tenant-id/", + "iat": 1234567890, + "exp": 1234571490, + "appid": "client-id", + "tid": "tenant-id" +} +``` + +### mTLS PoP Token with Binding +```json +{ + "aud": "api://your-api", + "iss": "https://login.microsoftonline.com/tenant-id/", + "iat": 1234567890, + "exp": 1234571490, + "appid": "client-id", + "tid": "tenant-id", + "cnf": { + "x5t#S256": "buc7x2HxS_hPnVJb9J5mwPr6jCw8Y_2LHDz-gp_-6KM" + } +} +``` + +The `cnf` (confirmation) claim contains the SHA-256 thumbprint of the certificate, Base64Url-encoded. + +## Current Limitations + +### Application Tokens Only +Token binding currently supports **only application (app-only) tokens**. Delegated (user) tokens are not supported. + +### Protocol Scheme Requirement +The `ProtocolScheme` property must be explicitly set to `"MTLS_POP"` to enable token binding. If not set, standard Bearer authentication is used. + +### Certificate Requirements +- The certificate must be configured in `ClientCredentials` with `SendX5c` set to `true` +- The certificate must be accessible at token acquisition time + +## Troubleshooting + +### Common Issues + +#### 1. "Missing 'cnf' claim in token" + +**Cause**: Token binding was not properly configured or the token is a standard Bearer token. + +**Solution**: +```json +{ + "DownstreamApi": { + "ProtocolScheme": "MTLS_POP", // ensure this is set + "RequestAppToken": true + } +} +``` + +#### 2. "Certificate thumbprint mismatch" + +**Cause**: The certificate presented to the API doesn't match the one used for token acquisition. + +**Solution**: +- Verify the same certificate is used for both token acquisition and API calls +- Check certificate loading configuration in `ClientCredentials` +- Ensure certificate is not expired or renewed + +#### 3. "A certificate, which is required for token binding, is missing" + +**Cause**: No certificate is configured in Azure AD settings. + +**Solution**: +```json +{ + "AzureAd": { + "ClientCredentials": [ + { + "SourceType": "StoreWithDistinguishedName", + "CertificateStorePath": "CurrentUser/My", + "CertificateDistinguishedName": "CN=YourCertificate" + } + ], + "SendX5c": true // required for token binding + } +} +``` + +#### 4. "Token binding requires enabled app token acquisition" + +**Cause**: `RequestAppToken` is not set to `true`. + +**Solution**: +```csharp +var options = new DownstreamApiOptions +{ + ProtocolScheme = "MTLS_POP", + RequestAppToken = true, // must be true +}; +``` + +### Debugging Tips + +#### Enable Detailed Logging + +```json +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Identity.Web": "Debug" + } + } +} +``` + +#### Inspect Token Claims + +```csharp +var handler = new JsonWebTokenHandler(); +var token = handler.ReadJsonWebToken(tokenString); + +foreach (var claim in token.Claims) +{ + Console.WriteLine($"{claim.Type}: {claim.Value}"); +} + +// Look for 'cnf' claim with x5t#S256 +var cnfClaim = token.Claims.FirstOrDefault(c => c.Type == "cnf"); +``` + +#### Verify Certificate Thumbprint + +```csharp +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using Microsoft.IdentityModel.Tokens; + +var cert = new X509Certificate2("path/to/cert.pfx", "password"); +using var sha256 = SHA256.Create(); +var hash = sha256.ComputeHash(cert.RawData); +var thumbprint = Base64UrlEncoder.Encode(hash); +Console.WriteLine($"Certificate thumbprint: {thumbprint}"); +``` + +## Security Considerations + +### Certificate Management +- **Store securely**: Use Azure Key Vault or secure certificate stores +- **Rotate regularly**: Implement certificate rotation procedures +- **Monitor expiration**: Set up alerts for certificate expiration +- **Restrict access**: Limit who can access certificate private keys + +### Network Security +- **Require TLS 1.2+**: Ensure all connections use modern TLS versions +- **Validate certificates**: Implement proper certificate validation on the server +- **Use strong ciphers**: Configure secure cipher suites + +### Token Handling +- **Short lifetimes**: Use short-lived tokens (recommended: 1 hour) +- **Proper storage**: Never log or expose tokens +- **Validate thoroughly**: Check all claims, expiration, and binding + +## Best Practices + +1. **Always use HTTPS**: mTLS PoP requires secure transport +2. **Use a certificate that stores the private key material in hardware, e.g., in TPM**: Utilize hardware over software security for a better protection +3. **Implement proper error handling**: Gracefully handle certificate and token errors +4. **Monitor certificate expiration**: Automate certificate renewal +5. **Use separate certificates per environment**: Dev, staging, and production certificates +6. **Log security events**: Track token binding failures and certificate mismatches +7. **Test certificate rotation**: Ensure your application handles certificate updates +8. **Document your configuration**: Keep clear documentation of certificate requirements + +## Related Resources + +- [Microsoft Identity Web Documentation](../README.md) +- [Calling Downstream APIs Overview](calling-downstream-apis-README.md) +- [Custom APIs Documentation](custom-apis.md) +- [Microsoft Entra Certificate Credentials](https://learn.microsoft.com/entra/identity-platform/certificate-credentials) +- [OAuth 2.0 Mutual-TLS Client Authentication](https://datatracker.ietf.org/doc/html/rfc8705) + +## Sample Code + +Complete working samples demonstrating mTLS PoP token binding are available in the repository: + +- **Client Application**: [`tests/DevApps/MtlsPop/MtlsPopClient`](../../tests/DevApps/MtlsPop/MtlsPopClient) +- **Web API Server**: [`tests/DevApps/MtlsPop/MtlsPopWebApi`](../../tests/DevApps/MtlsPop/MtlsPopWebApi) + +These samples demonstrate: +- Complete client and server configuration +- Token acquisition with certificate binding +- Custom authentication handler implementation +- Certificate validation and thumbprint verification diff --git a/docs/ciam-authority-examples.md b/docs/ciam-authority-examples.md new file mode 100644 index 000000000..7f7fad5d6 --- /dev/null +++ b/docs/ciam-authority-examples.md @@ -0,0 +1,351 @@ +# CIAM (Customer Identity Access Management) Authority Configuration Examples + +This guide provides detailed examples and best practices for configuring authentication authorities in Microsoft Entra External ID (formerly CIAM - Customer Identity and Access Management) scenarios using Microsoft.Identity.Web. + +## Overview + +Microsoft Entra External ID (CIAM) is a customer identity and access management solution that enables organizations to create secure, customized sign-in experiences for customer-facing applications. CIAM authority configuration has specific requirements, particularly when using custom domains. + +## Key CIAM Concepts + +### CIAM vs Traditional Azure AD + +| Feature | Traditional Azure AD | CIAM | +|---------|---------------------|------| +| **Primary Use Case** | Employee/organizational identity | Customer/consumer identity | +| **Default Domain** | `login.microsoftonline.com` | `{tenant}.ciamlogin.com` | +| **Custom Domains** | Optional | Commonly used for branding | +| **Authority Handling** | Parsed into Instance + TenantId | Use complete Authority URL | + +## Recommended Configuration Patterns + +### Standard CIAM Domain (ciamlogin.com) + +For CIAM tenants using the default `.ciamlogin.com` domain: + +```json +{ + "AzureAd": { + "Authority": "https://contoso.ciamlogin.com/contoso.onmicrosoft.com", + "ClientId": "11111111-1111-1111-1111-111111111111", + "CallbackPath": "/signin-oidc" + } +} +``` + +**Key Points**: +- Include the full tenant domain in the Authority +- The library automatically handles CIAM authorities correctly +- Do not mix Authority with Instance/TenantId properties + +### CIAM with Custom Domain + +### CIAM with Custom Domain + +Custom domains are frequently used in CIAM for seamless brand experience: + +```json +{ + "AzureAd": { + "Authority": "https://login.contoso.com/contoso.onmicrosoft.com", + "ClientId": "11111111-1111-1111-1111-111111111111", + "CallbackPath": "/signin-oidc" + } +} +``` + +**Important**: Ensure your custom domain is properly configured in your CIAM tenant. The library handles custom CIAM domains automatically. + +### CIAM with Tenant ID (GUID) + +Using the tenant GUID instead of domain name: + +```json +{ + "AzureAd": { + "Authority": "https://contoso.ciamlogin.com/12345678-1234-1234-1234-123456789012", + "ClientId": "11111111-1111-1111-1111-111111111111" + } +} +``` + +### CIAM Multi-Tenant Scenario + +For CIAM applications supporting multiple customer tenants: + +```json +{ + "AzureAd": { + "Authority": "https://contoso.ciamlogin.com/common", + "ClientId": "11111111-1111-1111-1111-111111111111" +, + "ValidateIssuer": false + } +} +``` + +**Warning**: Multi-tenant CIAM scenarios require careful issuer validation configuration. Ensure you implement proper tenant isolation in your application logic. + +## Code Configuration + +### ASP.NET Core Startup Configuration + +**Program.cs (.NET 6+)**: +```csharp +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.Identity.Web; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(options => + { + builder.Configuration.Bind("AzureAd", options); + }); + +builder.Services.AddAuthorization(); +builder.Services.AddRazorPages(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline +app.UseAuthentication(); +app.UseAuthorization(); +app.MapRazorPages(); + +app.Run(); +``` + +### Advanced Configuration with Events + +```csharp +builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(options => + { + builder.Configuration.Bind("AzureAd", options); + + options.Events = new OpenIdConnectEvents + { + OnRedirectToIdentityProvider = context => + { + // Add custom parameters if needed + context.ProtocolMessage.SetParameter("ui_locales", "en-US"); + return Task.CompletedTask; + }, + OnTokenValidated = context => + { + // Custom validation logic + var tenantId = context.Principal?.FindFirst("tid")?.Value; + // Implement tenant-specific logic + return Task.CompletedTask; + } + }; + }); +``` + +## Custom Domain Configuration in Azure + +To use custom domains with CIAM, configure your tenant: + +### Step 1: Configure Custom Domain in Azure Portal + +1. Navigate to **Microsoft Entra admin center** +2. Select your CIAM tenant +3. Go to **Custom domains** +4. Add and verify your custom domain (e.g., `login.contoso.com`) +5. Configure DNS CNAME records as instructed + +### Step 2: Update Application Configuration + +```json +{ + "AzureAd": { + "Authority": "https://login.contoso.com/{tenant-id-or-domain}", + "ClientId": "your-client-id", + "CallbackPath": "/signin-oidc" + } +} +``` + +### Step 3: Update Redirect URIs + +In your app registration, add redirect URIs using the custom domain: +- `https://yourapp.com/signin-oidc` +- `https://yourapp.com/signout-callback-oidc` + +## Common CIAM Configuration Mistakes + +### āŒ Mistake 1: Mixing Authority with Instance/TenantId in CIAM + +**Wrong**: +```json +{ + "AzureAd": { + "Authority": "https://contoso.ciamlogin.com/contoso.onmicrosoft.com", + "Instance": "https://contoso.ciamlogin.com/", + "TenantId": "contoso.onmicrosoft.com", + "ClientId": "11111111-1111-1111-1111-111111111111" + } +} +``` + +**Correct**: Use only Authority: +```json +{ + "AzureAd": { + "Authority": "https://contoso.ciamlogin.com/contoso.onmicrosoft.com", + "ClientId": "11111111-1111-1111-1111-111111111111" + } +} +``` + +### āŒ Mistake 2: Using Standard AAD Login URL for CIAM + +**Wrong**: +```json +{ + "AzureAd": { + "Authority": "https://login.microsoftonline.com/contoso.onmicrosoft.com", + "ClientId": "11111111-1111-1111-1111-111111111111" + } +} +``` + +**Correct**: Use the CIAM-specific domain: +```json +{ + "AzureAd": { + "Authority": "https://contoso.ciamlogin.com/contoso.onmicrosoft.com", + "ClientId": "11111111-1111-1111-1111-111111111111" + } +} +``` + +## Migration from B2C to CIAM + +Organizations migrating from Azure AD B2C to CIAM should note key differences: + +### B2C Configuration +```json +{ + "AzureAdB2C": { + "Authority": "https://contoso.b2clogin.com/contoso.onmicrosoft.com/B2C_1_susi", + "ClientId": "11111111-1111-1111-1111-111111111111", + "Domain": "contoso.onmicrosoft.com", + "SignUpSignInPolicyId": "B2C_1_susi" + } +} +``` + +### CIAM Configuration +```json +{ + "AzureAd": { + "Authority": "https://contoso.ciamlogin.com/contoso.onmicrosoft.com", + "ClientId": "22222222-2222-2222-2222-222222222222" + } +} +``` + +**Key Differences**: +- CIAM doesn't use policy IDs in the Authority +- CIAM uses `AzureAd` configuration section (not `AzureAdB2C`) +- The library automatically handles CIAM authorities +- No need for `Domain` property unless specific scenarios require it + +## Environment-Specific Configuration + +### Development Environment + +```json +{ + "AzureAd": { + "Authority": "https://contoso-dev.ciamlogin.com/contoso-dev.onmicrosoft.com", + "ClientId": "dev-client-id", + "CallbackPath": "/signin-oidc" + } +} +``` + +### Production Environment + +```json +{ + "AzureAd": { + "Authority": "https://login.contoso.com/contoso.onmicrosoft.com", + "ClientId": "prod-client-id", + "CallbackPath": "/signin-oidc" + } +} +``` + +Use separate configuration files: +- `appsettings.Development.json` +- `appsettings.Staging.json` +- `appsettings.Production.json` + +## Testing and Validation + +### Verify CIAM Configuration + +1. **Check Authority Format**: Ensure it uses CIAM domain (`.ciamlogin.com` or custom domain) +2. **Avoid Instance/TenantId**: Use Authority only, not mixed with Instance/TenantId +3. **Test Sign-in Flow**: Verify authentication redirects work correctly +4. **Monitor Logs**: Check for EventId 408 warnings indicating configuration conflicts + +### Debugging Tips + +**Issue**: "The reply URL specified does not match" +- **Cause**: Redirect URI mismatch between app configuration and Azure registration +- **Fix**: Ensure CallbackPath matches Azure app registration redirect URIs + +**Issue**: "AADSTS50011: The reply URL does not match" +- **Cause**: Custom domain not properly configured +- **Fix**: Verify custom domain DNS settings and Azure CIAM configuration + +**Issue**: Authority configuration errors +- **Cause**: Mixing Authority with Instance/TenantId +- **Fix**: Use Authority only for CIAM configurations + +## Advanced Scenarios + +### CIAM with API Protection + +Protecting a Web API with CIAM: + +```csharp +// Program.cs for Web API +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(options => + { + builder.Configuration.Bind("AzureAd", options); + }, + options => + { + builder.Configuration.Bind("AzureAd", options); + }); +``` + +### CIAM with Downstream API Calls + +```csharp +builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(options => + { + builder.Configuration.Bind("AzureAd", options); + }) + .EnableTokenAcquisitionToCallDownstreamApi( + builder.Configuration.GetSection("DownstreamApi:Scopes").Get()) + .AddInMemoryTokenCaches(); + +builder.Services.AddDownstreamApi("DownstreamApi", + builder.Configuration.GetSection("DownstreamApi")); +``` + +## Additional Resources + +- [Authority Configuration & Precedence Guide](authority-configuration.md) +- [Azure AD B2C Authority Examples](b2c-authority-examples.md) +- [Migration Guide](migration-authority-vs-instance.md) +- [Microsoft Entra External ID documentation](https://learn.microsoft.com/entra/external-id/) +- [CIAM custom domains](https://learn.microsoft.com/entra/external-id/customers/how-to-custom-domain) diff --git a/docs/faq-authority-precedence.md b/docs/faq-authority-precedence.md new file mode 100644 index 000000000..ec7fbf0e6 --- /dev/null +++ b/docs/faq-authority-precedence.md @@ -0,0 +1,465 @@ +# Authority Precedence FAQ + +This FAQ addresses common questions about authority configuration, precedence rules, and troubleshooting configuration warnings in Microsoft.Identity.Web. + +## General Questions + +### Q: What is the difference between Authority, Instance, and TenantId? + +**A**: These are different ways to specify your authentication endpoint: + +- **Authority**: A complete URL including both the authentication service endpoint and tenant identifier + - Example: `https://login.microsoftonline.com/contoso.onmicrosoft.com` + +- **Instance**: The base URL of the authentication service (without tenant information) + - Example: `https://login.microsoftonline.com/` + +- **TenantId**: The tenant identifier, which can be a GUID, domain name, or special value (`common`, `organizations`, `consumers`) + - Example: `contoso.onmicrosoft.com` + +When you provide Instance and TenantId, they are combined to form an authority: `{Instance}{TenantId}` + +### Q: Which configuration approach should I use: Authority or Instance/TenantId? + +**A**: Both approaches are valid, but recommendations vary by scenario: + +**Use Instance + TenantId when**: +- Configuring Azure AD single-tenant applications (most common) +- You want clear separation between instance and tenant +- You need to easily swap between environments with different tenants +- Following official Microsoft documentation examples + +**Use Authority when**: +- Configuring Azure AD B2C (must include policy path) +- Configuring CIAM (standard or custom domains) +- You prefer a single, complete URL +- Migrating from legacy configurations + +**Never mix both** – choose one approach to avoid configuration conflicts. + +### Q: What happens if I configure both Authority and Instance/TenantId? + +**A**: **Instance and TenantId take precedence**, and Authority is completely ignored. You'll see a warning: + +``` +[Warning] [MsIdWeb] Authority 'https://login.microsoftonline.com/common' is being ignored +because Instance 'https://login.microsoftonline.com/' and/or TenantId 'organizations' +are already configured. +``` + +**EventId**: 408 (AuthorityConflict) + +To fix: Remove either Authority OR both Instance and TenantId from your configuration. + +## Warning Messages + +### Q: Why do I see a warning "Both Authority and Instance/TenantId are set"? + +**A**: You have configured conflicting authority properties. The warning indicates that your `Authority` setting is being ignored because you also specified `Instance` and/or `TenantId`, which take precedence. + +**To resolve**: + +**Option 1** - Remove Authority: +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "organizations", + "ClientId": "your-client-id" + } +} +``` + +**Option 2** - Remove Instance and TenantId: +```json +{ + "AzureAd": { + "Authority": "https://login.microsoftonline.com/organizations", + "ClientId": "your-client-id" + } +} +``` + +### Q: Is the warning critical? Will my application still work? + +**A**: Your application will continue to work, but the warning indicates a configuration inconsistency: + +- āœ… **Authentication works**: Instance and TenantId are used +- āš ļø **Potential confusion**: Authority is ignored, which might not be what you intended +- šŸ“ **Best practice**: Clean up the configuration to remove the warning and improve clarity + +The warning helps you catch potential misconfigurations before they cause issues. + +### Q: How do I suppress the warning without changing my configuration? + +**A**: **You shouldn't suppress the warning** – it indicates a real configuration issue. Instead, fix the configuration by choosing one approach (Authority OR Instance/TenantId). + +If you have a specific reason to keep both (e.g., gradual migration), the warning will remain until the configuration is corrected. + +## B2C-Specific Questions + +### Q: Should I use Authority or Instance/TenantId for B2C? + +**A**: **Always use Authority** for B2C applications. The Authority must include the policy path, which cannot be represented by Instance/TenantId alone. + +**Correct B2C configuration**: +```json +{ + "AzureAdB2C": { + "Authority": "https://contoso.b2clogin.com/contoso.onmicrosoft.com/B2C_1_susi", + "ClientId": "your-client-id", + "Domain": "contoso.onmicrosoft.com", + "SignUpSignInPolicyId": "B2C_1_susi" + } +} +``` + +**Why**: B2C policies are part of the authority path. Using Instance/TenantId would lose the policy information. + +### Q: My B2C configuration has `/tfp/` in the URL. Do I need to remove it? + +**A**: No, you don't need to remove it, but you can for clarity. + +Microsoft.Identity.Web **automatically normalizes** the `/tfp/` segment: +- `https://contoso.b2clogin.com/tfp/contoso.onmicrosoft.com/B2C_1_susi` +- Becomes: `https://contoso.b2clogin.com/contoso.onmicrosoft.com/B2C_1_susi` + +Both formats work identically. The modern format without `/tfp/` is recommended for new configurations. + +### Q: Can I use Instance and TenantId with B2C if I set the policy ID separately? + +**A**: **No, this is not recommended** and won't work correctly. The policy must be part of the Authority URL: + +āŒ **Wrong**: +```json +{ + "AzureAdB2C": { + "Instance": "https://contoso.b2clogin.com/", + "TenantId": "contoso.onmicrosoft.com", + "SignUpSignInPolicyId": "B2C_1_susi", + "ClientId": "your-client-id" + } +} +``` + +āœ… **Correct**: +```json +{ + "AzureAdB2C": { + "Authority": "https://contoso.b2clogin.com/contoso.onmicrosoft.com/B2C_1_susi", + "SignUpSignInPolicyId": "B2C_1_susi", + "ClientId": "your-client-id", + "Domain": "contoso.onmicrosoft.com" + } +} +``` + +## CIAM-Specific Questions + +### Q: How do I configure CIAM applications? + +**A**: For CIAM applications, use the complete Authority URL. The library automatically handles CIAM authorities correctly: + +**Standard CIAM domain**: +```json +{ + "AzureAd": { + "Authority": "https://contoso.ciamlogin.com/contoso.onmicrosoft.com", + "ClientId": "your-client-id" + } +} +``` + +**Custom CIAM domain**: +```json +{ + "AzureAd": { + "Authority": "https://login.contoso.com/contoso.onmicrosoft.com", + "ClientId": "your-client-id" + } +} +``` + +**Important**: Do not mix Authority with Instance/TenantId for CIAM scenarios. Use Authority only. + +### Q: Do I need special configuration for CIAM custom domains? + +**A**: No special configuration is needed beyond ensuring your custom domain is properly configured in your CIAM tenant. Use the complete Authority URL with your custom domain, and the library will handle it automatically: + +```json +{ + "AzureAd": { + "Authority": "https://login.contoso.com/contoso.onmicrosoft.com", + "ClientId": "your-client-id" + } +} +``` + +Make sure your custom domain DNS records are correctly configured in the Azure portal before using it in your application. + +## Multi-Tenant Questions + +### Q: How do I configure a multi-tenant Azure AD application? + +**A**: Use `organizations` or `common` as the tenant identifier: + +**Using Instance/TenantId**: +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "organizations", + "ClientId": "your-client-id" + } +} +``` + +**Using Authority**: +```json +{ + "AzureAd": { + "Authority": "https://login.microsoftonline.com/organizations", + "ClientId": "your-client-id" + } +} +``` + +**Tenant values**: +- `organizations`: Work/school accounts from any Azure AD tenant +- `common`: Both work/school accounts and Microsoft personal accounts +- `consumers`: Microsoft personal accounts only + +### Q: What's the difference between 'common' and 'organizations'? + +**A**: + +| Tenant Value | Work/School Accounts | Personal Microsoft Accounts | Use Case | +|--------------|---------------------|---------------------------|----------| +| `organizations` | āœ… Yes | āŒ No | Business applications (most common) | +| `common` | āœ… Yes | āœ… Yes | Consumer-facing apps accepting both account types | +| `consumers` | āŒ No | āœ… Yes | Personal account-only applications | + +**Recommendation**: Use `organizations` for most multi-tenant business applications. + +## Configuration Edge Cases + +### Q: Does the trailing slash in Instance matter? + +**A**: No, trailing slashes are automatically normalized. These are equivalent: + +```json +"Instance": "https://login.microsoftonline.com/" +"Instance": "https://login.microsoftonline.com" +``` + +Both work identically. The library ensures a trailing slash when needed. + +### Q: Should I include `/v2.0` in my Authority? + +**A**: **No**, Microsoft.Identity.Web uses the v2.0 endpoint by default. Don't append `/v2.0`: + +āŒ **Avoid**: +```json +{ + "AzureAd": { + "Authority": "https://login.microsoftonline.com/common/v2.0" + } +} +``` + +āœ… **Correct**: +```json +{ + "AzureAd": { + "Authority": "https://login.microsoftonline.com/common" + } +} +``` + +### Q: Can I include query parameters in the Authority URL? + +**A**: While technically possible, it's **not recommended**. Use the `ExtraQueryParameters` configuration option instead: + +āŒ **Not Recommended**: +```json +{ + "AzureAd": { + "Authority": "https://login.microsoftonline.com/common?domain_hint=contoso.com" + } +} +``` + +āœ… **Recommended**: +```json +{ + "AzureAd": { + "Authority": "https://login.microsoftonline.com/common", + "ExtraQueryParameters": { + "domain_hint": "contoso.com" + } + } +} +``` + +### Q: What if I forget the `https://` scheme in Authority? + +**A**: You'll likely encounter parsing errors. Always include the full URL with scheme: + +āŒ **Wrong**: +```json +{ + "AzureAd": { + "Authority": "login.microsoftonline.com/common" + } +} +``` + +āœ… **Correct**: +```json +{ + "AzureAd": { + "Authority": "https://login.microsoftonline.com/common" + } +} +``` + +## Government Cloud Questions + +### Q: How do I configure for Azure Government Cloud? + +**A**: Use the government cloud instance URL: + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.us/", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id" + } +} +``` + +Or with Authority: +```json +{ + "AzureAd": { + "Authority": "https://login.microsoftonline.us/your-tenant-id", + "ClientId": "your-client-id" + } +} +``` + +### Q: What are the instance URLs for different Azure clouds? + +**A**: + +| Cloud | Instance URL | +|-------|-------------| +| Azure Public | `https://login.microsoftonline.com/` | +| Azure Government (US) | `https://login.microsoftonline.us/` | +| Azure China | `https://login.chinacloudapi.cn/` | +| Azure Germany (deprecated) | `https://login.microsoftonline.de/` | + +## Troubleshooting + +### Q: Authentication fails after changing my configuration. What should I check? + +**A**: Follow this checklist: + +1. **Check for warnings**: Look for EventId 408 in your logs +2. **Verify redirect URIs**: Ensure they match your app registration in Azure +3. **Confirm authority format**: Validate the URL structure is correct +4. **Test sign-in flow**: Try to authenticate and note any error messages +5. **Enable debug logging**: Set `Microsoft.Identity.Web` log level to `Debug` +6. **Check HTTPS**: Ensure all URLs use `https://` (not `http://`) + +### Q: How do I enable detailed logging to troubleshoot authority configuration? + +**A**: Update your `appsettings.json`: + +```json +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Identity.Web": "Debug", + "Microsoft.Identity.Web.TokenAcquisition": "Trace" + } + } +} +``` + +This provides detailed logs about authority resolution and configuration. + +### Q: My application was working, but now I see the EventId 408 warning. What changed? + +**A**: Possible reasons: + +1. **Configuration file updated**: Someone added conflicting Instance/TenantId or Authority +2. **Library upgrade**: Newer versions of Microsoft.Identity.Web added the warning +3. **Environment-specific override**: Different settings in environment-specific config files +4. **Code configuration**: Authority properties set programmatically in addition to configuration files + +**Solution**: Review your configuration files (including environment-specific ones) and remove conflicting properties. + +### Q: Can I have different authority configurations for development and production? + +**A**: Yes, use environment-specific configuration files: + +**appsettings.Development.json**: +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "common" + } +} +``` + +**appsettings.Production.json**: +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "12345678-1234-1234-1234-123456789012" + } +} +``` + +## Migration Questions + +### Q: I'm upgrading from an older version of Microsoft.Identity.Web. Do I need to change my configuration? + +**A**: Not necessarily, but you might see new warnings: + +- **Existing configurations continue to work** due to backward compatibility +- **New warning (EventId 408)** may appear if you have conflicting settings +- **Recommended**: Review and clean up your configuration to follow current best practices + +See the [Migration Guide](migration-authority-vs-instance.md) for detailed upgrade paths. + +### Q: Should I migrate all my applications at once? + +**A**: **No**, migrate incrementally: + +1. Start with a non-production application +2. Test thoroughly +3. Migrate production applications one at a time +4. Monitor logs and authentication flows after each migration + +## Additional Resources + +- [Authority Configuration & Precedence Guide](authority-configuration.md) +- [Azure AD B2C Authority Examples](b2c-authority-examples.md) +- [CIAM Authority Examples](ciam-authority-examples.md) +- [Migration Guide: Authority vs Instance/TenantId](migration-authority-vs-instance.md) +- [Microsoft identity platform documentation](https://learn.microsoft.com/azure/active-directory/develop/) + +## Still Have Questions? + +If your question isn't answered here: + +1. Check the [main documentation](authority-configuration.md) +2. Review [GitHub Issues](https://github.com/AzureAD/microsoft-identity-web/issues) +3. Ask on [Stack Overflow](https://stackoverflow.com) with tags `microsoft-identity-web` and `azure-ad` +4. Open a [new GitHub issue](https://github.com/AzureAD/microsoft-identity-web/issues/new) with your specific scenario diff --git a/docs/frameworks/aspire.md b/docs/frameworks/aspire.md new file mode 100644 index 000000000..7bf0f267b --- /dev/null +++ b/docs/frameworks/aspire.md @@ -0,0 +1,1586 @@ +# Manually add Entra ID authentication and authorization to an Aspire App + +This guide shows how to secure a **.NET Aspire** distributed application with **Microsoft Entra ID** (Azure AD) authentication and authorization. It covers: + +1. **Blazor Server frontend** (`MyService.Web`): User sign-in with OpenID Connect and token acquisition +2. **Protected API backend** (`MyService.ApiService`): JWT validation using **Microsoft.Identity.Web** +3. **End-to-end flow**: Blazor acquires access tokens and calls the protected API with Aspire service discovery + +It assumes you started with an Aspire project created using the following command: + +```sh +aspire new aspire-starter --name MyService +``` + +--- + +## Table of contents + +- [Prerequisites](#prerequisites) +- [Two-phase implementation workflow](#two-phase-implementation-workflow) +- [App registrations in Entra ID](#app-registrations-in-entra-id) +- [Quick start (TL;DR)](#quick-start-tldr) +- [Files you'll modify](#files-youll-modify) +- [What you'll build & how it works](#what-youll-build--how-it-works) +- [Part 1: Secure the API (Phase 1)](#part-1-secure-the-api-backend-with-microsoftidentityweb) +- [Part 2: Configure Blazor frontend (Phase 1)](#part-2-configure-blazor-frontend-for-authentication) +- [Implementation checklist](#implementation-checklist) +- [Part 3: Testing and troubleshooting](#part-3-testing-and-troubleshooting) +- [Part 4: Common scenarios](#part-4-common-scenarios) +- [Resources](#resources) +- [AI coding assistant skills](#-ai-coding-assistant-skills) + +--- + +## Prerequisites + +- **.NET 9 SDK** or later (or .NET 10+ for latest features) +- **.NET Aspire CLI** - See [Install Aspire CLI](https://aspire.dev/get-started/install-cli/) +- **Azure AD tenant** — See [App Registrations in Entra ID](#app-registrations-in-entra-id) section below for setup + +> šŸ“š **New to Aspire?** See [.NET Aspire Overview](https://learn.microsoft.com/dotnet/aspire/get-started/aspire-overview) + +--- + +## Two-phase implementation workflow + +This guide follows a **two-phase approach**: + +| Phase | What Happens | Result | +|-------|--------------|--------| +| **Phase 1** | Add authentication code with placeholder values | App **builds** but won't **run** | +| **Phase 2** | Provision Entra ID app registrations | App **runs** with real authentication | + +> šŸ’” **AI Assistant Tip:** If you're using GitHub Copilot or another AI assistant, the [entra-id-aspire-authentication](../../.github/skills/entra-id-aspire-authentication/SKILL.md) and [entra-id-aspire-provisioning](../../.github/skills/entra-id-aspire-provisioning/SKILL.md) skills can automate both phases. + +--- + +## App registrations in Entra ID + +
+šŸ“‹ Already have app registrations? Skip to Quick Start or Part 1. + +If you already have app registrations configured, you just need these values for your `appsettings.json`: +- **TenantId** — Your Azure AD tenant ID +- **API ClientId** — Application (client) ID of your API app registration +- **API App ID URI** — Usually `api://` (used in `Audiences` and `Scopes`) +- **Web App ClientId** — Application (client) ID of your web app registration +- **Client Secret** (or certificate) — Credential for the web app (store in user-secrets, not appsettings.json) +- **Scopes** — The scope(s) your web app requests, e.g., `api:///.default` or `api:///access_as_user` + +
+ +Before your app can authenticate users, you need **two app registrations** in Microsoft Entra ID: + +| App Registration | Purpose | Key Configuration | +|------------------|---------|-------------------| +| **API** (`MyService.ApiService`) | Validates incoming tokens | App ID URI, `access_as_user` scope | +| **Web App** (`MyService.Web`) | Signs in users, acquires tokens | Redirect URIs, client secret, API permissions | + +### Option A: Azure Portal (manual) + +
+šŸ“‹ Step 1: Register the API + +1. Go to [Microsoft Entra admin center](https://entra.microsoft.com) > **Identity** > **Applications** > **App registrations** +2. Click **New registration** + - **Name:** `MyService.ApiService` + - **Supported account types:** Accounts in this organizational directory only (Single tenant) + - Click **Register** +3. **Expose an API:** + - Go to **Expose an API** > **Add** next to Application ID URI + - Accept the default (`api://`) or customize it + - Click **Add a scope**: + - **Scope name:** `access_as_user` + - **Who can consent:** Admins and users + - **Admin consent display name:** Access MyService API + - **Admin consent description:** Allows the app to access MyService API on behalf of the signed-in user. + - Click **Add scope** +4. Copy the **Application (client) ID** — you'll need this for both `appsettings.json` files + +šŸ“š [Quickstart: Configure an app to expose a web API](https://learn.microsoft.com/entra/identity-platform/quickstart-configure-app-expose-web-apis) + +
+ +
+šŸ“‹ Step 2: Register the Web App + +1. Go to **App registrations** > **New registration** + - **Name:** `MyService.Web` + - **Supported account types:** Accounts in this organizational directory only + - **Redirect URI:** Select **Web** and enter your app's URL + `/signin-oidc` + - For local development: `https://localhost:7001/signin-oidc` (check your `launchSettings.json` for the actual port) + - Click **Register** +2. **Configure redirect URIs:** + - Go to **Authentication** > **Add URI** to add all your dev URLs (from `launchSettings.json`) +3. **Create a client secret:** + - Go to **Certificates & secrets** > **Client secrets** > **New client secret** + - Add a description and expiration + - **Copy the secret value immediately** — it won't be shown again! + + > **Note:** Some organizations don't allow client secrets. Alternatives: + > - **Certificates** — See [Certificate credentials](https://learn.microsoft.com/entra/identity-platform/certificate-credentials) and [Microsoft.Identity.Web certificate support](../authentication/credentials/credentials-README.md#certificates) + > - **Federated Identity Credentials (FIC) + Managed Identity** — See [Workload identity federation](https://learn.microsoft.com/entra/workload-id/workload-identity-federation) and [Certificateless authentication](../authentication/credentials/certificateless.md) + +4. **Add API permission:** + - Go to **API permissions** > **Add a permission** > **My APIs** + - Select `MyService.ApiService` + - Check `access_as_user` > **Add permissions** + - Click **Grant admin consent for [tenant]** (or users will be prompted) +5. Copy the **Application (client) ID** for the web app's `appsettings.json` + +šŸ“š [Quickstart: Register an application](https://learn.microsoft.com/entra/identity-platform/quickstart-register-app) +šŸ“š [Add credentials to your app](https://learn.microsoft.com/entra/identity-platform/quickstart-register-app#add-credentials) +šŸ“š [Add a redirect URI](https://learn.microsoft.com/entra/identity-platform/quickstart-register-app#add-a-redirect-uri) + +
+ +
+šŸ“‹ Step 3: Update Configuration + +After creating the app registrations, update your `appsettings.json` files: + +**API (`MyService.ApiService/appsettings.json`):** +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "YOUR_TENANT_ID", + "ClientId": "YOUR_API_CLIENT_ID", + "Audiences": ["api://YOUR_API_CLIENT_ID"] + } +} +``` + +**Web App (`MyService.Web/appsettings.json`):** +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "YOUR_TENANT_ID", + "ClientId": "YOUR_WEB_CLIENT_ID", + "CallbackPath": "/signin-oidc", + "ClientCredentials": [ + { "SourceType": "ClientSecret" } + ] + }, + "WeatherApi": { + "Scopes": ["api://YOUR_API_CLIENT_ID/.default"] + } +} +``` + +**Store the secret securely:** +```powershell +cd MyService.Web +dotnet user-secrets set "AzureAd:ClientCredentials:0:ClientSecret" "YOUR_SECRET_VALUE" +``` + +| Value | Where to Find | +|-------|---------------| +| `TenantId` | Azure Portal > Entra ID > Overview > Tenant ID | +| `API ClientId` | App registrations > MyService.ApiService > Application (client) ID | +| `Web ClientId` | App registrations > MyService.Web > Application (client) ID | +| `Client Secret` | Created in Step 2 (copy immediately when created) | + +
+ +### Option B: Automated with PowerShell + +For automated provisioning using Microsoft Graph PowerShell, use the **entra-id-aspire-provisioning** skill: + +```powershell +# Prerequisites (one-time) +Install-Module Microsoft.Graph.Applications -Scope CurrentUser + +# Connect to your tenant +Connect-MgGraph -Scopes "Application.ReadWrite.All" +``` + +Then ask your AI assistant: +> "Using the entra-id-aspire-provisioning skill, create app registrations for my Aspire solution" + +šŸ“š [Microsoft Graph PowerShell SDK](https://learn.microsoft.com/powershell/microsoftgraph/installation) +šŸ“ [Provisioning Skill](../../.github/skills/entra-id-aspire-provisioning/SKILL.md) + +--- + +> **Note:** The Aspire starter template automatically creates a `WeatherApiClient` class in the `MyService.Web` project. This "typed HttpClient" is used throughout this guide to demonstrate calling the protected API. You don't need to create this class yourself—it's part of the template. + +## Quick start (TL;DR) + +
+Click to expand the 5-minute version + +### API (`MyService.ApiService`) + +```powershell +dotnet add package Microsoft.Identity.Web +``` + +**appsettings.json:** +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "", + "ClientId": "", + "Audiences": ["api://"] + } +} +``` + +**Program.cs:** +```csharp +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")); +builder.Services.AddAuthorization(); +// ... +app.UseAuthentication(); +app.UseAuthorization(); +// ... +app.MapGet("/weatherforecast", () => { /* ... */ }).RequireAuthorization(); +``` + +### Web App (`MyService.Web`) + +```powershell +dotnet add package Microsoft.Identity.Web +``` + +**appsettings.json:** +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "", + "ClientId": "", + "CallbackPath": "/signin-oidc", + "ClientCredentials": [{ "SourceType": "ClientSecret", "ClientSecret": "" }] + }, + "WeatherApi": { "Scopes": ["api:///.default"] } +} +``` + +**Program.cs:** +```csharp +builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd")) + .EnableTokenAcquisitionToCallDownstreamApi() + .AddInMemoryTokenCaches(); + +builder.Services.AddCascadingAuthenticationState(); +builder.Services.AddScoped(); + +builder.Services.AddHttpClient(client => + client.BaseAddress = new("https+http://apiservice")) + .AddMicrosoftIdentityMessageHandler(builder.Configuration.GetSection("WeatherApi")); +// ... +app.UseAuthentication(); +app.UseAuthorization(); +app.MapGroup("/authentication").MapLoginAndLogout(); +``` + +> āš ļø **Don't forget:** Copy the helper files (`BlazorAuthenticationChallengeHandler.cs`, `LoginLogoutEndpointRouteBuilderExtensions.cs`) and create `UserInfo.razor`. See [Part 2](#part-2-configure-blazor-frontend-for-authentication) for details. + +**That's it!** The `MicrosoftIdentityMessageHandler` automatically acquires and attaches tokens, and `BlazorAuthenticationChallengeHandler` handles consent/Conditional Access challenges. + +
+ +--- + +## Files you'll modify + +| Project | File | Changes | +|---------|------|---------| +| **ApiService** | `Program.cs` | JWT Bearer auth, authorization middleware | +| | `appsettings.json` | Azure AD configuration | +| | `.csproj` | Add `Microsoft.Identity.Web` | +| **Web** | `Program.cs` | OIDC auth, token acquisition, BlazorAuthenticationChallengeHandler | +| | `appsettings.json` | Azure AD config, downstream API scopes | +| | `.csproj` | Add `Microsoft.Identity.Web` | +| | `LoginLogoutEndpointRouteBuilderExtensions.cs` | Login/logout with incremental consent *(copy from skill)* | +| | `BlazorAuthenticationChallengeHandler.cs` | Auth challenge handler *(copy from skill)* | +| | `Components/UserInfo.razor` | **Login button UI** *(new file)* | +| | `Components/Layout/MainLayout.razor` | Include UserInfo component | +| | `Components/Routes.razor` | AuthorizeRouteView for protected pages | +| | Pages calling APIs | Try/catch with ChallengeHandler | + +--- + +## What you'll build & how it works + +```mermaid +flowchart LR + A[User Browser] -->|1 Login OIDC| B[Blazor Server
MyService.Web] + B -->|2 Redirect| C[Entra ID] + C -->|3 auth code| B + B -->|4 exchange auth code| C + C -->|5 tokens| B + B -->|6 cookie + session| A + B -->|7 HTTP + Bearer token| D[ASP.NET API
MyService.ApiService
Microsoft.Identity.Web] + D -->|8 Validate JWT| C + D -->|9 Weather data| B +``` + +**Key Technologies:** +- **Microsoft.Identity.Web** (Blazor & API): OIDC authentication, JWT validation, token acquisition +- **.NET Aspire**: Service discovery (`https+http://apiservice`), orchestration, health checks + +### How the authentication flow works + +1. **User visits Blazor app** → Not authenticated → sees "Login" button +2. **User clicks Login** → Redirects to `/authentication/login` → OIDC challenge → Entra ID +3. **User signs in** → Entra ID redirects to `/signin-oidc` → cookie established +4. **User navigates to Weather page** → Blazor calls `WeatherApiClient.GetAsync()` +5. **`MicrosoftIdentityMessageHandler`** intercepts the request: + - Checks token cache for valid access token + - If expired/missing, silently acquires new token using refresh token + - Attaches `Authorization: Bearer ` header + - Automatically handles WWW-Authenticate challenges for Conditional Access +6. **API receives request** → Microsoft.Identity.Web validates JWT → returns data +7. **Blazor renders weather data** + +
+šŸ” Aspire Service Discovery Details + +In `Program.cs`, the HttpClient uses: + +```csharp +client.BaseAddress = new("https+http://apiservice"); +``` + +**At runtime:** +- Aspire resolves `"apiservice"` to the actual endpoint (e.g., `https://localhost:7123`) +- No hardcoded URLs needed +- Works in local dev, Docker, Kubernetes, Azure Container Apps + +šŸ“š [Aspire Service Discovery](https://learn.microsoft.com/dotnet/aspire/service-discovery/overview) + +
+ +--- + +## Solution structure + +``` +MyService/ +ā”œā”€ā”€ MyService.AppHost/ # Aspire orchestration +ā”œā”€ā”€ MyService.ApiService/ # Protected API (Microsoft.Identity.Web) +ā”œā”€ā”€ MyService.Web/ # Blazor Server (Microsoft.Identity.Web) +ā”œā”€ā”€ MyService.ServiceDefaults/ # Shared defaults +└── MyService.Tests/ # Tests +``` + +--- + +## Part 1: Secure the API backend with Microsoft.Identity.Web + +> šŸ“ **You are in Phase 1** — Parts 1 and 2 add the authentication code. After completing both, proceed to [App Registrations](#app-registrations-in-entra-id) if you haven't already. + +**Microsoft.Identity.Web** provides streamlined JWT Bearer authentication for ASP.NET Core APIs with minimal configuration. + +šŸ“š **Learn more:** [Microsoft.Identity.Web Documentation](https://github.com/AzureAD/microsoft-identity-web/tree/master/docs) + +### 1.1: Add Microsoft.Identity.Web package + +```powershell +cd MyService.ApiService +dotnet add package Microsoft.Identity.Web +``` + +
+šŸ“„ View updated csproj + +```xml + + + + +``` + +
+ +### 1.2: Configure Azure AD settings + +Add Azure AD configuration to `MyService.ApiService/appsettings.json`: + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "", + "ClientId": "", + "Audiences": [ + "api://" + ] + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} +``` + +
+šŸ”‘ Key Properties Explained + +- **`ClientId`**: Entra ID API app registration ID +- **`TenantId`**: Your Azure AD tenant ID, or `"organizations"` for multi-tenant, or `"common"` for any Microsoft account +- **`Audiences`**: Valid token audiences (typically your App ID URI) + +
+ +### 1.3: Update `MyService.ApiService/Program.cs` + +```csharp +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Identity.Web; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); + +// Add Microsoft.Identity.Web JWT Bearer authentication +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")); + +builder.Services.AddProblemDetails(); +builder.Services.AddOpenApi(); + +// Authorization +builder.Services.AddAuthorization(); + +var app = builder.Build(); + +app.UseExceptionHandler(); + +app.UseAuthentication(); +app.UseAuthorization(); + +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +string[] summaries = ["Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"]; + +app.MapGet("/", () => "API service is running. Navigate to /weatherforecast to see sample data."); + +app.MapGet("/weatherforecast", () => +{ + var forecast = Enumerable.Range(1, 5).Select(index => + new WeatherForecast + ( + DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + Random.Shared.Next(-20, 55), + summaries[Random.Shared.Next(summaries.Length)] + )) + .ToArray(); + return forecast; +}) +.WithName("GetWeatherForecast") +.RequireAuthorization(); + +app.MapDefaultEndpoints(); +app.Run(); + +record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) +{ + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); +} +``` + +
+šŸ”‘ Key Changes Explained + +- Register JWT Bearer authentication with `AddMicrosoftIdentityWebApi` +- Add `app.UseAuthentication()` and `app.UseAuthorization()` middleware +- Apply `.RequireAuthorization()` to protected endpoints + +
+ +### 1.4: Test the protected API + +
+🧪 Test with curl + +Without a token: + +```powershell +curl https://localhost:/weatherforecast +# Expected: 401 Unauthorized +``` + +With a valid token: + +```powershell +curl -H "Authorization: Bearer " https://localhost:/weatherforecast +# Expected: 200 OK with weather data +``` + +
+ +--- + +## Part 2: Configure Blazor frontend for authentication + +> šŸ“ **Still in Phase 1** — This part completes the code implementation. You'll need the helper files from the skill folder. + +The Blazor Server app uses **Microsoft.Identity.Web** to: +- Sign users in with OIDC +- Acquire access tokens to call the API +- Attach tokens to outgoing HTTP requests + +### 2.1: Add Microsoft.Identity.Web package + +```powershell +cd MyService.Web +dotnet add package Microsoft.Identity.Web +``` + +
+šŸ“„ View updated csproj + +```xml + + + +``` + +
+ +### 2.2: Configure Azure AD settings + +Add Azure AD configuration and downstream API scopes to `MyService.Web/appsettings.json`: + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "Domain": ".onmicrosoft.com", + "TenantId": "", + "ClientId": "", + "CallbackPath": "/signin-oidc", + "ClientCredentials": [ + { + "SourceType": "ClientSecret", + "ClientSecret": "" + } + ] + }, + "WeatherApi": { + "Scopes": [ "api:///.default" ] + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} +``` + +
+šŸ”‘ Configuration Details + +- **`ClientId`**: Web app registration ID (not the API ID) +- **`ClientCredentials`**: Credentials for the web app to acquire tokens. Supports multiple credential types including certificates, Key Vault, managed identity, and client secrets. See [Credentials documentation](../authentication/credentials/credentials-README.md) for production-ready options. +- **`Scopes`**: Must match the API's App ID URI with `/.default` suffix +- Replace `` with the **API** app registration client ID + +
+ +> āš ļø **Security Note:** For production, use certificates or managed identity instead of client secrets. See [Certificateless authentication](../authentication/credentials/certificateless.md) for the recommended approach. + +### 2.3: Update `MyService.Web/Program.cs` + +```csharp +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Web; +using MyService.Web; +using MyService.Web.Components; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); + +// 1) Authentication + Microsoft Identity Web +builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd")) + .EnableTokenAcquisitionToCallDownstreamApi() + .AddInMemoryTokenCaches(); + +builder.Services.Configure(OpenIdConnectDefaults.AuthenticationScheme, options => +{ + options.Prompt = "login"; // Optional: force fresh sign-in each time +}); + +builder.Services.AddCascadingAuthenticationState(); + +// 2) Blazor components +builder.Services.AddRazorComponents().AddInteractiveServerComponents(); + +// 3) Blazor authentication challenge handler for incremental consent & Conditional Access +builder.Services.AddScoped(); + +builder.Services.AddOutputCache(); + +// 4) Downstream API client with MicrosoftIdentityMessageHandler +builder.Services.AddHttpClient(client => +{ + // Aspire service discovery: resolves "apiservice" at runtime + client.BaseAddress = new("https+http://apiservice"); +}) +.AddMicrosoftIdentityMessageHandler(builder.Configuration.GetSection("WeatherApi")); + +var app = builder.Build(); + +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler("/Error", createScopeForErrors: true); + app.UseHsts(); +} + +app.UseHttpsRedirection(); +app.UseAuthentication(); +app.UseAuthorization(); +app.UseAntiforgery(); +app.UseOutputCache(); + +app.MapStaticAssets(); +app.MapRazorComponents() + .AddInteractiveServerRenderMode(); + +// 5) Login/Logout endpoints with incremental consent support +app.MapGroup("/authentication").MapLoginAndLogout(); + +app.MapDefaultEndpoints(); +app.Run(); +``` + +
+šŸ”‘ Key Points Explained + +- **`AddMicrosoftIdentityWebApp`**: Configures OIDC authentication +- **`EnableTokenAcquisitionToCallDownstreamApi`**: Enables token acquisition for downstream APIs +- **`AddScoped`**: Handles incremental consent and Conditional Access in Blazor Server +- **`AddMicrosoftIdentityMessageHandler`**: Attaches bearer tokens to HttpClient requests automatically +- **`https+http://apiservice`**: Aspire service discovery resolves this to the actual API URL +- **Middleware order**: `UseAuthentication()` → `UseAuthorization()` → endpoints + +šŸ“š **Deep dive:** [MicrosoftIdentityMessageHandler documentation](../calling-downstream-apis/custom-apis.md#microsoftidentitymessagehandler---for-httpclient-integration) + +
+ +
+āš™ļø Alternative Configuration Approaches + +#### Alternative configuration approaches + +The `AddMicrosoftIdentityMessageHandler` extension supports multiple configuration patterns: + +**Option 1: Configuration from appsettings.json (shown above)** +```csharp +.AddMicrosoftIdentityMessageHandler(builder.Configuration.GetSection("WeatherApi")); +``` + +**Option 2: Inline configuration with Action delegate** +```csharp +.AddMicrosoftIdentityMessageHandler(options => +{ + options.Scopes.Add("api:///.default"); +}); +``` + +**Option 3: Per-request configuration (parameterless)** +```csharp +.AddMicrosoftIdentityMessageHandler(); + +// Then in your service, configure per-request: +var request = new HttpRequestMessage(HttpMethod.Get, "/weatherforecast") + .WithAuthenticationOptions(options => + { + options.Scopes.Add("api:///.default"); + }); +var response = await _httpClient.SendAsync(request); +``` + +**Option 4: Pre-configured options object** +```csharp +var options = new MicrosoftIdentityMessageHandlerOptions +{ + Scopes = { "api:///.default" } +}; +builder.Services.AddHttpClient(client => +{ + client.BaseAddress = new("https+http://apiservice"); +}) +.AddMicrosoftIdentityMessageHandler(options); +``` + +
+ +### 2.4: Copy helper files from skill folder + +The authentication implementation requires two helper files. **Copy these from the skill folder** rather than creating them manually: + +```powershell +# From your solution root, copy the helper files +$skillPath = ".github/skills/entra-id-aspire-authentication" +Copy-Item "$skillPath/LoginLogoutEndpointRouteBuilderExtensions.cs" "MyService.Web/" +Copy-Item "$skillPath/BlazorAuthenticationChallengeHandler.cs" "MyService.Web/" +``` + +> šŸ’” **Tip:** These files are in the `Microsoft.Identity.Web` namespace, so they're available once you reference the package. Eventually they will +> ship in the Microsoft.Identity.Web NuGet packge. + +
+šŸ“„ View LoginLogoutEndpointRouteBuilderExtensions.cs + +This enhanced version supports **incremental consent** and **Conditional Access** via query parameters: + +```csharp +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.Identity.Web; + +/// +/// Extension methods for mapping login and logout endpoints that support +/// incremental consent and Conditional Access scenarios. +/// +public static class LoginLogoutEndpointRouteBuilderExtensions +{ + /// + /// Maps login and logout endpoints under the current route group. + /// The login endpoint supports incremental consent via scope, loginHint, domainHint, and claims parameters. + /// + public static IEndpointConventionBuilder MapLoginAndLogout(this IEndpointRouteBuilder endpoints) + { + var group = endpoints.MapGroup(""); + + // Enhanced login endpoint that supports incremental consent and Conditional Access + group.MapGet("/login", ( + string? returnUrl, + string? scope, + string? loginHint, + string? domainHint, + string? claims) => + { + var properties = GetAuthProperties(returnUrl); + + // Add scopes if provided (for incremental consent) + if (!string.IsNullOrEmpty(scope)) + { + var scopes = scope.Split(' ', StringSplitOptions.RemoveEmptyEntries); + properties.SetParameter(OpenIdConnectParameterNames.Scope, scopes); + } + + // Add login hint (pre-fills username) + if (!string.IsNullOrEmpty(loginHint)) + { + properties.SetParameter(OpenIdConnectParameterNames.LoginHint, loginHint); + } + + // Add domain hint (skips home realm discovery) + if (!string.IsNullOrEmpty(domainHint)) + { + properties.SetParameter(OpenIdConnectParameterNames.DomainHint, domainHint); + } + + // Add claims challenge (for Conditional Access / step-up auth) + if (!string.IsNullOrEmpty(claims)) + { + properties.Items["claims"] = claims; + } + + return TypedResults.Challenge(properties, [OpenIdConnectDefaults.AuthenticationScheme]); + }) + .AllowAnonymous(); + + group.MapPost("/logout", async (HttpContext context) => + { + string? returnUrl = null; + if (context.Request.HasFormContentType) + { + var form = await context.Request.ReadFormAsync(); + returnUrl = form["ReturnUrl"]; + } + + return TypedResults.SignOut(GetAuthProperties(returnUrl), + [CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectDefaults.AuthenticationScheme]); + }) + .DisableAntiforgery(); + + return group; + } + + private static AuthenticationProperties GetAuthProperties(string? returnUrl) + { + const string pathBase = "/"; + if (string.IsNullOrEmpty(returnUrl)) returnUrl = pathBase; + else if (returnUrl.StartsWith("//", StringComparison.Ordinal)) returnUrl = pathBase; + else if (!Uri.IsWellFormedUriString(returnUrl, UriKind.Relative)) returnUrl = new Uri(returnUrl, UriKind.Absolute).PathAndQuery; + else if (returnUrl[0] != '/') returnUrl = $"{pathBase}{returnUrl}"; + return new AuthenticationProperties { RedirectUri = returnUrl }; + } +} +``` + +**Key features:** +- `scope`: Request additional scopes (incremental consent) +- `loginHint`: Pre-fill username field +- `domainHint`: Skip home realm discovery (`organizations` or `consumers`) +- `claims`: Pass Conditional Access claims challenge + +
+ +
+šŸ“„ View BlazorAuthenticationChallengeHandler.cs + +This handler manages authentication challenges in Blazor Server components: + +```csharp +using System.Security.Claims; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Authorization; + +namespace Microsoft.Identity.Web; + +/// +/// Handles authentication challenges for Blazor Server components. +/// Provides functionality for incremental consent and Conditional Access scenarios. +/// +public class BlazorAuthenticationChallengeHandler( + NavigationManager navigation, + AuthenticationStateProvider authenticationStateProvider, + IConfiguration configuration) +{ + private const string MsaTenantId = "9188040d-6c67-4c5b-b112-36a304b66dad"; + + /// + /// Gets the current user's authentication state. + /// + public async Task GetUserAsync() + { + var authState = await authenticationStateProvider.GetAuthenticationStateAsync(); + return authState.User; + } + + /// + /// Checks if the current user is authenticated. + /// + public async Task IsAuthenticatedAsync() + { + var user = await GetUserAsync(); + return user.Identity?.IsAuthenticated == true; + } + + /// + /// Handles exceptions that may require user re-authentication. + /// Returns true if a challenge was initiated, false otherwise. + /// + public async Task HandleExceptionAsync(Exception exception) + { + var challengeException = exception as MicrosoftIdentityWebChallengeUserException + ?? exception.InnerException as MicrosoftIdentityWebChallengeUserException; + + if (challengeException != null) + { + var user = await GetUserAsync(); + ChallengeUser(user, challengeException.Scopes, challengeException.MsalUiRequiredException?.Claims); + return true; + } + + return false; + } + + /// + /// Initiates a challenge to authenticate the user or request additional consent. + /// + public void ChallengeUser(ClaimsPrincipal user, string[]? scopes = null, string? claims = null) + { + var currentUri = navigation.Uri; + + // Build scopes string (add OIDC scopes) + var allScopes = (scopes ?? []) + .Union(["openid", "offline_access", "profile"]) + .Distinct(); + var scopeString = Uri.EscapeDataString(string.Join(" ", allScopes)); + + // Get login hint from user claims + var loginHint = Uri.EscapeDataString(GetLoginHint(user)); + + // Get domain hint + var domainHint = Uri.EscapeDataString(GetDomainHint(user)); + + // Build the challenge URL + var challengeUrl = $"/authentication/login?returnUrl={Uri.EscapeDataString(currentUri)}" + + $"&scope={scopeString}" + + $"&loginHint={loginHint}" + + $"&domainHint={domainHint}"; + + // Add claims if present (for Conditional Access) + if (!string.IsNullOrEmpty(claims)) + { + challengeUrl += $"&claims={Uri.EscapeDataString(claims)}"; + } + + navigation.NavigateTo(challengeUrl, forceLoad: true); + } + + /// + /// Initiates a challenge with scopes from configuration. + /// + public async Task ChallengeUserWithConfiguredScopesAsync(string configurationSection) + { + var user = await GetUserAsync(); + var scopes = configuration.GetSection(configurationSection).Get(); + ChallengeUser(user, scopes); + } + + private static string GetLoginHint(ClaimsPrincipal user) + { + return user.FindFirst("preferred_username")?.Value ?? + user.FindFirst("login_hint")?.Value ?? + string.Empty; + } + + private static string GetDomainHint(ClaimsPrincipal user) + { + var tenantId = user.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid")?.Value ?? + user.FindFirst("tid")?.Value; + + if (string.IsNullOrEmpty(tenantId)) + return "organizations"; + + // MSA tenant + if (tenantId == MsaTenantId) + return "consumers"; + + return "organizations"; + } +} +``` + +**Key methods:** +- `IsAuthenticatedAsync()`: Check if user is signed in before API calls +- `HandleExceptionAsync(ex)`: Catches `MicrosoftIdentityWebChallengeUserException` and redirects for re-auth +- `ChallengeUser()`: Manually trigger authentication with specific scopes/claims +- `ChallengeUserWithConfiguredScopesAsync()`: Challenge with scopes from config section + +
+ +### 2.5: Add Blazor UI components + +> āš ļø **CRITICAL: This step is frequently forgotten!** Without the UserInfo component, users have **no way to log in**. + +
+šŸ“„ Create UserInfo.razor component (THE LOGIN BUTTON) + +**Create `MyService.Web/Components/UserInfo.razor`:** + +```razor +@using Microsoft.AspNetCore.Components.Authorization + + + + Hello, @context.User.Identity?.Name + + + Login + + +``` + +**Key Features:** +- ``: Renders different UI based on auth state +- Shows username when authenticated +- Login link redirects to OIDC flow +- Logout form posts to sign-out endpoint + +
+ +**Add to Layout:** Include `` in your `MainLayout.razor`: + +```razor +@inherits LayoutComponentBase + +
+ + +
+
+ @* <-- THE LOGIN BUTTON *@ +
+ +
+ @Body +
+
+
+``` + +### 2.6: Update Routes.razor for AuthorizeRouteView + +Replace `RouteView` with `AuthorizeRouteView` in `Components/Routes.razor`: + +```razor +@using Microsoft.AspNetCore.Components.Authorization + + + + + +

You are not authorized to view this page.

+ Login +
+
+ +
+
+``` + +### 2.7: Handle exceptions on pages calling APIs + +> āš ļø **This is NOT optional** — Blazor Server requires explicit exception handling for Conditional Access and consent. + +When calling APIs, Conditional Access policies or consent requirements can trigger `MicrosoftIdentityWebChallengeUserException`. You **MUST** handle this on **every page that calls a downstream API** unless your app is pre-authrorized and you have requested all the scopes ahead of time (in the Program.cs), which is possible if you call only one downstream API. + +**Example: Weather.razor with proper exception handling:** + +```razor +@page "/weather" +@attribute [Authorize] + +@using Microsoft.AspNetCore.Authorization +@using Microsoft.Identity.Web + +@inject WeatherApiClient WeatherApi +@inject BlazorAuthenticationChallengeHandler ChallengeHandler + +Weather + +

Weather

+ +@if (!string.IsNullOrEmpty(errorMessage)) +{ +
@errorMessage
+} +else if (forecasts == null) +{ +

Loading...

+} +else +{ + + + + + + + + + + @foreach (var forecast in forecasts) + { + + + + + + } + +
DateTemp. (C)Summary
@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.Summary
+} + +@code { + private WeatherForecast[]? forecasts; + private string? errorMessage; + + protected override async Task OnInitializedAsync() + { + // Check authentication before making API calls + if (!await ChallengeHandler.IsAuthenticatedAsync()) + { + // Not authenticated - redirect to login with required scopes + await ChallengeHandler.ChallengeUserWithConfiguredScopesAsync("WeatherApi:Scopes"); + return; + } + + try + { + forecasts = await WeatherApi.GetWeatherAsync(); + } + catch (Exception ex) + { + // Handle incremental consent / Conditional Access + if (!await ChallengeHandler.HandleExceptionAsync(ex)) + { + // Not a challenge exception - show error to user + errorMessage = $"Error loading weather data: {ex.Message}"; + } + // If HandleExceptionAsync returned true, user is being redirected + } + } +} +``` + +
+🧠 Why this pattern? + +1. **`IsAuthenticatedAsync()`** checks if user is signed in before making API calls +2. **`HandleExceptionAsync()`** catches `MicrosoftIdentityWebChallengeUserException` (or as InnerException) +3. If it **is** a challenge exception → redirects user to re-authenticate with required claims/scopes +4. If it is **NOT** a challenge exception → returns false so you can handle the error + +**Why isn't this automatic?** Blazor Server's circuit-based architecture requires explicit handling. The handler re-challenges the user by navigating to the login endpoint with the required claims/scopes. + +
+ +### 2.8: Store client secret in user secrets + +> āš ļø **Never commit secrets to source control!** + +```powershell +cd MyService.Web +dotnet user-secrets init +dotnet user-secrets set "AzureAd:ClientCredentials:0:ClientSecret" "" +``` + +Then update `appsettings.json` to reference user secrets (remove the hardcoded secret): + +```jsonc +{ + "AzureAd": { + "ClientCredentials": [ + { + // Secret is stored in user-secrets, not here + // For more options see https://aka.ms/ms-id-web/credentials + "SourceType": "ClientSecret" + } + ] + } +} +``` + +Alternatively, Microsoft.Identity.Web offers all kind of client credentials. See [Client credentials](../authentication/credentials/credentials-README.md) + +--- + +## Implementation checklist + +Use this checklist to verify all steps are complete: + +### API project +- [ ] Added `Microsoft.Identity.Web` package +- [ ] Updated `appsettings.json` with `AzureAd` section +- [ ] Updated `Program.cs` with `AddMicrosoftIdentityWebApi` +- [ ] Added `.RequireAuthorization()` to protected endpoints + +### Web/Blazor project +- [ ] Added `Microsoft.Identity.Web` package +- [ ] Updated `appsettings.json` with `AzureAd` and `WeatherApi` sections +- [ ] Updated `Program.cs` with OIDC, token acquisition +- [ ] Added `AddScoped()` +- [ ] Copied `LoginLogoutEndpointRouteBuilderExtensions.cs` from skill folder +- [ ] Copied `BlazorAuthenticationChallengeHandler.cs` from skill folder +- [ ] Created `Components/UserInfo.razor` (**THE LOGIN BUTTON**) +- [ ] Updated `MainLayout.razor` to include `` +- [ ] Updated `Routes.razor` with `AuthorizeRouteView` +- [ ] Added try/catch with `ChallengeHandler` on **every page calling APIs** +- [ ] Stored client secret in user-secrets + +### Verification +- [ ] `dotnet build` succeeds +- [ ] App registrations created (via provisioning skill or Azure Portal) +- [ ] `appsettings.json` has real GUIDs (no placeholders) + +--- + +## Part 3: Testing and troubleshooting + +### 3.1: Run the application + +```powershell +# From solution root +dotnet restore +dotnet build + +# Launch AppHost (starts both Web and API) +dotnet run --project .\MyService.AppHost\MyService.AppHost.csproj +``` + +### 3.2: Test flow + +1. Open browser → Blazor Web UI (check Aspire dashboard for URL) +2. Click **Login** → Sign in with Azure AD +3. Navigate to **Weather** page +4. Verify weather data loads (from protected API) + +### 3.3: Common issues + +| Issue | Solution | +|-------|----------| +| **401 on API calls** | Verify scopes in `appsettings.json` match the API's App ID URI | +| **OIDC redirect fails** | Add `/signin-oidc` to Azure AD redirect URIs | +| **Token not attached** | Ensure `AddMicrosoftIdentityMessageHandler` is called on the `HttpClient` | +| **Service discovery fails** | Check `AppHost.cs` references both projects and they're running | +| **AADSTS65001** | Admin consent required - grant consent in Azure Portal | +| **CORS errors** | Add CORS policy in API `Program.cs` if needed | +| **No login button** | Ensure `UserInfo.razor` exists and is included in `MainLayout.razor` | +| **404 on `/MicrosoftIdentity/Account/Challenge`** | Use `BlazorAuthenticationChallengeHandler` instead of old `MicrosoftIdentityConsentHandler` | +| **Consent loop** | Ensure try/catch with `HandleExceptionAsync` is on all API-calling pages | + +### 3.4: Enable MSAL logging + +
+šŸ” Debug authentication with MSAL logs + +When troubleshooting authentication issues, enable detailed MSAL (Microsoft Authentication Library) logging to see token acquisition details: + +**In `Program.cs` (both Web and API):** + +```csharp +// Add detailed logging for Microsoft.Identity +builder.Logging.AddFilter("Microsoft.Identity", LogLevel.Debug); +builder.Logging.AddFilter("Microsoft.IdentityModel", LogLevel.Debug); +``` + +**Or in `appsettings.json`:** + +```json +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.Identity": "Debug", + "Microsoft.IdentityModel": "Debug" + } + } +} +``` + +**What the logs show:** +- Token cache hits/misses +- Token acquisition attempts (silent vs interactive) +- Token expiration and refresh +- Claims in the token +- Errors with detailed AADSTS codes + +**Example log output:** +``` +dbug: Microsoft.Identity.Web.TokenAcquisition[0] + AcquireTokenSilent returned a token from the cache +dbug: Microsoft.Identity.Web.MicrosoftIdentityMessageHandler[0] + Attaching token to request: https://localhost:7123/weatherforecast +``` + +> āš ļø **Security Note:** Disable debug logging in production as it may be very verbose. + +
+ +### 3.5: Inspect tokens + +
+šŸŽ« Decode and verify JWT tokens + +To debug token issues, decode your JWT at [jwt.ms](https://jwt.ms) and verify: + +- **`aud` (audience)**: Matches your API's Client ID or App ID URI +- **`iss` (issuer)**: Matches your tenant (`https://login.microsoftonline.com//v2.0`) +- **`scp` (scopes)**: Contains the required scopes +- **`exp` (expiration)**: Token hasn't expired + +
+ +--- + +## Part 4: Common scenarios + +### 4.1: Protect Blazor pages + +Add `[Authorize]` to pages requiring authentication: + +```razor +@page "/weather" +@attribute [Authorize] +``` + +Or use authorization policies: + +```csharp +// Program.cs +builder.Services.AddAuthorization(options => +{ + options.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin")); +}); +``` + +```razor +@attribute [Authorize(Policy = "AdminOnly")] +``` + +### 4.2: Scope validation in the API + +To ensure the API only accepts tokens with specific scopes: + +```csharp +// In MyService.ApiService/Program.cs +app.MapGet("/weatherforecast", () => +{ + // ... implementation +}) +.RequireAuthorization() +.RequireScope("access_as_user"); // Requires this specific scope +``` + +Or configure scope validation globally: + +```csharp +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")); + +builder.Services.AddAuthorization(options => +{ + options.AddPolicy("ReadWeather", policy => + policy.RequireScope("Weather.Read")); +}); +``` + +### 4.3: Use app-only tokens (service-to-service) + +For daemon scenarios or service-to-service calls without a user context: + +```csharp +// Configure with RequestAppToken = true +builder.Services.AddHttpClient(client => +{ + client.BaseAddress = new("https+http://apiservice"); +}) +.AddMicrosoftIdentityMessageHandler(options => +{ + options.Scopes.Add("api:///.default"); + options.RequestAppToken = true; +}); +``` + +### 4.4: Override options per request + +Override default options on a per-request basis using the `WithAuthenticationOptions` extension method: + +```csharp +public class WeatherApiClient +{ + private readonly HttpClient _httpClient; + + public WeatherApiClient(HttpClient httpClient) + { + _httpClient = httpClient; + } + + public async Task GetSensitiveDataAsync() + { + // Override scopes for this specific request + var request = new HttpRequestMessage(HttpMethod.Get, "/weatherforecast") + .WithAuthenticationOptions(options => + { + options.Scopes.Clear(); + options.Scopes.Add("api:///sensitive.read"); + }); + + var response = await _httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadFromJsonAsync(); + } +} +``` + +### 4.5: Use federated identity credentials with Managed Identity (production) + +For production deployments in Azure, use managed identity instead of client secrets: + +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "", + "ClientId": "", + "ClientCredentials": [ + { + "SourceType": "SignedAssertionFromManagedIdentity", + "ManagedIdentityClientId": "" + } + ] + } +} +``` + +šŸ“š [Certificateless Authentication](../authentication/credentials/certificateless.md) + +### 4.6: Handle Conditional Access / MFA + +`MicrosoftIdentityMessageHandler` automatically handles WWW-Authenticate challenges for Conditional Access scenarios. No additional code is needed - the handler will: +1. Detect 401 responses with WWW-Authenticate challenges +2. Extract required claims from the challenge +3. Acquire a new token with the additional claims +4. Retry the request with the new token + +For Blazor Server, use the `BlazorAuthenticationChallengeHandler` to handle consent and Conditional Access in the UI: + +```razor +@inject BlazorAuthenticationChallengeHandler ChallengeHandler + +@code { + try + { + forecasts = await WeatherApi.GetWeatherAsync(); + } + catch (Exception ex) + { + // Handles MicrosoftIdentityWebChallengeUserException and redirects for re-auth + if (!await ChallengeHandler.HandleExceptionAsync(ex)) + { + errorMessage = $"Error: {ex.Message}"; + } + } +} +``` + +> šŸ“š See [Section 2.7](#27-handle-exceptions-on-pages-calling-apis) for the complete pattern. + +### 4.7: Multi-tenant API + +To accept tokens from any Azure AD tenant: + +```jsonc +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "organizations", // or common + "ClientId": "" + } +} +``` + +### 4.8: Call downstream APIs from the API (on-behalf-of) + +If your API needs to call another downstream API on behalf of the user: + +```csharp +// MyService.ApiService/Program.cs +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")) + .EnableTokenAcquisitionToCallDownstreamApi() + .AddInMemoryTokenCaches(); + +builder.Services.AddDownstreamApi("GraphApi", builder.Configuration.GetSection("GraphApi")); +``` + +```json +{ + "GraphApi": { + "BaseUrl": "https://graph.microsoft.com/v1.0", + "Scopes": [ "User.Read" ] + } +} +``` + +```csharp +// In a controller or endpoint +app.MapGet("/me", async (IDownstreamApi downstreamApi) => +{ + var user = await downstreamApi.GetForUserAsync("GraphApi", "me"); + return user; +}).RequireAuthorization(); +``` + +šŸ“š [Calling Downstream APIs](../calling-downstream-apis/calling-downstream-apis-README.md) + +### 4.9: Composing with other handlers + +Chain multiple handlers in the pipeline: + +```csharp +builder.Services.AddHttpClient(client => +{ + client.BaseAddress = new("https+http://apiservice"); +}) +.AddMicrosoftIdentityMessageHandler(options => +{ + options.Scopes.Add("api:///.default"); +}) +.AddHttpMessageHandler() +.AddHttpMessageHandler(); +``` + +--- + +## Resources + +
+šŸ“š Microsoft.Identity.Web + +- šŸ“˜ [Microsoft.Identity.Web Documentation](https://github.com/AzureAD/microsoft-identity-web/tree/master/docs) +- šŸ“˜ [Quick Start: Web App](../getting-started/quickstart-webapp.md) +- šŸ“˜ [Quick Start: Web API](../getting-started/quickstart-webapi.md) +- šŸ“˜ [Calling Custom APIs](../calling-downstream-apis/custom-apis.md) +- šŸ” [Credentials Guide](../authentication/credentials/credentials-README.md) +- šŸŽ« [Microsoft Identity Platform](https://learn.microsoft.com/entra/identity-platform/) + +
+ +
+šŸš€ .NET Aspire + +- šŸ“˜ [Aspire Overview](https://learn.microsoft.com/dotnet/aspire/get-started/aspire-overview) +- šŸš€ [Build Your First Aspire App](https://learn.microsoft.com/dotnet/aspire/get-started/build-your-first-aspire-app) +- šŸ” [Service Discovery](https://learn.microsoft.com/dotnet/aspire/service-discovery/overview) + +
+ +
+🧪 Samples + +- 🧪 [ASP.NET Core Web App signing-in users](https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/tree/master/1-WebApp-OIDC) +- 🧪 [ASP.NET Core Web API protected by Azure AD](https://github.com/Azure-Samples/active-directory-dotnet-native-aspnetcore-v2) + +
+ +--- + +## šŸ¤– AI coding assistant skills + +This guide has companion **AI Skills** for GitHub Copilot, Claude, and other AI coding assistants. The skills help automate both phases of the implementation: + +| Skill | Purpose | Location | +|-------|---------|----------| +| **entra-id-aspire-authentication** | Phase 1: Add authentication code | [SKILL.md](../../.github/skills/entra-id-aspire-authentication/SKILL.md) | +| **entra-id-aspire-provisioning** | Phase 2: Create app registrations | [SKILL.md](../../.github/skills/entra-id-aspire-provisioning/SKILL.md) | + +The authentication skill folder also contains **ready-to-copy helper files**: +- `BlazorAuthenticationChallengeHandler.cs` - Handles incremental consent and Conditional Access +- `LoginLogoutEndpointRouteBuilderExtensions.cs` - Enhanced login/logout endpoints + +See the [Skills README](../../.github/skills/README.md) for installation instructions. + +--- + +**Last Updated:** February 2026 +**Solution:** MyService (.NET Aspire, Microsoft.Identity.Web) 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/migration-authority-vs-instance.md b/docs/migration-authority-vs-instance.md new file mode 100644 index 000000000..d0156a728 --- /dev/null +++ b/docs/migration-authority-vs-instance.md @@ -0,0 +1,519 @@ +# Migration Guide: Authority vs Instance/TenantId Configuration + +This guide helps you upgrade existing Microsoft.Identity.Web applications to use recommended authority configuration patterns, especially if you're encountering the EventId 408 warning about conflicting Authority and Instance/TenantId settings. + +## Understanding the Warning + +If you see a log message like this: + +``` +[Warning] [MsIdWeb] Authority 'https://login.microsoftonline.com/common' is being ignored +because Instance 'https://login.microsoftonline.com/' and/or TenantId 'organizations' +are already configured. To use Authority, remove Instance and TenantId from the configuration. +``` + +This indicates your configuration has **conflicting authority settings**. The library uses Instance and TenantId, completely ignoring the Authority value. + +**Event ID**: 408 (AuthorityConflict) + +## Quick Fix Options + +### Option 1: Remove Authority (Recommended for most scenarios) + +**Before**: +```json +{ + "AzureAd": { + "Authority": "https://login.microsoftonline.com/common", + "Instance": "https://login.microsoftonline.com/", + "TenantId": "organizations", + "ClientId": "11111111-1111-1111-1111-111111111111" + } +} +``` + +**After**: +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "organizations", + "ClientId": "11111111-1111-1111-1111-111111111111" + } +} +``` + +### Option 2: Remove Instance and TenantId (Simpler for some scenarios) + +**Before**: +```json +{ + "AzureAd": { + "Authority": "https://login.microsoftonline.com/common", + "Instance": "https://login.microsoftonline.com/", + "TenantId": "common", + "ClientId": "11111111-1111-1111-1111-111111111111" + } +} +``` + +**After**: +```json +{ + "AzureAd": { + "Authority": "https://login.microsoftonline.com/common", + "ClientId": "11111111-1111-1111-1111-111111111111" + } +} +``` + +## Scenario-Specific Migration Patterns + +### Azure AD Single-Tenant Applications + +#### Pattern 1: From Authority to Instance/TenantId + +**Before (using Authority)**: +```json +{ + "AzureAd": { + "Authority": "https://login.microsoftonline.com/12345678-1234-1234-1234-123456789012", + "ClientId": "11111111-1111-1111-1111-111111111111" + } +} +``` + +**After (split into Instance/TenantId - Recommended)**: +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "12345678-1234-1234-1234-123456789012", + "ClientId": "11111111-1111-1111-1111-111111111111" + } +} +``` + +**Benefits**: +- Clear separation of instance and tenant +- Easier to update for different environments +- Consistent with Microsoft documentation + +#### Pattern 2: Keep Authority (Also Valid) + +If you prefer the Authority format, you can keep it: + +```json +{ + "AzureAd": { + "Authority": "https://login.microsoftonline.com/12345678-1234-1234-1234-123456789012", + "ClientId": "11111111-1111-1111-1111-111111111111" + } +} +``` + +**Note**: The library will automatically parse this into Instance and TenantId internally. + +### Azure AD Multi-Tenant Applications + +#### From Mixed Configuration + +**Before (conflicting configuration)**: +```json +{ + "AzureAd": { + "Authority": "https://login.microsoftonline.com/organizations", + "Instance": "https://login.microsoftonline.com/", + "TenantId": "common", + "ClientId": "11111111-1111-1111-1111-111111111111" + } +} +``` + +**After (using Instance/TenantId)**: +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "organizations", + "ClientId": "11111111-1111-1111-1111-111111111111" + } +} +``` + +**Alternative (using Authority)**: +```json +{ + "AzureAd": { + "Authority": "https://login.microsoftonline.com/organizations", + "ClientId": "11111111-1111-1111-1111-111111111111" + } +} +``` + +### Azure AD B2C Applications + +For B2C, **always use Authority** including the policy path. Do NOT use Instance/TenantId separately. + +#### Consolidate to Authority-Only + +**Before (incorrect mixed configuration)**: +```json +{ + "AzureAdB2C": { + "Authority": "https://contoso.b2clogin.com/contoso.onmicrosoft.com/B2C_1_susi", + "Instance": "https://contoso.b2clogin.com/", + "TenantId": "contoso.onmicrosoft.com", + "ClientId": "11111111-1111-1111-1111-111111111111", + "Domain": "contoso.onmicrosoft.com" + } +} +``` + +**After (correct Authority-based configuration)**: +```json +{ + "AzureAdB2C": { + "Authority": "https://contoso.b2clogin.com/contoso.onmicrosoft.com/B2C_1_susi", + "ClientId": "11111111-1111-1111-1111-111111111111", + "Domain": "contoso.onmicrosoft.com", + "SignUpSignInPolicyId": "B2C_1_susi" + } +} +``` + +**Critical**: B2C requires the policy path in the Authority. Splitting into Instance/TenantId loses the policy information. + +### CIAM Applications + +For CIAM, use the complete Authority URL. The library handles CIAM authorities automatically. + +#### Remove Conflicting Properties + +**Before (conflicting configuration)**: +```json +{ + "AzureAd": { + "Authority": "https://contoso.ciamlogin.com/contoso.onmicrosoft.com", + "Instance": "https://contoso.ciamlogin.com/", + "TenantId": "contoso.onmicrosoft.com", + "ClientId": "11111111-1111-1111-1111-111111111111" + } +} +``` + +**After (correct CIAM configuration)**: +```json +{ + "AzureAd": { + "Authority": "https://contoso.ciamlogin.com/contoso.onmicrosoft.com", + "ClientId": "11111111-1111-1111-1111-111111111111" + } +} +``` + +#### Custom Domain CIAM + +**Before**: +```json +{ + "AzureAd": { + "Authority": "https://login.contoso.com/contoso.onmicrosoft.com", + "Instance": "https://login.contoso.com/", + "ClientId": "11111111-1111-1111-1111-111111111111" + } +} +``` + +**After**: +```json +{ + "AzureAd": { + "Authority": "https://login.contoso.com/contoso.onmicrosoft.com", + "ClientId": "11111111-1111-1111-1111-111111111111" + } +} +``` + +**Note**: Ensure your custom domain is properly configured in your CIAM tenant before using it in your application. + +## Government Cloud Migrations + +### Azure Government (US) + +**Before (conflicting)**: +```json +{ + "AzureAd": { + "Authority": "https://login.microsoftonline.us/12345678-1234-1234-1234-123456789012", + "Instance": "https://login.microsoftonline.com/", + "TenantId": "12345678-1234-1234-1234-123456789012", + "ClientId": "11111111-1111-1111-1111-111111111111" + } +} +``` + +**After (corrected)**: +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.us/", + "TenantId": "12345678-1234-1234-1234-123456789012", + "ClientId": "11111111-1111-1111-1111-111111111111" + } +} +``` + +### Azure China + +**After (using Instance/TenantId)**: +```json +{ + "AzureAd": { + "Instance": "https://login.chinacloudapi.cn/", + "TenantId": "12345678-1234-1234-1234-123456789012", + "ClientId": "11111111-1111-1111-1111-111111111111" + } +} +``` + +**Alternative (using Authority)**: +```json +{ + "AzureAd": { + "Authority": "https://login.chinacloudapi.cn/12345678-1234-1234-1234-123456789012", + "ClientId": "11111111-1111-1111-1111-111111111111" + } +} +``` + +## Multi-Environment Configuration Strategy + +### Using Environment-Specific Files + +Instead of maintaining different configurations in code, use environment-specific settings files: + +#### appsettings.json (base configuration) +```json +{ + "AzureAd": { + "ClientId": "11111111-1111-1111-1111-111111111111", + "CallbackPath": "/signin-oidc" + } +} +``` + +#### appsettings.Development.json +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "common" + } +} +``` + +#### appsettings.Production.json +```json +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "12345678-1234-1234-1234-123456789012" + } +} +``` + +### Using Azure Key Vault for Authority Settings + +```csharp +// Program.cs +var builder = WebApplication.CreateBuilder(args); + +// Load configuration from Key Vault +if (builder.Environment.IsProduction()) +{ + var keyVaultEndpoint = new Uri(builder.Configuration["KeyVaultEndpoint"]!); + builder.Configuration.AddAzureKeyVault( + keyVaultEndpoint, + new DefaultAzureCredential()); +} + +builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd")); +``` + +**Key Vault Secrets**: +- `AzureAd--Instance`: `https://login.microsoftonline.com/` +- `AzureAd--TenantId`: `12345678-1234-1234-1234-123456789012` +- `AzureAd--ClientId`: `11111111-1111-1111-1111-111111111111` +- `AzureAd--ClientSecret`: `your-client-secret` + +## Code-Based Configuration Migration + +### Before: Mixed Configuration in Code + +```csharp +// Startup.cs or Program.cs (old pattern) +services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(options => + { + options.Authority = "https://login.microsoftonline.com/common"; + options.Instance = "https://login.microsoftonline.com/"; + options.TenantId = "organizations"; + options.ClientId = "11111111-1111-1111-1111-111111111111"; + }); +``` + +### After: Consistent Configuration + +**Option 1: Using Instance/TenantId**: +```csharp +services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(options => + { + options.Instance = "https://login.microsoftonline.com/"; + options.TenantId = "organizations"; + options.ClientId = "11111111-1111-1111-1111-111111111111"; + }); +``` + +**Option 2: Using Authority**: +```csharp +services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(options => + { + options.Authority = "https://login.microsoftonline.com/organizations"; + options.ClientId = "11111111-1111-1111-1111-111111111111"; + }); +``` + +**Option 3: Using Configuration Section (Recommended)**: +```csharp +services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAd")); +``` + +## Testing Your Migration + +### Step 1: Update Configuration + +Choose your preferred pattern and update `appsettings.json` accordingly. + +### Step 2: Clear Warning Logs + +After updating your configuration, restart your application and verify that the EventId 408 warning no longer appears in your logs. + +### Step 3: Verify Authentication Flow + +1. Navigate to a protected page in your application +2. Verify you're redirected to the correct sign-in page +3. Sign in and verify successful authentication +4. Check that tokens are acquired correctly + +### Step 4: Monitor Logs + +Enable detailed logging to verify the configuration is applied correctly: + +```json +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Identity.Web": "Debug" + } + } +} +``` + +Look for log entries confirming your authority configuration without warnings. + +## Common Migration Issues + +### Issue 1: Sign-in Redirect to Wrong Tenant + +**Symptom**: Users are redirected to an unexpected tenant for authentication. + +**Cause**: Instance or TenantId values don't match the intended authority. + +**Solution**: Verify that Instance and TenantId, when combined, equal your intended Authority URL. + +### Issue 2: Configuration Not Taking Effect + +**Symptom**: Changes to configuration don't seem to apply. + +**Cause**: Configuration caching or environment-specific overrides. + +**Solution**: +- Restart the application +- Check for environment-specific settings files that might override your changes +- Verify configuration binding in code + +### Issue 3: B2C Policy Not Found + +**Symptom**: "AADB2C90008: The provided grant has not been issued for this endpoint" + +**Cause**: Policy path missing from Authority after migration. + +**Solution**: Ensure the B2C Authority includes the full policy path: +```json +{ + "AzureAdB2C": { + "Authority": "https://contoso.b2clogin.com/contoso.onmicrosoft.com/B2C_1_susi", + "ClientId": "..." + } +} +``` + +### Issue 4: CIAM Custom Domain Errors + +**Symptom**: Authentication fails with custom domain. + +**Cause**: Mixing Authority with Instance/TenantId or custom domain not configured properly in Azure. + +**Solution**: Use Authority only and verify custom domain configuration: +```json +{ + "AzureAd": { + "Authority": "https://login.contoso.com/contoso.onmicrosoft.com", + "ClientId": "..." + } +} +``` + +Ensure your custom domain is properly configured in the Azure portal. + +## Rollback Plan + +If migration causes issues, you can temporarily revert while investigating: + +### Quick Rollback + +1. Restore your previous `appsettings.json` from version control +2. Restart the application +3. Verify authentication works with the old configuration + +### Gradual Migration + +If you have multiple applications: + +1. Migrate one application first +2. Test thoroughly in non-production environments +3. Monitor for issues before migrating additional applications +4. Use feature flags if available to toggle between configurations + +## Additional Resources + +- [Authority Configuration & Precedence Guide](authority-configuration.md) +- [Azure AD B2C Authority Examples](b2c-authority-examples.md) +- [CIAM Authority Examples](ciam-authority-examples.md) +- [Authority Precedence FAQ](faq-authority-precedence.md) +- [Microsoft.Identity.Web Wiki](https://github.com/AzureAD/microsoft-identity-web/wiki) + +## Getting Help + +If you encounter issues during migration: + +1. Check the [FAQ](faq-authority-precedence.md) for common questions +2. Enable debug logging to gather diagnostic information +3. Review the [GitHub Issues](https://github.com/AzureAD/microsoft-identity-web/issues) for similar problems +4. Open a new issue with detailed configuration (sanitize sensitive values) and log output 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/global.json b/global.json index f232680ce..8d2d57fe9 100644 --- a/global.json +++ b/global.json @@ -3,7 +3,7 @@ "Microsoft.Build.NoTargets": "3.7.56" }, "sdk": { - "version": "10.0.100", + "version": "10.0.103", "allowPrerelease": false, "rollForward": "latestFeature" } 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/AgentIdentitiesExtension.cs b/src/Microsoft.Identity.Web.AgentIdentities/AgentIdentitiesExtension.cs index 6d3c42f2d..a73f1bfe0 100644 --- a/src/Microsoft.Identity.Web.AgentIdentities/AgentIdentitiesExtension.cs +++ b/src/Microsoft.Identity.Web.AgentIdentities/AgentIdentitiesExtension.cs @@ -117,8 +117,9 @@ internal static AcquireTokenOptions ForAgentIdentity(this AcquireTokenOptions op // Until it makes it way through Abstractions options.ExtraParameters[Constants.FmiPathForClientAssertion] = agentApplicationId; - // TODO: do we want to expose a mechanism to override the MicrosoftIdentityOptions instead of leveraging - // the default configuration section / named options?. + // Use the developer's AuthenticationOptionsName if set, otherwise default to "AzureAd" + string configurationSection = options.AuthenticationOptionsName ?? "AzureAd"; + options.ExtraParameters[Constants.MicrosoftIdentityOptionsParameter] = new MicrosoftEntraApplicationOptions { ClientId = agentApplicationId, // Agent identity Client ID. @@ -126,7 +127,7 @@ internal static AcquireTokenOptions ForAgentIdentity(this AcquireTokenOptions op SourceType = CredentialSource.CustomSignedAssertion, CustomSignedAssertionProviderName = "OidcIdpSignedAssertion", CustomSignedAssertionProviderData = new Dictionary { - { "ConfigurationSection", "AzureAd" }, // Use the default configuration section name + { "ConfigurationSection", configurationSection }, // Use the developer's choice or default to "AzureAd" { "RequiresSignedAssertionFmiPath", true }, // The OidcIdpSignedAssertionProvider will require the fmiPath to be provided in the assertionRequestOptions. } }] 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.Azure/Microsoft.Identity.Web.Azure.csproj b/src/Microsoft.Identity.Web.Azure/Microsoft.Identity.Web.Azure.csproj index 2b68c2a06..697258a9e 100644 --- a/src/Microsoft.Identity.Web.Azure/Microsoft.Identity.Web.Azure.csproj +++ b/src/Microsoft.Identity.Web.Azure/Microsoft.Identity.Web.Azure.csproj @@ -2,6 +2,7 @@ Microsoft Identity Web.Azure Microsoft Identity Web.Azure + true This package enables ASP.NET Core web apps and web APIs to use the Azure SDKs with the Microsoft identity platform (formerly Azure AD v2.0) diff --git a/src/Microsoft.Identity.Web.Certificate/CertificateDescription.cs b/src/Microsoft.Identity.Web.Certificate/CertificateDescription.cs index 72cb9cc28..b0a380cb3 100644 --- a/src/Microsoft.Identity.Web.Certificate/CertificateDescription.cs +++ b/src/Microsoft.Identity.Web.Certificate/CertificateDescription.cs @@ -161,12 +161,19 @@ public static CertificateDescription FromStoreWithDistinguishedName( /// /// . /// +#if NET10_0_OR_GREATER + public X509Certificate2? Certificate + { + get { return base.GetCertificateInternal(); } + protected internal set { base.SetCertificateInternal(value); } + } +#else public new X509Certificate2? Certificate { get { return base.Certificate; } protected internal set { base.Certificate = value; } } - +#endif /// /// . /// diff --git a/src/Microsoft.Identity.Web.Certificate/CertificateErrorMessage.cs b/src/Microsoft.Identity.Web.Certificate/CertificateErrorMessage.cs index f2e370029..d896f3f9e 100644 --- a/src/Microsoft.Identity.Web.Certificate/CertificateErrorMessage.cs +++ b/src/Microsoft.Identity.Web.Certificate/CertificateErrorMessage.cs @@ -18,9 +18,9 @@ internal static class CertificateErrorMessage public const string ClientCertificatesHaveExpiredOrCannotBeLoaded = "IDW10109: All client certificates passed to the configuration have expired or can't be loaded. "; public const string CustomProviderNameAlreadyExists = "IDW10111 The custom signed assertion provider '{0}' already exists, only the the first instance of ICustomSignedAssertionProvider with this name will be used."; - public const string CustomProviderNameNullOrEmpty = "IDW10112 The name of the custom signed assertion provider is null or empty."; - public const string CustomProviderNotFound = "IDW10113: The custom signed assertion provider with name '{0}' was not found. Was it registered in the service collection?"; - public const string CustomProviderSourceLoaderNullOrEmpty = "IDW10114 The dictionary of SourceLoaders for custom signed assertion providers is null or empty."; + public const string CustomProviderNameNullOrEmpty = "IDW10112: You configured a custom signed assertion but did not specify a provider name in the CustomSignedAssertionProviderName property of the CredentialDescription. Please specify the name of the custom assertion provider."; + public const string CustomProviderNotFound = "IDW10113: You configured a custom signed assertion with provider name '{0}' but it was not found. Did you register it in the service collection? You need to add a reference to the credential package and call the appropriate registration method, e.g., services.AddOidcFic() or services.AddFmiSignedAssertion()."; + public const string CustomProviderSourceLoaderNullOrEmpty = "IDW10114: You configured a custom signed assertion but no custom assertion providers have been registered. You need to add a reference to the credential package and call the appropriate registration method, e.g., services.AddOidcFic() or services.AddFmiSignedAssertion()."; // Encoding IDW10600 = "IDW10600:" public const string InvalidBase64UrlString = "IDW10601: Invalid Base64URL string. "; diff --git a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs index afab77dcd..fc81c6a32 100644 --- a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs +++ b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.CustomSignedAssertion.cs @@ -78,16 +78,23 @@ private async Task ProcessCustomSignedAssertionAsync(CredentialDescription crede { // No source loader(s) _logger.CustomProviderSourceLoaderNullOrEmpty(); + throw new InvalidOperationException(CertificateErrorMessage.CustomProviderSourceLoaderNullOrEmpty); } else if (string.IsNullOrEmpty(credentialDescription.CustomSignedAssertionProviderName)) { // No provider name _logger.CustomProviderNameNullOrEmpty(); + throw new InvalidOperationException(CertificateErrorMessage.CustomProviderNameNullOrEmpty); } else if (!CustomSignedAssertionCredentialSourceLoaders!.TryGetValue(credentialDescription.CustomSignedAssertionProviderName!, out ICustomSignedAssertionProvider? sourceLoader)) { // No source loader for provider name _logger.CustomProviderNotFound(credentialDescription.CustomSignedAssertionProviderName!); + string errorMessage = string.Format( + CultureInfo.InvariantCulture, + CertificateErrorMessage.CustomProviderNotFound, + credentialDescription.CustomSignedAssertionProviderName!); + throw new InvalidOperationException(errorMessage); } else { diff --git a/src/Microsoft.Identity.Web.Certificate/InternalAPI.Shipped.txt b/src/Microsoft.Identity.Web.Certificate/InternalAPI.Shipped.txt index 10213ca27..d356c0f3d 100644 --- a/src/Microsoft.Identity.Web.Certificate/InternalAPI.Shipped.txt +++ b/src/Microsoft.Identity.Web.Certificate/InternalAPI.Shipped.txt @@ -6,8 +6,11 @@ const Microsoft.Identity.Web.CertificateErrorMessage.ClientCertificatesHaveExpir const Microsoft.Identity.Web.CertificateErrorMessage.ClientSecretAndCertificateNull = "IDW10104: Both client secret and client certificate cannot be null or whitespace, and only ONE must be included in the configuration of the web app when calling a web API. For instance, in the appsettings.json file. " -> string! const Microsoft.Identity.Web.CertificateErrorMessage.CustomProviderNameAlreadyExists = "IDW10111 The custom signed assertion provider '{0}' already exists, only the the first instance of ICustomSignedAssertionProvider with this name will be used." -> string! const Microsoft.Identity.Web.CertificateErrorMessage.CustomProviderNameNullOrEmpty = "IDW10112 The name of the custom signed assertion provider is null or empty." -> string! +const Microsoft.Identity.Web.CertificateErrorMessage.CustomProviderNameNullOrEmpty = "IDW10112: You configured a custom signed assertion but did not specify a provider name in the CustomSignedAssertionProviderName property of the CredentialDescription. Please specify the name of the custom assertion provider." -> string! const Microsoft.Identity.Web.CertificateErrorMessage.CustomProviderNotFound = "IDW10113: The custom signed assertion provider with name '{0}' was not found. Was it registered in the service collection?" -> string! +const Microsoft.Identity.Web.CertificateErrorMessage.CustomProviderNotFound = "IDW10113: You configured a custom signed assertion with provider name '{0}' but it was not found. Did you register it in the service collection? You need to add a reference to the credential package and call the appropriate registration method, e.g., services.AddOidcFic() or services.AddFmiSignedAssertion()." -> string! const Microsoft.Identity.Web.CertificateErrorMessage.CustomProviderSourceLoaderNullOrEmpty = "IDW10114 The dictionary of SourceLoaders for custom signed assertion providers is null or empty." -> string! +const Microsoft.Identity.Web.CertificateErrorMessage.CustomProviderSourceLoaderNullOrEmpty = "IDW10114: You configured a custom signed assertion but no custom assertion providers have been registered. You need to add a reference to the credential package and call the appropriate registration method, e.g., services.AddOidcFic() or services.AddFmiSignedAssertion()." -> string! const Microsoft.Identity.Web.CertificateErrorMessage.FromStoreWithThumbprintIsObsolete = "IDW10803: Use FromStoreWithThumbprint instead, due to spelling error. " -> string! const Microsoft.Identity.Web.CertificateErrorMessage.IncorrectNumberOfUriSegments = "IDW10702: Number of URI segments is incorrect: {0}, URI: {1}. " -> string! const Microsoft.Identity.Web.CertificateErrorMessage.InvalidBase64UrlString = "IDW10601: Invalid Base64URL string. " -> string! diff --git a/src/Microsoft.Identity.Web.Certificate/Microsoft.Identity.Web.Certificate.csproj b/src/Microsoft.Identity.Web.Certificate/Microsoft.Identity.Web.Certificate.csproj index a94798335..7e06581fb 100644 --- a/src/Microsoft.Identity.Web.Certificate/Microsoft.Identity.Web.Certificate.csproj +++ b/src/Microsoft.Identity.Web.Certificate/Microsoft.Identity.Web.Certificate.csproj @@ -5,6 +5,7 @@ This package brings certificate management for MSAL.NET. {1E0B96CD-FDBF-482C-996A-775F691D984E} README.md + true diff --git a/src/Microsoft.Identity.Web.Certificateless/CertificatelessOptions.cs b/src/Microsoft.Identity.Web.Certificateless/CertificatelessOptions.cs index ee4e722ff..cee3e2bb8 100644 --- a/src/Microsoft.Identity.Web.Certificateless/CertificatelessOptions.cs +++ b/src/Microsoft.Identity.Web.Certificateless/CertificatelessOptions.cs @@ -17,9 +17,8 @@ public class CertificatelessOptions /// /// The value is used to establish a connection between external workload identities - /// and Azure Active Directory. If Azure AD is the issuer, this value should be the object - /// ID of the managed identity service principal in the tenant that will be used to - /// impersonate the app. + /// and Azure Active Directory. If Azure AD is the issuer, this value should be the client + /// ID of the managed identity. /// Can be null if you are using the machine assigned managed identity. /// Needs to be assigned if you are using a user assigned managed identity. /// diff --git a/src/Microsoft.Identity.Web.Certificateless/InternalAPI.Shipped.txt b/src/Microsoft.Identity.Web.Certificateless/InternalAPI.Shipped.txt index 2da3dab46..a36f8b883 100644 --- a/src/Microsoft.Identity.Web.Certificateless/InternalAPI.Shipped.txt +++ b/src/Microsoft.Identity.Web.Certificateless/InternalAPI.Shipped.txt @@ -2,3 +2,7 @@ const Microsoft.Identity.Web.Certificateless.CertificatelessConstants.DefaultTokenExchangeUrl = "api://AzureADTokenExchange" -> string! Microsoft.Identity.Web.Certificateless.CertificatelessConstants Microsoft.Identity.Web.Certificateless.CertificatelessConstants.CertificatelessConstants() -> void +Microsoft.Identity.Web.ManagedIdentityClientAssertion.ManagedIdentityClientAssertion(string? managedIdentityClientId, string? tokenExchangeUrl, Microsoft.Extensions.Logging.ILogger? logger, Microsoft.Identity.Client.IMsalHttpClientFactory? testHttpClientFactory) -> void +Microsoft.Identity.Web.TestOnly.ManagedIdentityClientAssertionTestHook +static Microsoft.Identity.Web.TestOnly.ManagedIdentityClientAssertionTestHook.HttpClientFactoryForTests.get -> Microsoft.Identity.Client.IMsalHttpClientFactory? +static Microsoft.Identity.Web.TestOnly.ManagedIdentityClientAssertionTestHook.HttpClientFactoryForTests.set -> void diff --git a/src/Microsoft.Identity.Web.Certificateless/ManagedIdentityClientAssertion.cs b/src/Microsoft.Identity.Web.Certificateless/ManagedIdentityClientAssertion.cs index 0f9568a47..a049e3499 100644 --- a/src/Microsoft.Identity.Web.Certificateless/ManagedIdentityClientAssertion.cs +++ b/src/Microsoft.Identity.Web.Certificateless/ManagedIdentityClientAssertion.cs @@ -9,6 +9,7 @@ using Microsoft.Identity.Client.AppConfig; using Microsoft.Identity.Client.Extensibility; using Microsoft.Identity.Web.Certificateless; +using Microsoft.Identity.Web.TestOnly; namespace Microsoft.Identity.Web { @@ -17,7 +18,7 @@ namespace Microsoft.Identity.Web /// public class ManagedIdentityClientAssertion : ClientAssertionProviderBase { - IManagedIdentityApplication _managedIdentityApplication; + private IManagedIdentityApplication _managedIdentityApplication; private readonly string _tokenExchangeUrl; private readonly ILogger? _logger; @@ -49,7 +50,33 @@ public ManagedIdentityClientAssertion(string? managedIdentityClientId, string? t /// Optional audience of the token to be requested from Managed Identity. Default value is "api://AzureADTokenExchange". /// This value is different on clouds other than Azure Public /// A logger - public ManagedIdentityClientAssertion(string? managedIdentityClientId, string? tokenExchangeUrl, ILogger? logger) + public ManagedIdentityClientAssertion( + string? managedIdentityClientId, + string? tokenExchangeUrl, + ILogger? logger) + : this( + managedIdentityClientId, + tokenExchangeUrl, + logger, + ManagedIdentityClientAssertionTestHook.HttpClientFactoryForTests) + { + } + + + /// + /// Same as , + /// but allows injecting a custom MSAL HttpClient factory (used by tests). + /// + /// Optional ClientId of the Managed Identity + /// Optional audience of the token to be requested from Managed Identity. Default value is "api://AzureADTokenExchange". + /// This value is different on clouds other than Azure Public + /// A logger. + /// Optional MSAL HttpClient factory. + internal ManagedIdentityClientAssertion( + string? managedIdentityClientId, + string? tokenExchangeUrl, + ILogger? logger, + IMsalHttpClientFactory? testHttpClientFactory) { _tokenExchangeUrl = tokenExchangeUrl ?? CertificatelessConstants.DefaultTokenExchangeUrl; _logger = logger; @@ -61,6 +88,12 @@ public ManagedIdentityClientAssertion(string? managedIdentityClientId, string? t } var builder = ManagedIdentityApplicationBuilder.Create(id); + + if (testHttpClientFactory != null) + { + builder = builder.WithHttpClientFactory(testHttpClientFactory); + } + if (_logger != null) { builder = builder.WithLogging(Log, ConvertMicrosoftExtensionsLogLevelToMsal(_logger), enablePiiLogging: false); @@ -76,10 +109,24 @@ public ManagedIdentityClientAssertion(string? managedIdentityClientId, string? t /// acquired with managed identity (certificateless). /// /// The signed assertion. - protected override async Task GetClientAssertionAsync(AssertionRequestOptions? assertionRequestOptions) + protected override async Task GetClientAssertionAsync( + AssertionRequestOptions? assertionRequestOptions) { - var result = await _managedIdentityApplication - .AcquireTokenForManagedIdentity(_tokenExchangeUrl) + // Start the MI token request for the token-exchange audience + var miBuilder = _managedIdentityApplication + .AcquireTokenForManagedIdentity(_tokenExchangeUrl); + + if (assertionRequestOptions is not null) + { + // Propagate claims into the MI token request. + // This also forces MSAL to bypass the MI token cache when claims are present. + if (!string.IsNullOrEmpty(assertionRequestOptions.Claims)) + { + miBuilder.WithClaims(assertionRequestOptions.Claims); + } + } + + var result = await miBuilder .ExecuteAsync(assertionRequestOptions?.CancellationToken ?? CancellationToken.None) .ConfigureAwait(false); diff --git a/src/Microsoft.Identity.Web.Certificateless/ManagedIdentityClientAssertionTestHook.cs b/src/Microsoft.Identity.Web.Certificateless/ManagedIdentityClientAssertionTestHook.cs new file mode 100644 index 000000000..cd314e5a5 --- /dev/null +++ b/src/Microsoft.Identity.Web.Certificateless/ManagedIdentityClientAssertionTestHook.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Identity.Client; + +namespace Microsoft.Identity.Web.TestOnly +{ + /// + /// TEST-ONLY hook so unit tests can override the HttpClient factory used by + /// ManagedIdentityClientAssertion. + /// + internal static class ManagedIdentityClientAssertionTestHook + { + /// + /// Gets or sets the used by ManagedIdentityClientAssertion for unit testing purposes. + /// + internal static IMsalHttpClientFactory? HttpClientFactoryForTests { get; set; } + } +} diff --git a/src/Microsoft.Identity.Web.Certificateless/Microsoft.Identity.Web.xml b/src/Microsoft.Identity.Web.Certificateless/Microsoft.Identity.Web.xml index ae836887c..fbc1f5156 100644 --- a/src/Microsoft.Identity.Web.Certificateless/Microsoft.Identity.Web.xml +++ b/src/Microsoft.Identity.Web.Certificateless/Microsoft.Identity.Web.xml @@ -19,9 +19,8 @@ The value is used to establish a connection between external workload identities - and Azure Active Directory. If Azure AD is the issuer, this value should be the object - ID of the managed identity service principal in the tenant that will be used to - impersonate the app. + and Azure Active Directory. If Azure AD is the issuer, this value should be the client + ID of the managed identity. Can be null if you are using the machine assigned managed identity. Needs to be assigned if you are using a user assigned managed identity. diff --git a/src/Microsoft.Identity.Web.Diagnostics/Microsoft.Identity.Web.Diagnostics.csproj b/src/Microsoft.Identity.Web.Diagnostics/Microsoft.Identity.Web.Diagnostics.csproj index 558ff39c4..7e955852d 100644 --- a/src/Microsoft.Identity.Web.Diagnostics/Microsoft.Identity.Web.Diagnostics.csproj +++ b/src/Microsoft.Identity.Web.Diagnostics/Microsoft.Identity.Web.Diagnostics.csproj @@ -4,6 +4,7 @@ disable enable README.md + true
diff --git a/src/Microsoft.Identity.Web.Diagnostics/PublicAPI/net10.0/InternalAPI.Shipped.txt b/src/Microsoft.Identity.Web.Diagnostics/PublicAPI/net10.0/InternalAPI.Shipped.txt index 9bf9f0311..b3ec4183f 100644 --- a/src/Microsoft.Identity.Web.Diagnostics/PublicAPI/net10.0/InternalAPI.Shipped.txt +++ b/src/Microsoft.Identity.Web.Diagnostics/PublicAPI/net10.0/InternalAPI.Shipped.txt @@ -11,13 +11,13 @@ static Microsoft.Identity.Web.Diagnostics.OsHelper.IsMacPlatform() -> bool static Microsoft.Identity.Web.Diagnostics.OsHelper.IsWindowsPlatform() -> bool static Microsoft.Identity.Web.IdHelper.CreateTelemetryInfo() -> string! static Microsoft.Identity.Web.IdHelper.GetIdWebVersion() -> string! -static Microsoft.Identity.Web.Throws.ArgumentException(string! paramName, string? message) -> void static Microsoft.Identity.Web.Throws.ArgumentException(string! paramName, string? message, System.Exception? innerException) -> void -static Microsoft.Identity.Web.Throws.ArgumentNullException(string! paramName) -> void +static Microsoft.Identity.Web.Throws.ArgumentException(string! paramName, string? message) -> void static Microsoft.Identity.Web.Throws.ArgumentNullException(string! paramName, string? message) -> void -static Microsoft.Identity.Web.Throws.ArgumentOutOfRangeException(string! paramName) -> void +static Microsoft.Identity.Web.Throws.ArgumentNullException(string! paramName) -> void static Microsoft.Identity.Web.Throws.ArgumentOutOfRangeException(string! paramName, object? actualValue, string? message) -> void static Microsoft.Identity.Web.Throws.ArgumentOutOfRangeException(string! paramName, string? message) -> void +static Microsoft.Identity.Web.Throws.ArgumentOutOfRangeException(string! paramName) -> void static Microsoft.Identity.Web.Throws.IfBufferTooSmall(int bufferSize, int requiredSize, string! paramName = "") -> void static Microsoft.Identity.Web.Throws.IfMemberNull(TParameter argument, TMember member, string! paramName = "", string! memberName = "") -> TMember static Microsoft.Identity.Web.Throws.IfNull(T argument, string! paramName = "") -> T @@ -27,5 +27,5 @@ static Microsoft.Identity.Web.Throws.IfNullOrEmpty(System.Collections.Generic static Microsoft.Identity.Web.Throws.IfNullOrMemberNull(TParameter argument, TMember member, string! paramName = "", string! memberName = "") -> TMember static Microsoft.Identity.Web.Throws.IfNullOrWhitespace(string? argument, string! paramName = "") -> string! static Microsoft.Identity.Web.Throws.IfOutOfRange(T argument, string! paramName = "") -> T -static Microsoft.Identity.Web.Throws.InvalidOperationException(string! message) -> void static Microsoft.Identity.Web.Throws.InvalidOperationException(string! message, System.Exception? innerException) -> void +static Microsoft.Identity.Web.Throws.InvalidOperationException(string! message) -> void diff --git a/src/Microsoft.Identity.Web.DownstreamApi/DownstreamApi.cs b/src/Microsoft.Identity.Web.DownstreamApi/DownstreamApi.cs index cfe802afe..75ef5a4b3 100644 --- a/src/Microsoft.Identity.Web.DownstreamApi/DownstreamApi.cs +++ b/src/Microsoft.Identity.Web.DownstreamApi/DownstreamApi.cs @@ -1,22 +1,22 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Runtime.CompilerServices; -using System.Security.Claims; -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization.Metadata; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Identity.Abstractions; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Runtime.CompilerServices; +using System.Security.Claims; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Identity.Abstractions; using Microsoft.Identity.Client; namespace Microsoft.Identity.Web @@ -26,11 +26,20 @@ internal partial class DownstreamApi : IDownstreamApi { private readonly IAuthorizationHeaderProvider _authorizationHeaderProvider; private readonly IHttpClientFactory _httpClientFactory; + + // This MSAL HTTP client factory is used to create HTTP clients with mTLS binding certificate. + // Note, that it doesn't replace _httpClientFactory to keep backward compatibility and ability + // to create named HTTP clients for non-mTLS scenarios. + private readonly IMsalHttpClientFactory? _msalHttpClientFactory; + private readonly IOptionsMonitor _namedDownstreamApiOptions; + private const string Authorization = "Authorization"; - protected readonly ILogger _logger; + private const string TokenBindingProtocolScheme = "MTLS_POP"; private const string AuthSchemeDstsSamlBearer = "http://schemas.microsoft.com/dsts/saml2-bearer"; + protected readonly ILogger _logger; + /// /// Constructor. /// @@ -43,10 +52,33 @@ public DownstreamApi( IOptionsMonitor namedDownstreamApiOptions, IHttpClientFactory httpClientFactory, ILogger logger) + : this(authorizationHeaderProvider, + namedDownstreamApiOptions, + httpClientFactory, + logger, + msalHttpClientFactory: null) + { + } + + /// + /// Constructor which accepts optional MSAL HTTP client factory. + /// + /// Authorization header provider. + /// Named options provider. + /// HTTP client factory. + /// Logger. + /// The MSAL HTTP client factory for mTLS PoP scenarios. + public DownstreamApi( + IAuthorizationHeaderProvider authorizationHeaderProvider, + IOptionsMonitor namedDownstreamApiOptions, + IHttpClientFactory httpClientFactory, + ILogger logger, + IMsalHttpClientFactory? msalHttpClientFactory) { _authorizationHeaderProvider = authorizationHeaderProvider; _namedDownstreamApiOptions = namedDownstreamApiOptions; _httpClientFactory = httpClientFactory; + _msalHttpClientFactory = msalHttpClientFactory ?? new MsalMtlsHttpClientFactory(httpClientFactory); _logger = logger; } @@ -102,10 +134,8 @@ public Task CallApiForAppAsync( } /// -#if NET8_0_OR_GREATER [RequiresUnreferencedCode("Calls JsonSerializer.Serialize")] [RequiresDynamicCode("Calls JsonSerializer.Serialize")] -#endif public async Task CallApiForUserAsync( string? serviceName, TInput input, @@ -129,10 +159,8 @@ public Task CallApiForAppAsync( } /// -#if NET8_0_OR_GREATER [RequiresUnreferencedCode("Calls JsonSerializer.Serialize")] [RequiresDynamicCode("Calls JsonSerializer.Serialize")] -#endif [MethodImpl(MethodImplOptions.AggressiveInlining)] public async Task CallApiForAppAsync( string? serviceName, @@ -155,10 +183,8 @@ public Task CallApiForAppAsync( } /// -#if NET8_0_OR_GREATER [RequiresUnreferencedCode("Calls JsonSerializer.Serialize")] [RequiresDynamicCode("Calls JsonSerializer.Serialize")] -#endif [MethodImpl(MethodImplOptions.AggressiveInlining)] public async Task CallApiForAppAsync(string serviceName, Action? downstreamApiOptionsOverride = null, @@ -172,10 +198,8 @@ public Task CallApiForAppAsync( } /// -#if NET8_0_OR_GREATER [RequiresUnreferencedCode("Calls JsonSerializer.Serialize")] [RequiresDynamicCode("Calls JsonSerializer.Serialize")] -#endif public async Task CallApiForUserAsync( string? serviceName, Action? downstreamApiOptionsOverride = null, @@ -357,10 +381,8 @@ public Task CallApiForAppAsync( return clonedOptions; } -#if NET7_0_OR_GREATER [RequiresUnreferencedCode("Calls JsonSerializer.Serialize")] [RequiresDynamicCode("Calls JsonSerializer.Serialize")] -#endif internal static HttpContent? SerializeInput(TInput input, DownstreamApiOptions effectiveOptions) { HttpContent? httpContent; @@ -392,10 +414,8 @@ public Task CallApiForAppAsync( return httpContent; } -#if NET7_0_OR_GREATER [RequiresUnreferencedCode("Calls JsonSerializer.Serialize")] [RequiresDynamicCode("Calls JsonSerializer.Serialize")] -#endif internal static async Task DeserializeOutputAsync(HttpResponseMessage response, DownstreamApiOptions effectiveOptions, CancellationToken cancellationToken = default) where TOutput : class { @@ -436,7 +456,7 @@ public Task CallApiForAppAsync( string stringContent = await content.ReadAsStringAsync(); if (mediaType == "application/json") { - return JsonSerializer.Deserialize(stringContent, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + return JsonSerializer.Deserialize(stringContent, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); } if (mediaType != null && !mediaType.StartsWith("text/", StringComparison.OrdinalIgnoreCase)) { @@ -514,11 +534,17 @@ public Task CallApiForAppAsync( new HttpMethod(effectiveOptions.HttpMethod), apiUrl); - await UpdateRequestAsync(httpRequestMessage, content, effectiveOptions, appToken, user, cancellationToken); + // Request result will contain authorization header and potentially binding certificate for mTLS + var requestResult = await UpdateRequestAsync(httpRequestMessage, content, effectiveOptions, appToken, user, cancellationToken); - using HttpClient client = string.IsNullOrEmpty(serviceName) ? _httpClientFactory.CreateClient() : _httpClientFactory.CreateClient(serviceName); + // If a binding certificate is specified (which means mTLS is required) and MSAL mTLS HTTP factory is present + // then create an HttpClient with the certificate by using IMsalMtlsHttpClientFactory. + // Otherwise use the default HttpClientFactory with optional named client. + HttpClient client = requestResult?.BindingCertificate != null && _msalHttpClientFactory != null && _msalHttpClientFactory is IMsalMtlsHttpClientFactory msalMtlsHttpClientFactory + ? msalMtlsHttpClientFactory.GetHttpClient(requestResult.BindingCertificate) + : (string.IsNullOrEmpty(serviceName) ? _httpClientFactory.CreateClient() : _httpClientFactory.CreateClient(serviceName)); - // Send the HTTP message + // Send the HTTP message var downstreamApiResult = await client.SendAsync(httpRequestMessage, cancellationToken).ConfigureAwait(false); // Retry only if the resource sent 401 Unauthorized with WWW-Authenticate header and claims @@ -541,7 +567,7 @@ public Task CallApiForAppAsync( return downstreamApiResult; } - internal /* internal for test */ async Task UpdateRequestAsync( + internal /* internal for test */ async Task UpdateRequestAsync( HttpRequestMessage httpRequestMessage, HttpContent? content, DownstreamApiOptions effectiveOptions, @@ -558,15 +584,42 @@ public Task CallApiForAppAsync( effectiveOptions.RequestAppToken = appToken; + AuthorizationHeaderInformation? authorizationHeaderInformation = null; + // Obtention of the authorization header (except when calling an anonymous endpoint // which is done by not specifying any scopes if (effectiveOptions.Scopes != null && effectiveOptions.Scopes.Any()) { - string authorizationHeader = await _authorizationHeaderProvider.CreateAuthorizationHeaderAsync( - effectiveOptions.Scopes, - effectiveOptions, - user, - cancellationToken).ConfigureAwait(false); + string authorizationHeader = string.Empty; + + // Firstly check if it's token binding scenario so authorization header provider returns + // a binding certificate along with acquired authorization header. + if (_authorizationHeaderProvider is IBoundAuthorizationHeaderProvider boundAuthorizationHeaderBoundProvider + && string.Equals(effectiveOptions.ProtocolScheme, TokenBindingProtocolScheme, StringComparison.OrdinalIgnoreCase)) + { + var authorizationHeaderResult = await boundAuthorizationHeaderBoundProvider.CreateBoundAuthorizationHeaderAsync( + effectiveOptions, + user, + cancellationToken).ConfigureAwait(false); + + if (!authorizationHeaderResult.Succeeded) + { + // in theory it shouldn't happen because in case of error during token acquisition + // there will be thrown corresponding exception, so it's more a safeguard + throw new InvalidOperationException("Cannot acquire bound authorization header."); + } + + authorizationHeaderInformation = authorizationHeaderResult.Result; + authorizationHeader = authorizationHeaderInformation?.AuthorizationHeaderValue!; + } + else + { + authorizationHeader = await _authorizationHeaderProvider.CreateAuthorizationHeaderAsync( + effectiveOptions.Scopes, + effectiveOptions, + user, + cancellationToken).ConfigureAwait(false); + } if (authorizationHeader.StartsWith(AuthSchemeDstsSamlBearer, StringComparison.OrdinalIgnoreCase)) { @@ -582,54 +635,56 @@ public Task CallApiForAppAsync( { Logger.UnauthenticatedApiCall(_logger, null); } - if (!string.IsNullOrEmpty(effectiveOptions.AcceptHeader)) - { - httpRequestMessage.Headers.Accept.ParseAdd(effectiveOptions.AcceptHeader); - } - - // Add extra headers if specified directly on DownstreamApiOptions - if (effectiveOptions.ExtraHeaderParameters != null) - { - foreach (var header in effectiveOptions.ExtraHeaderParameters) - { - httpRequestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value); - } - } - - // Add extra query parameters if specified directly on DownstreamApiOptions - if (effectiveOptions.ExtraQueryParameters != null && effectiveOptions.ExtraQueryParameters.Count > 0) - { - var uriBuilder = new UriBuilder(httpRequestMessage.RequestUri!); - var existingQuery = uriBuilder.Query; - var queryString = new StringBuilder(existingQuery); - - foreach (var queryParam in effectiveOptions.ExtraQueryParameters) - { - if (queryString.Length > 1) // if there are existing query parameters - { - queryString.Append('&'); - } - else if (queryString.Length == 0) - { - queryString.Append('?'); - } - - queryString.Append(Uri.EscapeDataString(queryParam.Key)); - queryString.Append('='); - queryString.Append(Uri.EscapeDataString(queryParam.Value)); - } - - uriBuilder.Query = queryString.ToString().TrimStart('?'); - httpRequestMessage.RequestUri = uriBuilder.Uri; - } - - // Opportunity to change the request message + if (!string.IsNullOrEmpty(effectiveOptions.AcceptHeader)) + { + httpRequestMessage.Headers.Accept.ParseAdd(effectiveOptions.AcceptHeader); + } + + // Add extra headers if specified directly on DownstreamApiOptions + if (effectiveOptions.ExtraHeaderParameters != null) + { + foreach (var header in effectiveOptions.ExtraHeaderParameters) + { + httpRequestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + } + + // Add extra query parameters if specified directly on DownstreamApiOptions + if (effectiveOptions.ExtraQueryParameters != null && effectiveOptions.ExtraQueryParameters.Count > 0) + { + var uriBuilder = new UriBuilder(httpRequestMessage.RequestUri!); + var existingQuery = uriBuilder.Query; + var queryString = new StringBuilder(existingQuery); + + foreach (var queryParam in effectiveOptions.ExtraQueryParameters) + { + if (queryString.Length > 1) // if there are existing query parameters + { + queryString.Append('&'); + } + else if (queryString.Length == 0) + { + queryString.Append('?'); + } + + queryString.Append(Uri.EscapeDataString(queryParam.Key)); + queryString.Append('='); + queryString.Append(Uri.EscapeDataString(queryParam.Value)); + } + + uriBuilder.Query = queryString.ToString().TrimStart('?'); + httpRequestMessage.RequestUri = uriBuilder.Uri; + } + + // Opportunity to change the request message effectiveOptions.CustomizeHttpRequestMessage?.Invoke(httpRequestMessage); + + return authorizationHeaderInformation; } internal /* for test */ static Dictionary CallerSDKDetails { get; } = new() { - { "caller-sdk-id", "IdWeb_1" }, + { "caller-sdk-id", "IdWeb_1" }, { "caller-sdk-ver", IdHelper.GetIdWebVersion() } }; @@ -657,14 +712,14 @@ private static void AddCallerSDKTelemetry(DownstreamApiOptions effectiveOptions) internal static async Task ReadErrorResponseContentAsync(HttpResponseMessage response, CancellationToken cancellationToken = default) { const int maxErrorContentLength = 4096; - + long? contentLength = response.Content.Headers.ContentLength; - + if (contentLength.HasValue && contentLength.Value > maxErrorContentLength) { return $"[Error response too large: {contentLength.Value} bytes, not captured]"; } - + // Use streaming to read only up to maxErrorContentLength to avoid loading entire response into memory #if NET5_0_OR_GREATER using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); @@ -672,18 +727,18 @@ internal static async Task ReadErrorResponseContentAsync(HttpResponseMes using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); #endif using var reader = new StreamReader(stream); - + char[] buffer = new char[maxErrorContentLength]; int readCount = await reader.ReadBlockAsync(buffer, 0, maxErrorContentLength).ConfigureAwait(false); - + string errorResponseContent = new string(buffer, 0, readCount); - + // Check if there's more content that was truncated if (readCount == maxErrorContentLength && reader.Peek() != -1) { errorResponseContent += "... (truncated)"; } - + return errorResponseContent; } } diff --git a/src/Microsoft.Identity.Web.DownstreamApi/Microsoft.Identity.Web.DownstreamApi.csproj b/src/Microsoft.Identity.Web.DownstreamApi/Microsoft.Identity.Web.DownstreamApi.csproj index 171f99ba5..cebd4a2f8 100644 --- a/src/Microsoft.Identity.Web.DownstreamApi/Microsoft.Identity.Web.DownstreamApi.csproj +++ b/src/Microsoft.Identity.Web.DownstreamApi/Microsoft.Identity.Web.DownstreamApi.csproj @@ -10,6 +10,10 @@ true true + + + + True diff --git a/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net10.0/InternalAPI.Shipped.txt b/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net10.0/InternalAPI.Shipped.txt index 64b7c7852..2c1df99c9 100644 --- a/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net10.0/InternalAPI.Shipped.txt +++ b/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net10.0/InternalAPI.Shipped.txt @@ -21,6 +21,7 @@ Microsoft.Identity.Web.DownstreamApi.DeleteForUserAsync(string? Microsoft.Identity.Web.DownstreamApi.DeleteForUserAsync(string? serviceName, TInput input, System.Text.Json.Serialization.Metadata.JsonTypeInfo! inputJsonTypeInfo, System.Text.Json.Serialization.Metadata.JsonTypeInfo! outputJsonTypeInfo, System.Action? downstreamApiOptionsOverride = null, System.Security.Claims.ClaimsPrincipal? user = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DownstreamApi.DeleteForUserAsync(string? serviceName, TInput input, System.Action? downstreamApiOptionsOverride = null, System.Security.Claims.ClaimsPrincipal? user = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DownstreamApi.DeleteForUserAsync(string? serviceName, TInput input, System.Text.Json.Serialization.Metadata.JsonTypeInfo! inputJsonTypeInfo, System.Action? downstreamApiOptionsOverride = null, System.Security.Claims.ClaimsPrincipal? user = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.DownstreamApi.DownstreamApi(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! authorizationHeaderProvider, Microsoft.Extensions.Options.IOptionsMonitor! namedDownstreamApiOptions, System.Net.Http.IHttpClientFactory! httpClientFactory, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Client.IMsalHttpClientFactory? msalHttpClientFactory) -> void Microsoft.Identity.Web.DownstreamApi.DownstreamApi(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! authorizationHeaderProvider, Microsoft.Extensions.Options.IOptionsMonitor! namedDownstreamApiOptions, System.Net.Http.IHttpClientFactory! httpClientFactory, Microsoft.Extensions.Logging.ILogger! logger) -> void Microsoft.Identity.Web.DownstreamApi.GetForAppAsync(string? serviceName, TInput input, System.Action? downstreamApiOptionsOverride = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DownstreamApi.GetForAppAsync(string? serviceName, TInput input, System.Text.Json.Serialization.Metadata.JsonTypeInfo! inputJsonTypeInfo, System.Text.Json.Serialization.Metadata.JsonTypeInfo! outputJsonTypeInfo, System.Action? downstreamApiOptionsOverride = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! @@ -58,15 +59,20 @@ Microsoft.Identity.Web.DownstreamApi.PutForUserAsync(string? se Microsoft.Identity.Web.DownstreamApi.PutForUserAsync(string? serviceName, TInput input, System.Action? downstreamApiOptionsOverride = null, System.Security.Claims.ClaimsPrincipal? user = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DownstreamApi.PutForUserAsync(string? serviceName, TInput input, System.Text.Json.Serialization.Metadata.JsonTypeInfo! inputJsonTypeInfo, System.Action? downstreamApiOptionsOverride = null, System.Security.Claims.ClaimsPrincipal? user = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DownstreamApi.UpdateRequestAsync(System.Net.Http.HttpRequestMessage! httpRequestMessage, System.Net.Http.HttpContent? content, Microsoft.Identity.Abstractions.DownstreamApiOptions! effectiveOptions, bool appToken, System.Security.Claims.ClaimsPrincipal? user, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.DownstreamApi.UpdateRequestAsync(System.Net.Http.HttpRequestMessage! httpRequestMessage, System.Net.Http.HttpContent? content, Microsoft.Identity.Abstractions.DownstreamApiOptions! effectiveOptions, bool appToken, System.Security.Claims.ClaimsPrincipal? user, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DownstreamApiLoggingEventId readonly Microsoft.Identity.Web.DownstreamApi._logger -> Microsoft.Extensions.Logging.ILogger! static Microsoft.Identity.Web.DownstreamApi.CallerSDKDetails.get -> System.Collections.Generic.Dictionary! -static Microsoft.Identity.Web.DownstreamApi.DeserializeOutputAsync(System.Net.Http.HttpResponseMessage! response, Microsoft.Identity.Abstractions.DownstreamApiOptions! effectiveOptions) -> System.Threading.Tasks.Task! +static Microsoft.Identity.Web.DownstreamApi.DeserializeOutputAsync(System.Net.Http.HttpResponseMessage! response, Microsoft.Identity.Abstractions.DownstreamApiOptions! effectiveOptions, System.Text.Json.Serialization.Metadata.JsonTypeInfo! outputJsonTypeInfo, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! static Microsoft.Identity.Web.DownstreamApi.DeserializeOutputAsync(System.Net.Http.HttpResponseMessage! response, Microsoft.Identity.Abstractions.DownstreamApiOptions! effectiveOptions, System.Text.Json.Serialization.Metadata.JsonTypeInfo! outputJsonTypeInfo) -> System.Threading.Tasks.Task! +static Microsoft.Identity.Web.DownstreamApi.DeserializeOutputAsync(System.Net.Http.HttpResponseMessage! response, Microsoft.Identity.Abstractions.DownstreamApiOptions! effectiveOptions, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +static Microsoft.Identity.Web.DownstreamApi.DeserializeOutputAsync(System.Net.Http.HttpResponseMessage! response, Microsoft.Identity.Abstractions.DownstreamApiOptions! effectiveOptions) -> System.Threading.Tasks.Task! +static Microsoft.Identity.Web.DownstreamApi.Logger.HttpRequestError(Microsoft.Extensions.Logging.ILogger! logger, string! ServiceName, string! BaseUrl, string! RelativePath, int statusCode, string! responseContent, System.Exception? ex) -> void static Microsoft.Identity.Web.DownstreamApi.Logger.HttpRequestError(Microsoft.Extensions.Logging.ILogger! logger, string! ServiceName, string! BaseUrl, string! RelativePath, System.Exception? ex) -> void static Microsoft.Identity.Web.DownstreamApi.Logger.UnauthenticatedApiCall(Microsoft.Extensions.Logging.ILogger! logger, System.Exception? ex) -> void -static Microsoft.Identity.Web.DownstreamApi.SerializeInput(TInput input, Microsoft.Identity.Abstractions.DownstreamApiOptions! effectiveOptions) -> System.Net.Http.HttpContent? +static Microsoft.Identity.Web.DownstreamApi.ReadErrorResponseContentAsync(System.Net.Http.HttpResponseMessage! response, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! static Microsoft.Identity.Web.DownstreamApi.SerializeInput(TInput input, Microsoft.Identity.Abstractions.DownstreamApiOptions! effectiveOptions, System.Text.Json.Serialization.Metadata.JsonTypeInfo! inputJsonTypeInfo) -> System.Net.Http.HttpContent? +static Microsoft.Identity.Web.DownstreamApi.SerializeInput(TInput input, Microsoft.Identity.Abstractions.DownstreamApiOptions! effectiveOptions) -> System.Net.Http.HttpContent? static Microsoft.Identity.Web.DownstreamApiExtensions.AddDownstreamApiWithLifetime(Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.DependencyInjection.ServiceLifetime lifetime) -> void static Microsoft.Identity.Web.DownstreamApiExtensions.RegisterDownstreamApi(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> void static readonly Microsoft.Identity.Web.DownstreamApiLoggingEventId.HttpRequestError -> Microsoft.Extensions.Logging.EventId diff --git a/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net10.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net10.0/InternalAPI.Unshipped.txt index 40d750c56..7dc5c5811 100644 --- a/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net10.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net10.0/InternalAPI.Unshipped.txt @@ -1,5 +1 @@ #nullable enable -static Microsoft.Identity.Web.DownstreamApi.DeserializeOutputAsync(System.Net.Http.HttpResponseMessage! response, Microsoft.Identity.Abstractions.DownstreamApiOptions! effectiveOptions, System.Text.Json.Serialization.Metadata.JsonTypeInfo! outputJsonTypeInfo, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -static Microsoft.Identity.Web.DownstreamApi.DeserializeOutputAsync(System.Net.Http.HttpResponseMessage! response, Microsoft.Identity.Abstractions.DownstreamApiOptions! effectiveOptions, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! -static Microsoft.Identity.Web.DownstreamApi.Logger.HttpRequestError(Microsoft.Extensions.Logging.ILogger! logger, string! ServiceName, string! BaseUrl, string! RelativePath, int statusCode, string! responseContent, System.Exception? ex) -> void -static Microsoft.Identity.Web.DownstreamApi.ReadErrorResponseContentAsync(System.Net.Http.HttpResponseMessage! response, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! diff --git a/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net462/InternalAPI.Shipped.txt b/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net462/InternalAPI.Shipped.txt index 134e27be0..2dd4c1a9e 100644 --- a/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net462/InternalAPI.Shipped.txt +++ b/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net462/InternalAPI.Shipped.txt @@ -13,6 +13,7 @@ Microsoft.Identity.Web.DownstreamApi.DeleteForAppAsync(string? Microsoft.Identity.Web.DownstreamApi.DeleteForAppAsync(string? serviceName, TInput input, System.Action? downstreamApiOptionsOverride = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DownstreamApi.DeleteForUserAsync(string? serviceName, TInput input, System.Action? downstreamApiOptionsOverride = null, System.Security.Claims.ClaimsPrincipal? user = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DownstreamApi.DeleteForUserAsync(string? serviceName, TInput input, System.Action? downstreamApiOptionsOverride = null, System.Security.Claims.ClaimsPrincipal? user = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.DownstreamApi.DownstreamApi(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! authorizationHeaderProvider, Microsoft.Extensions.Options.IOptionsMonitor! namedDownstreamApiOptions, System.Net.Http.IHttpClientFactory! httpClientFactory, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Client.IMsalHttpClientFactory? msalHttpClientFactory) -> void Microsoft.Identity.Web.DownstreamApi.DownstreamApi(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! authorizationHeaderProvider, Microsoft.Extensions.Options.IOptionsMonitor! namedDownstreamApiOptions, System.Net.Http.IHttpClientFactory! httpClientFactory, Microsoft.Extensions.Logging.ILogger! logger) -> void Microsoft.Identity.Web.DownstreamApi.GetForAppAsync(string? serviceName, TInput input, System.Action? downstreamApiOptionsOverride = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DownstreamApi.GetForAppAsync(string? serviceName, System.Action? downstreamApiOptionsOverride = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! @@ -30,6 +31,7 @@ Microsoft.Identity.Web.DownstreamApi.PutForAppAsync(string? serviceName, Microsoft.Identity.Web.DownstreamApi.PutForUserAsync(string? serviceName, TInput input, System.Action? downstreamApiOptionsOverride = null, System.Security.Claims.ClaimsPrincipal? user = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DownstreamApi.PutForUserAsync(string? serviceName, TInput input, System.Action? downstreamApiOptionsOverride = null, System.Security.Claims.ClaimsPrincipal? user = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DownstreamApi.UpdateRequestAsync(System.Net.Http.HttpRequestMessage! httpRequestMessage, System.Net.Http.HttpContent? content, Microsoft.Identity.Abstractions.DownstreamApiOptions! effectiveOptions, bool appToken, System.Security.Claims.ClaimsPrincipal? user, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.DownstreamApi.UpdateRequestAsync(System.Net.Http.HttpRequestMessage! httpRequestMessage, System.Net.Http.HttpContent? content, Microsoft.Identity.Abstractions.DownstreamApiOptions! effectiveOptions, bool appToken, System.Security.Claims.ClaimsPrincipal? user, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DownstreamApiLoggingEventId readonly Microsoft.Identity.Web.DownstreamApi._logger -> Microsoft.Extensions.Logging.ILogger! static Microsoft.Identity.Web.DownstreamApi.CallerSDKDetails.get -> System.Collections.Generic.Dictionary! diff --git a/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net472/InternalAPI.Shipped.txt b/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net472/InternalAPI.Shipped.txt index 134e27be0..2dd4c1a9e 100644 --- a/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net472/InternalAPI.Shipped.txt +++ b/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net472/InternalAPI.Shipped.txt @@ -13,6 +13,7 @@ Microsoft.Identity.Web.DownstreamApi.DeleteForAppAsync(string? Microsoft.Identity.Web.DownstreamApi.DeleteForAppAsync(string? serviceName, TInput input, System.Action? downstreamApiOptionsOverride = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DownstreamApi.DeleteForUserAsync(string? serviceName, TInput input, System.Action? downstreamApiOptionsOverride = null, System.Security.Claims.ClaimsPrincipal? user = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DownstreamApi.DeleteForUserAsync(string? serviceName, TInput input, System.Action? downstreamApiOptionsOverride = null, System.Security.Claims.ClaimsPrincipal? user = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.DownstreamApi.DownstreamApi(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! authorizationHeaderProvider, Microsoft.Extensions.Options.IOptionsMonitor! namedDownstreamApiOptions, System.Net.Http.IHttpClientFactory! httpClientFactory, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Client.IMsalHttpClientFactory? msalHttpClientFactory) -> void Microsoft.Identity.Web.DownstreamApi.DownstreamApi(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! authorizationHeaderProvider, Microsoft.Extensions.Options.IOptionsMonitor! namedDownstreamApiOptions, System.Net.Http.IHttpClientFactory! httpClientFactory, Microsoft.Extensions.Logging.ILogger! logger) -> void Microsoft.Identity.Web.DownstreamApi.GetForAppAsync(string? serviceName, TInput input, System.Action? downstreamApiOptionsOverride = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DownstreamApi.GetForAppAsync(string? serviceName, System.Action? downstreamApiOptionsOverride = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! @@ -30,6 +31,7 @@ Microsoft.Identity.Web.DownstreamApi.PutForAppAsync(string? serviceName, Microsoft.Identity.Web.DownstreamApi.PutForUserAsync(string? serviceName, TInput input, System.Action? downstreamApiOptionsOverride = null, System.Security.Claims.ClaimsPrincipal? user = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DownstreamApi.PutForUserAsync(string? serviceName, TInput input, System.Action? downstreamApiOptionsOverride = null, System.Security.Claims.ClaimsPrincipal? user = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DownstreamApi.UpdateRequestAsync(System.Net.Http.HttpRequestMessage! httpRequestMessage, System.Net.Http.HttpContent? content, Microsoft.Identity.Abstractions.DownstreamApiOptions! effectiveOptions, bool appToken, System.Security.Claims.ClaimsPrincipal? user, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.DownstreamApi.UpdateRequestAsync(System.Net.Http.HttpRequestMessage! httpRequestMessage, System.Net.Http.HttpContent? content, Microsoft.Identity.Abstractions.DownstreamApiOptions! effectiveOptions, bool appToken, System.Security.Claims.ClaimsPrincipal? user, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DownstreamApiLoggingEventId readonly Microsoft.Identity.Web.DownstreamApi._logger -> Microsoft.Extensions.Logging.ILogger! static Microsoft.Identity.Web.DownstreamApi.CallerSDKDetails.get -> System.Collections.Generic.Dictionary! diff --git a/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net8.0/InternalAPI.Shipped.txt b/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net8.0/InternalAPI.Shipped.txt index 17b76b112..2c1df99c9 100644 --- a/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net8.0/InternalAPI.Shipped.txt +++ b/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net8.0/InternalAPI.Shipped.txt @@ -21,6 +21,7 @@ Microsoft.Identity.Web.DownstreamApi.DeleteForUserAsync(string? Microsoft.Identity.Web.DownstreamApi.DeleteForUserAsync(string? serviceName, TInput input, System.Text.Json.Serialization.Metadata.JsonTypeInfo! inputJsonTypeInfo, System.Text.Json.Serialization.Metadata.JsonTypeInfo! outputJsonTypeInfo, System.Action? downstreamApiOptionsOverride = null, System.Security.Claims.ClaimsPrincipal? user = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DownstreamApi.DeleteForUserAsync(string? serviceName, TInput input, System.Action? downstreamApiOptionsOverride = null, System.Security.Claims.ClaimsPrincipal? user = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DownstreamApi.DeleteForUserAsync(string? serviceName, TInput input, System.Text.Json.Serialization.Metadata.JsonTypeInfo! inputJsonTypeInfo, System.Action? downstreamApiOptionsOverride = null, System.Security.Claims.ClaimsPrincipal? user = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.DownstreamApi.DownstreamApi(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! authorizationHeaderProvider, Microsoft.Extensions.Options.IOptionsMonitor! namedDownstreamApiOptions, System.Net.Http.IHttpClientFactory! httpClientFactory, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Client.IMsalHttpClientFactory? msalHttpClientFactory) -> void Microsoft.Identity.Web.DownstreamApi.DownstreamApi(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! authorizationHeaderProvider, Microsoft.Extensions.Options.IOptionsMonitor! namedDownstreamApiOptions, System.Net.Http.IHttpClientFactory! httpClientFactory, Microsoft.Extensions.Logging.ILogger! logger) -> void Microsoft.Identity.Web.DownstreamApi.GetForAppAsync(string? serviceName, TInput input, System.Action? downstreamApiOptionsOverride = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DownstreamApi.GetForAppAsync(string? serviceName, TInput input, System.Text.Json.Serialization.Metadata.JsonTypeInfo! inputJsonTypeInfo, System.Text.Json.Serialization.Metadata.JsonTypeInfo! outputJsonTypeInfo, System.Action? downstreamApiOptionsOverride = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! @@ -58,6 +59,7 @@ Microsoft.Identity.Web.DownstreamApi.PutForUserAsync(string? se Microsoft.Identity.Web.DownstreamApi.PutForUserAsync(string? serviceName, TInput input, System.Action? downstreamApiOptionsOverride = null, System.Security.Claims.ClaimsPrincipal? user = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DownstreamApi.PutForUserAsync(string? serviceName, TInput input, System.Text.Json.Serialization.Metadata.JsonTypeInfo! inputJsonTypeInfo, System.Action? downstreamApiOptionsOverride = null, System.Security.Claims.ClaimsPrincipal? user = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DownstreamApi.UpdateRequestAsync(System.Net.Http.HttpRequestMessage! httpRequestMessage, System.Net.Http.HttpContent? content, Microsoft.Identity.Abstractions.DownstreamApiOptions! effectiveOptions, bool appToken, System.Security.Claims.ClaimsPrincipal? user, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.DownstreamApi.UpdateRequestAsync(System.Net.Http.HttpRequestMessage! httpRequestMessage, System.Net.Http.HttpContent? content, Microsoft.Identity.Abstractions.DownstreamApiOptions! effectiveOptions, bool appToken, System.Security.Claims.ClaimsPrincipal? user, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DownstreamApiLoggingEventId readonly Microsoft.Identity.Web.DownstreamApi._logger -> Microsoft.Extensions.Logging.ILogger! static Microsoft.Identity.Web.DownstreamApi.CallerSDKDetails.get -> System.Collections.Generic.Dictionary! diff --git a/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net9.0/InternalAPI.Shipped.txt b/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net9.0/InternalAPI.Shipped.txt index 17b76b112..2c1df99c9 100644 --- a/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net9.0/InternalAPI.Shipped.txt +++ b/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/net9.0/InternalAPI.Shipped.txt @@ -21,6 +21,7 @@ Microsoft.Identity.Web.DownstreamApi.DeleteForUserAsync(string? Microsoft.Identity.Web.DownstreamApi.DeleteForUserAsync(string? serviceName, TInput input, System.Text.Json.Serialization.Metadata.JsonTypeInfo! inputJsonTypeInfo, System.Text.Json.Serialization.Metadata.JsonTypeInfo! outputJsonTypeInfo, System.Action? downstreamApiOptionsOverride = null, System.Security.Claims.ClaimsPrincipal? user = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DownstreamApi.DeleteForUserAsync(string? serviceName, TInput input, System.Action? downstreamApiOptionsOverride = null, System.Security.Claims.ClaimsPrincipal? user = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DownstreamApi.DeleteForUserAsync(string? serviceName, TInput input, System.Text.Json.Serialization.Metadata.JsonTypeInfo! inputJsonTypeInfo, System.Action? downstreamApiOptionsOverride = null, System.Security.Claims.ClaimsPrincipal? user = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.DownstreamApi.DownstreamApi(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! authorizationHeaderProvider, Microsoft.Extensions.Options.IOptionsMonitor! namedDownstreamApiOptions, System.Net.Http.IHttpClientFactory! httpClientFactory, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Client.IMsalHttpClientFactory? msalHttpClientFactory) -> void Microsoft.Identity.Web.DownstreamApi.DownstreamApi(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! authorizationHeaderProvider, Microsoft.Extensions.Options.IOptionsMonitor! namedDownstreamApiOptions, System.Net.Http.IHttpClientFactory! httpClientFactory, Microsoft.Extensions.Logging.ILogger! logger) -> void Microsoft.Identity.Web.DownstreamApi.GetForAppAsync(string? serviceName, TInput input, System.Action? downstreamApiOptionsOverride = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DownstreamApi.GetForAppAsync(string? serviceName, TInput input, System.Text.Json.Serialization.Metadata.JsonTypeInfo! inputJsonTypeInfo, System.Text.Json.Serialization.Metadata.JsonTypeInfo! outputJsonTypeInfo, System.Action? downstreamApiOptionsOverride = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! @@ -58,6 +59,7 @@ Microsoft.Identity.Web.DownstreamApi.PutForUserAsync(string? se Microsoft.Identity.Web.DownstreamApi.PutForUserAsync(string? serviceName, TInput input, System.Action? downstreamApiOptionsOverride = null, System.Security.Claims.ClaimsPrincipal? user = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DownstreamApi.PutForUserAsync(string? serviceName, TInput input, System.Text.Json.Serialization.Metadata.JsonTypeInfo! inputJsonTypeInfo, System.Action? downstreamApiOptionsOverride = null, System.Security.Claims.ClaimsPrincipal? user = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DownstreamApi.UpdateRequestAsync(System.Net.Http.HttpRequestMessage! httpRequestMessage, System.Net.Http.HttpContent? content, Microsoft.Identity.Abstractions.DownstreamApiOptions! effectiveOptions, bool appToken, System.Security.Claims.ClaimsPrincipal? user, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.DownstreamApi.UpdateRequestAsync(System.Net.Http.HttpRequestMessage! httpRequestMessage, System.Net.Http.HttpContent? content, Microsoft.Identity.Abstractions.DownstreamApiOptions! effectiveOptions, bool appToken, System.Security.Claims.ClaimsPrincipal? user, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DownstreamApiLoggingEventId readonly Microsoft.Identity.Web.DownstreamApi._logger -> Microsoft.Extensions.Logging.ILogger! static Microsoft.Identity.Web.DownstreamApi.CallerSDKDetails.get -> System.Collections.Generic.Dictionary! diff --git a/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/netstandard2.0/InternalAPI.Shipped.txt b/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/netstandard2.0/InternalAPI.Shipped.txt index 134e27be0..2dd4c1a9e 100644 --- a/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/netstandard2.0/InternalAPI.Shipped.txt +++ b/src/Microsoft.Identity.Web.DownstreamApi/PublicAPI/netstandard2.0/InternalAPI.Shipped.txt @@ -13,6 +13,7 @@ Microsoft.Identity.Web.DownstreamApi.DeleteForAppAsync(string? Microsoft.Identity.Web.DownstreamApi.DeleteForAppAsync(string? serviceName, TInput input, System.Action? downstreamApiOptionsOverride = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DownstreamApi.DeleteForUserAsync(string? serviceName, TInput input, System.Action? downstreamApiOptionsOverride = null, System.Security.Claims.ClaimsPrincipal? user = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DownstreamApi.DeleteForUserAsync(string? serviceName, TInput input, System.Action? downstreamApiOptionsOverride = null, System.Security.Claims.ClaimsPrincipal? user = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.DownstreamApi.DownstreamApi(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! authorizationHeaderProvider, Microsoft.Extensions.Options.IOptionsMonitor! namedDownstreamApiOptions, System.Net.Http.IHttpClientFactory! httpClientFactory, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Client.IMsalHttpClientFactory? msalHttpClientFactory) -> void Microsoft.Identity.Web.DownstreamApi.DownstreamApi(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! authorizationHeaderProvider, Microsoft.Extensions.Options.IOptionsMonitor! namedDownstreamApiOptions, System.Net.Http.IHttpClientFactory! httpClientFactory, Microsoft.Extensions.Logging.ILogger! logger) -> void Microsoft.Identity.Web.DownstreamApi.GetForAppAsync(string? serviceName, TInput input, System.Action? downstreamApiOptionsOverride = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DownstreamApi.GetForAppAsync(string? serviceName, System.Action? downstreamApiOptionsOverride = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! @@ -30,6 +31,7 @@ Microsoft.Identity.Web.DownstreamApi.PutForAppAsync(string? serviceName, Microsoft.Identity.Web.DownstreamApi.PutForUserAsync(string? serviceName, TInput input, System.Action? downstreamApiOptionsOverride = null, System.Security.Claims.ClaimsPrincipal? user = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DownstreamApi.PutForUserAsync(string? serviceName, TInput input, System.Action? downstreamApiOptionsOverride = null, System.Security.Claims.ClaimsPrincipal? user = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DownstreamApi.UpdateRequestAsync(System.Net.Http.HttpRequestMessage! httpRequestMessage, System.Net.Http.HttpContent? content, Microsoft.Identity.Abstractions.DownstreamApiOptions! effectiveOptions, bool appToken, System.Security.Claims.ClaimsPrincipal? user, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.DownstreamApi.UpdateRequestAsync(System.Net.Http.HttpRequestMessage! httpRequestMessage, System.Net.Http.HttpContent? content, Microsoft.Identity.Abstractions.DownstreamApiOptions! effectiveOptions, bool appToken, System.Security.Claims.ClaimsPrincipal? user, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DownstreamApiLoggingEventId readonly Microsoft.Identity.Web.DownstreamApi._logger -> Microsoft.Extensions.Logging.ILogger! static Microsoft.Identity.Web.DownstreamApi.CallerSDKDetails.get -> System.Collections.Generic.Dictionary! 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/src/Microsoft.Identity.Web.GraphServiceClient/GraphServiceCollectionExtensions.cs b/src/Microsoft.Identity.Web.GraphServiceClient/GraphServiceCollectionExtensions.cs index eb22c3149..5a0da88a1 100644 --- a/src/Microsoft.Identity.Web.GraphServiceClient/GraphServiceCollectionExtensions.cs +++ b/src/Microsoft.Identity.Web.GraphServiceClient/GraphServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Net.Http; using Microsoft.Extensions.Configuration; @@ -40,6 +41,8 @@ public static IServiceCollection AddMicrosoftGraph(this IServiceCollection servi /// Builder. /// Configuration section containing the Microsoft graph config. /// The service collection to chain. + [RequiresUnreferencedCode("Calls Microsoft.Extensions.Configuration.ConfigurationBinder.Bind(IConfiguration, Object).")] + [RequiresDynamicCode("Calls Microsoft.Extensions.Configuration.ConfigurationBinder.Bind(IConfiguration, Object).")] public static IServiceCollection AddMicrosoftGraph(this IServiceCollection services, IConfiguration configurationSection) { return services.AddMicrosoftGraph(o => configurationSection.Bind(o)); diff --git a/src/Microsoft.Identity.Web.GraphServiceClient/Microsoft.Identity.Web.GraphServiceClient.csproj b/src/Microsoft.Identity.Web.GraphServiceClient/Microsoft.Identity.Web.GraphServiceClient.csproj index 596b233bd..3452a5881 100644 --- a/src/Microsoft.Identity.Web.GraphServiceClient/Microsoft.Identity.Web.GraphServiceClient.csproj +++ b/src/Microsoft.Identity.Web.GraphServiceClient/Microsoft.Identity.Web.GraphServiceClient.csproj @@ -1,5 +1,6 @@ + true Microsoft Identity Web, Microsoft Graph v5+ helper Microsoft Identity Web diff --git a/src/Microsoft.Identity.Web.GraphServiceClient/MicrosoftGraphExtensions.cs b/src/Microsoft.Identity.Web.GraphServiceClient/MicrosoftGraphExtensions.cs index 8ae7f2013..915c24249 100644 --- a/src/Microsoft.Identity.Web.GraphServiceClient/MicrosoftGraphExtensions.cs +++ b/src/Microsoft.Identity.Web.GraphServiceClient/MicrosoftGraphExtensions.cs @@ -26,9 +26,8 @@ public static class MicrosoftGraphExtensions /// Builder. /// Configuration section. /// The builder to chain. -#if NET6_0_OR_GREATER && !NET8_0_OR_GREATER - [RequiresUnreferencedCode("Microsoft.Extensions.Configuration.ConfigurationBinder.Bind(IConfiguration, Object).")] -#endif + [RequiresUnreferencedCode("Calls Microsoft.Extensions.Configuration.ConfigurationBinder.Bind(IConfiguration, Object).")] + [RequiresDynamicCode("Calls Microsoft.Extensions.Configuration.ConfigurationBinder.Bind(IConfiguration, Object).")] public static MicrosoftIdentityAppCallsWebApiAuthenticationBuilder AddMicrosoftGraph( this MicrosoftIdentityAppCallsWebApiAuthenticationBuilder builder, IConfigurationSection configurationSection) diff --git a/src/Microsoft.Identity.Web.GraphServiceClient/PublicAPI/net10.0/PublicAPI.Shipped.txt b/src/Microsoft.Identity.Web.GraphServiceClient/PublicAPI/net10.0/PublicAPI.Shipped.txt index eab501d56..1a3971297 100644 --- a/src/Microsoft.Identity.Web.GraphServiceClient/PublicAPI/net10.0/PublicAPI.Shipped.txt +++ b/src/Microsoft.Identity.Web.GraphServiceClient/PublicAPI/net10.0/PublicAPI.Shipped.txt @@ -11,9 +11,9 @@ Microsoft.Identity.Web.GraphServiceClientOptions.User.set -> void Microsoft.Identity.Web.GraphServiceCollectionExtensions Microsoft.Identity.Web.MicrosoftGraphExtensions Microsoft.Identity.Web.RequestOptionsExtension -static Microsoft.Identity.Web.GraphServiceCollectionExtensions.AddMicrosoftGraph(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.Identity.Web.GraphServiceCollectionExtensions.AddMicrosoftGraph(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfiguration! configurationSection) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.Identity.Web.GraphServiceCollectionExtensions.AddMicrosoftGraph(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureMicrosoftGraphOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Identity.Web.GraphServiceCollectionExtensions.AddMicrosoftGraph(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.Identity.Web.MicrosoftGraphExtensions.AddMicrosoftGraph(this Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder! builder, Microsoft.Extensions.Configuration.IConfigurationSection! configurationSection) -> Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder! static Microsoft.Identity.Web.MicrosoftGraphExtensions.AddMicrosoftGraph(this Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder! builder, string! graphBaseUrl = "https://graph.microsoft.com/v1.0", System.Collections.Generic.IEnumerable? defaultScopes = null) -> Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder! static Microsoft.Identity.Web.MicrosoftGraphExtensions.AddMicrosoftGraph(this Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder! builder, System.Action! configureMicrosoftGraphOptions) -> Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder! diff --git a/src/Microsoft.Identity.Web.GraphServiceClientBeta/GraphBetaServiceCollectionExtensions.cs b/src/Microsoft.Identity.Web.GraphServiceClientBeta/GraphBetaServiceCollectionExtensions.cs index 0a5abcdbd..11f70a65f 100644 --- a/src/Microsoft.Identity.Web.GraphServiceClientBeta/GraphBetaServiceCollectionExtensions.cs +++ b/src/Microsoft.Identity.Web.GraphServiceClientBeta/GraphBetaServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Net.Http; using Microsoft.Extensions.Configuration; @@ -40,6 +41,8 @@ public static IServiceCollection AddMicrosoftGraphBeta(this IServiceCollection s /// Builder. /// Configuration section containing the Microsoft graph config. /// The service collection to chain. + [RequiresUnreferencedCode("Calls Microsoft.Extensions.Configuration.ConfigurationBinder.Bind(IConfiguration, Object).")] + [RequiresDynamicCode("Calls Microsoft.Extensions.Configuration.ConfigurationBinder.Bind(IConfiguration, Object).")] public static IServiceCollection AddMicrosoftGraphBeta(this IServiceCollection services, IConfiguration configurationSection) { return services.AddMicrosoftGraphBeta(o => configurationSection.Bind(o)); diff --git a/src/Microsoft.Identity.Web.GraphServiceClientBeta/Microsoft.Identity.Web.GraphServiceClientBeta.csproj b/src/Microsoft.Identity.Web.GraphServiceClientBeta/Microsoft.Identity.Web.GraphServiceClientBeta.csproj index 5dd4c9890..bd398e82c 100644 --- a/src/Microsoft.Identity.Web.GraphServiceClientBeta/Microsoft.Identity.Web.GraphServiceClientBeta.csproj +++ b/src/Microsoft.Identity.Web.GraphServiceClientBeta/Microsoft.Identity.Web.GraphServiceClientBeta.csproj @@ -1,5 +1,6 @@ + true Microsoft Identity Web, Microsoft Graph v5+ helper Microsoft Identity Web diff --git a/src/Microsoft.Identity.Web.GraphServiceClientBeta/PublicAPI/net10.0/PublicAPI.Shipped.txt b/src/Microsoft.Identity.Web.GraphServiceClientBeta/PublicAPI/net10.0/PublicAPI.Shipped.txt index d1e2360f3..b3a6ce656 100644 --- a/src/Microsoft.Identity.Web.GraphServiceClientBeta/PublicAPI/net10.0/PublicAPI.Shipped.txt +++ b/src/Microsoft.Identity.Web.GraphServiceClientBeta/PublicAPI/net10.0/PublicAPI.Shipped.txt @@ -1,5 +1,5 @@ #nullable enable Microsoft.Identity.Web.GraphBetaServiceCollectionExtensions -static Microsoft.Identity.Web.GraphBetaServiceCollectionExtensions.AddMicrosoftGraphBeta(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.Identity.Web.GraphBetaServiceCollectionExtensions.AddMicrosoftGraphBeta(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfiguration! configurationSection) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.Identity.Web.GraphBetaServiceCollectionExtensions.AddMicrosoftGraphBeta(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureMicrosoftGraphOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Identity.Web.GraphBetaServiceCollectionExtensions.AddMicrosoftGraphBeta(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Microsoft.Identity.Web.MicrosoftGraph/PublicAPI/net10.0/PublicAPI.Shipped.txt b/src/Microsoft.Identity.Web.MicrosoftGraph/PublicAPI/net10.0/PublicAPI.Shipped.txt index 24f4758da..402f0f049 100644 --- a/src/Microsoft.Identity.Web.MicrosoftGraph/PublicAPI/net10.0/PublicAPI.Shipped.txt +++ b/src/Microsoft.Identity.Web.MicrosoftGraph/PublicAPI/net10.0/PublicAPI.Shipped.txt @@ -13,8 +13,8 @@ static Microsoft.Identity.Web.BaseRequestExtensions.WithAuthenticationOptions static Microsoft.Identity.Web.BaseRequestExtensions.WithAuthenticationScheme(this T baseRequest, string! authenticationScheme) -> T static Microsoft.Identity.Web.BaseRequestExtensions.WithScopes(this T baseRequest, params string![]! scopes) -> T static Microsoft.Identity.Web.BaseRequestExtensions.WithUser(this T baseRequest, System.Security.Claims.ClaimsPrincipal! user) -> T -static Microsoft.Identity.Web.GraphServiceCollectionExtensions.AddMicrosoftGraph(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.Identity.Web.GraphServiceCollectionExtensions.AddMicrosoftGraph(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureMicrosoftGraphOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Identity.Web.GraphServiceCollectionExtensions.AddMicrosoftGraph(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.Identity.Web.MicrosoftGraphExtensions.AddMicrosoftGraph(this Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder! builder, Microsoft.Extensions.Configuration.IConfigurationSection! configurationSection) -> Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder! static Microsoft.Identity.Web.MicrosoftGraphExtensions.AddMicrosoftGraph(this Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder! builder, string! graphBaseUrl = "https://graph.microsoft.com/v1.0", string! defaultScopes = "user.read") -> Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder! static Microsoft.Identity.Web.MicrosoftGraphExtensions.AddMicrosoftGraph(this Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder! builder, System.Action! configureMicrosoftGraphOptions) -> Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder! diff --git a/src/Microsoft.Identity.Web.MicrosoftGraphBeta/PublicAPI/net10.0/PublicAPI.Shipped.txt b/src/Microsoft.Identity.Web.MicrosoftGraphBeta/PublicAPI/net10.0/PublicAPI.Shipped.txt index 24f4758da..402f0f049 100644 --- a/src/Microsoft.Identity.Web.MicrosoftGraphBeta/PublicAPI/net10.0/PublicAPI.Shipped.txt +++ b/src/Microsoft.Identity.Web.MicrosoftGraphBeta/PublicAPI/net10.0/PublicAPI.Shipped.txt @@ -13,8 +13,8 @@ static Microsoft.Identity.Web.BaseRequestExtensions.WithAuthenticationOptions static Microsoft.Identity.Web.BaseRequestExtensions.WithAuthenticationScheme(this T baseRequest, string! authenticationScheme) -> T static Microsoft.Identity.Web.BaseRequestExtensions.WithScopes(this T baseRequest, params string![]! scopes) -> T static Microsoft.Identity.Web.BaseRequestExtensions.WithUser(this T baseRequest, System.Security.Claims.ClaimsPrincipal! user) -> T -static Microsoft.Identity.Web.GraphServiceCollectionExtensions.AddMicrosoftGraph(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.Identity.Web.GraphServiceCollectionExtensions.AddMicrosoftGraph(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureMicrosoftGraphOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Identity.Web.GraphServiceCollectionExtensions.AddMicrosoftGraph(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.Identity.Web.MicrosoftGraphExtensions.AddMicrosoftGraph(this Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder! builder, Microsoft.Extensions.Configuration.IConfigurationSection! configurationSection) -> Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder! static Microsoft.Identity.Web.MicrosoftGraphExtensions.AddMicrosoftGraph(this Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder! builder, string! graphBaseUrl = "https://graph.microsoft.com/v1.0", string! defaultScopes = "user.read") -> Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder! static Microsoft.Identity.Web.MicrosoftGraphExtensions.AddMicrosoftGraph(this Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder! builder, System.Action! configureMicrosoftGraphOptions) -> Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder! diff --git a/src/Microsoft.Identity.Web.OidcFIC/OidcIdpSignedAssertionProvider.cs b/src/Microsoft.Identity.Web.OidcFIC/OidcIdpSignedAssertionProvider.cs index 56286193b..ba37998dc 100644 --- a/src/Microsoft.Identity.Web.OidcFIC/OidcIdpSignedAssertionProvider.cs +++ b/src/Microsoft.Identity.Web.OidcFIC/OidcIdpSignedAssertionProvider.cs @@ -55,9 +55,17 @@ protected override async Task GetClientAssertionAsync(Assertion if (assertionRequestOptions != null && !string.IsNullOrEmpty(assertionRequestOptions.ClientAssertionFmiPath)) { + // Extract tenant from TokenEndpoint if available and if it's from the same cloud instance. + // This enables tenant override propagation while preserving cross-cloud scenarios. + // TokenEndpoint format: https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token + string? tenant = ExtractTenantFromTokenEndpointIfSameInstance( + assertionRequestOptions.TokenEndpoint, + _options.Instance); + acquireTokenOptions = new AcquireTokenOptions() { - FmiPath = assertionRequestOptions.ClientAssertionFmiPath + FmiPath = assertionRequestOptions.ClientAssertionFmiPath, + Tenant = tenant }; } @@ -83,5 +91,60 @@ protected override async Task GetClientAssertionAsync(Assertion } return clientAssertion; } + + /// + /// Extracts the tenant from a token endpoint URL if the endpoint is from the same cloud instance. + /// This enables tenant override propagation while preserving cross-cloud scenarios. + /// + /// Token endpoint URL in the format https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token + /// The configured instance URL (e.g., https://login.microsoftonline.com/) + /// The tenant ID if the endpoint is from the same instance, otherwise null. + internal static string? ExtractTenantFromTokenEndpointIfSameInstance(string? tokenEndpoint, string? configuredInstance) + { + if (string.IsNullOrEmpty(tokenEndpoint) || string.IsNullOrEmpty(configuredInstance)) + { + return null; + } + + try + { + var endpointUri = new Uri(tokenEndpoint!); + + // Safely construct instance URI by trimming trailing slash + var normalizedInstance = configuredInstance!.TrimEnd('/'); + var instanceUri = new Uri(normalizedInstance); + + // Only extract tenant if the host matches (same cloud instance) + if (!string.Equals(endpointUri.Host, instanceUri.Host, StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + // TokenEndpoint format: https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token + // Validate the path follows the expected pattern before extracting tenant. + var pathSegments = endpointUri.AbsolutePath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + + // Expected pattern: [tenantId, oauth2, v2.0, token] or similar + // We need at least the tenant segment and some oauth2 path segments + if (pathSegments.Length >= 2) + { + // Verify this looks like a token endpoint (contains "oauth2" somewhere after tenant) + for (int i = 1; i < pathSegments.Length; i++) + { + if (string.Equals(pathSegments[i], "oauth2", StringComparison.OrdinalIgnoreCase)) + { + // Found oauth2 segment, the first segment is likely the tenant + return pathSegments[0]; + } + } + } + } + catch (UriFormatException) + { + // Invalid URI, return null + } + + return null; + } } } diff --git a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net10.0/InternalAPI.Shipped.txt b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net10.0/InternalAPI.Shipped.txt index 0d6497753..0787ef54a 100644 --- a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net10.0/InternalAPI.Shipped.txt +++ b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net10.0/InternalAPI.Shipped.txt @@ -21,3 +21,4 @@ static Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.Logger.Config static Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.Logger.ConfigurationSectionNull(Microsoft.Extensions.Logging.ILogger! logger) -> void static Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.Logger.CustomSignedAssertionProviderDataNull(Microsoft.Extensions.Logging.ILogger! logger) -> void static Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.Logger.SignedAssertionProviderFailed(Microsoft.Extensions.Logging.ILogger! logger, string! providerName, string! message) -> void +static Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionProvider.ExtractTenantFromTokenEndpointIfSameInstance(string? tokenEndpoint, string? configuredInstance) -> string? diff --git a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net462/InternalAPI.Shipped.txt b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net462/InternalAPI.Shipped.txt index 0d6497753..0787ef54a 100644 --- a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net462/InternalAPI.Shipped.txt +++ b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net462/InternalAPI.Shipped.txt @@ -21,3 +21,4 @@ static Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.Logger.Config static Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.Logger.ConfigurationSectionNull(Microsoft.Extensions.Logging.ILogger! logger) -> void static Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.Logger.CustomSignedAssertionProviderDataNull(Microsoft.Extensions.Logging.ILogger! logger) -> void static Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.Logger.SignedAssertionProviderFailed(Microsoft.Extensions.Logging.ILogger! logger, string! providerName, string! message) -> void +static Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionProvider.ExtractTenantFromTokenEndpointIfSameInstance(string? tokenEndpoint, string? configuredInstance) -> string? diff --git a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net472/InternalAPI.Shipped.txt b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net472/InternalAPI.Shipped.txt index 0d6497753..0787ef54a 100644 --- a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net472/InternalAPI.Shipped.txt +++ b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net472/InternalAPI.Shipped.txt @@ -21,3 +21,4 @@ static Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.Logger.Config static Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.Logger.ConfigurationSectionNull(Microsoft.Extensions.Logging.ILogger! logger) -> void static Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.Logger.CustomSignedAssertionProviderDataNull(Microsoft.Extensions.Logging.ILogger! logger) -> void static Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.Logger.SignedAssertionProviderFailed(Microsoft.Extensions.Logging.ILogger! logger, string! providerName, string! message) -> void +static Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionProvider.ExtractTenantFromTokenEndpointIfSameInstance(string? tokenEndpoint, string? configuredInstance) -> string? diff --git a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net8.0/InternalAPI.Shipped.txt b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net8.0/InternalAPI.Shipped.txt index 0d6497753..0787ef54a 100644 --- a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net8.0/InternalAPI.Shipped.txt +++ b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net8.0/InternalAPI.Shipped.txt @@ -21,3 +21,4 @@ static Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.Logger.Config static Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.Logger.ConfigurationSectionNull(Microsoft.Extensions.Logging.ILogger! logger) -> void static Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.Logger.CustomSignedAssertionProviderDataNull(Microsoft.Extensions.Logging.ILogger! logger) -> void static Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.Logger.SignedAssertionProviderFailed(Microsoft.Extensions.Logging.ILogger! logger, string! providerName, string! message) -> void +static Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionProvider.ExtractTenantFromTokenEndpointIfSameInstance(string? tokenEndpoint, string? configuredInstance) -> string? diff --git a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net9.0/InternalAPI.Shipped.txt b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net9.0/InternalAPI.Shipped.txt index 0d6497753..0787ef54a 100644 --- a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net9.0/InternalAPI.Shipped.txt +++ b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/net9.0/InternalAPI.Shipped.txt @@ -21,3 +21,4 @@ static Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.Logger.Config static Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.Logger.ConfigurationSectionNull(Microsoft.Extensions.Logging.ILogger! logger) -> void static Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.Logger.CustomSignedAssertionProviderDataNull(Microsoft.Extensions.Logging.ILogger! logger) -> void static Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.Logger.SignedAssertionProviderFailed(Microsoft.Extensions.Logging.ILogger! logger, string! providerName, string! message) -> void +static Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionProvider.ExtractTenantFromTokenEndpointIfSameInstance(string? tokenEndpoint, string? configuredInstance) -> string? diff --git a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/netstandard2.0/InternalAPI.Shipped.txt b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/netstandard2.0/InternalAPI.Shipped.txt index 0d6497753..0787ef54a 100644 --- a/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/netstandard2.0/InternalAPI.Shipped.txt +++ b/src/Microsoft.Identity.Web.OidcFIC/PublicAPI/netstandard2.0/InternalAPI.Shipped.txt @@ -21,3 +21,4 @@ static Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.Logger.Config static Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.Logger.ConfigurationSectionNull(Microsoft.Extensions.Logging.ILogger! logger) -> void static Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.Logger.CustomSignedAssertionProviderDataNull(Microsoft.Extensions.Logging.ILogger! logger) -> void static Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionLoader.Logger.SignedAssertionProviderFailed(Microsoft.Extensions.Logging.ILogger! logger, string! providerName, string! message) -> void +static Microsoft.Identity.Web.OidcFic.OidcIdpSignedAssertionProvider.ExtractTenantFromTokenEndpointIfSameInstance(string? tokenEndpoint, string? configuredInstance) -> string? diff --git a/src/Microsoft.Identity.Web.Sidecar/DownstreamApiOptionsMerger.cs b/src/Microsoft.Identity.Web.Sidecar/DownstreamApiOptionsMerger.cs index 97e3c1423..7251f2272 100644 --- a/src/Microsoft.Identity.Web.Sidecar/DownstreamApiOptionsMerger.cs +++ b/src/Microsoft.Identity.Web.Sidecar/DownstreamApiOptionsMerger.cs @@ -21,6 +21,37 @@ public static DownstreamApiOptions MergeOptions(DownstreamApiOptions left, Downs res.Scopes = right.Scopes; } + // RequestAppToken determines whether to use client credentials (app token) or user delegation (OBO) + if (right.RequestAppToken) + { + res.RequestAppToken = right.RequestAppToken; + } + + if (!string.IsNullOrEmpty(right.BaseUrl)) + { + res.BaseUrl = right.BaseUrl; + } + + if (!string.IsNullOrEmpty(right.RelativePath)) + { + res.RelativePath = right.RelativePath; + } + + if (!string.IsNullOrEmpty(right.HttpMethod)) + { + res.HttpMethod = right.HttpMethod; + } + + if (!string.IsNullOrEmpty(right.ContentType)) + { + res.ContentType = right.ContentType; + } + + if (!string.IsNullOrEmpty(right.AcceptHeader)) + { + res.AcceptHeader = right.AcceptHeader; + } + if (!string.IsNullOrEmpty(right.AcquireTokenOptions.Tenant)) { res.AcquireTokenOptions.Tenant = right.AcquireTokenOptions.Tenant; @@ -41,9 +72,29 @@ public static DownstreamApiOptions MergeOptions(DownstreamApiOptions left, Downs res.AcquireTokenOptions.FmiPath = right.AcquireTokenOptions.FmiPath; } - if (!string.IsNullOrEmpty(right.RelativePath)) + if (!string.IsNullOrEmpty(right.AcquireTokenOptions.LongRunningWebApiSessionKey)) { - res.RelativePath = right.RelativePath; + res.AcquireTokenOptions.LongRunningWebApiSessionKey = right.AcquireTokenOptions.LongRunningWebApiSessionKey; + } + + if (!string.IsNullOrEmpty(right.AcquireTokenOptions.PopPublicKey)) + { + res.AcquireTokenOptions.PopPublicKey = right.AcquireTokenOptions.PopPublicKey; + } + + if (!string.IsNullOrEmpty(right.AcquireTokenOptions.PopClaim)) + { + res.AcquireTokenOptions.PopClaim = right.AcquireTokenOptions.PopClaim; + } + + if (right.AcquireTokenOptions.CorrelationId != Guid.Empty) + { + res.AcquireTokenOptions.CorrelationId = right.AcquireTokenOptions.CorrelationId; + } + + if (right.AcquireTokenOptions.ManagedIdentity is not null) + { + res.AcquireTokenOptions.ManagedIdentity = right.AcquireTokenOptions.ManagedIdentity; } res.AcquireTokenOptions.ForceRefresh = right.AcquireTokenOptions.ForceRefresh; diff --git a/src/Microsoft.Identity.Web.Sidecar/Microsoft.Identity.Web.Sidecar.http b/src/Microsoft.Identity.Web.Sidecar/Microsoft.Identity.Web.Sidecar.http index e71dab7b0..a3f599538 100644 --- a/src/Microsoft.Identity.Web.Sidecar/Microsoft.Identity.Web.Sidecar.http +++ b/src/Microsoft.Identity.Web.Sidecar/Microsoft.Identity.Web.Sidecar.http @@ -1,7 +1,7 @@ @Microsoft.Identity.Web.Sidecar_HostAddress = http://localhost:5178 @AccessToken = -@ApiName = +@ApiName = ### @@ -23,7 +23,7 @@ Get {{Microsoft.Identity.Web.Sidecar_HostAddress}}/AuthorizationHeaderUnauthenti ### -GET {{Microsoft.Identity.Web.Sidecar_HostAddress}}/AuthorizationHeader/{{ApiName}}?optionsOverride.Scopes=User.Read&optionsOverride.AcquireTokenOptions.Tenant=f645ad92-e38d-4d1a-b510-d1b09a74a8ca +GET {{Microsoft.Identity.Web.Sidecar_HostAddress}}/AuthorizationHeader/{{ApiName}}?optionsOverride.Scopes=User.Read&optionsOverride.AcquireTokenOptions.Tenant=10c419d4-4a50-45b2-aa4e-919fb84df24f Authorization: Bearer {{AccessToken}} ### diff --git a/src/Microsoft.Identity.Web.Sidecar/Program.cs b/src/Microsoft.Identity.Web.Sidecar/Program.cs index c230e5584..2fec2e85e 100644 --- a/src/Microsoft.Identity.Web.Sidecar/Program.cs +++ b/src/Microsoft.Identity.Web.Sidecar/Program.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System.Diagnostics.CodeAnalysis; using System.IdentityModel.Tokens.Jwt; using System.Security.Cryptography.X509Certificates; using Microsoft.AspNetCore.Authentication.JwtBearer; @@ -12,6 +13,10 @@ namespace Microsoft.Identity.Web.Sidecar; public class Program { + + // Adding these the time to merge Andy's PR. Then will do the work to remove reflexion usage + [RequiresUnreferencedCode("EnableTokenAcquisitionToCallDownstreamApis uses reflection")] + [RequiresDynamicCode("EnableTokenAcquisitionToCallDownstreamApis uses reflection")] public static void Main(string[] args) { var builder = WebApplication.CreateSlimBuilder(args); diff --git a/src/Microsoft.Identity.Web.Sidecar/appsettings.json b/src/Microsoft.Identity.Web.Sidecar/appsettings.json index 1a0c82932..ed24c71e2 100644 --- a/src/Microsoft.Identity.Web.Sidecar/appsettings.json +++ b/src/Microsoft.Identity.Web.Sidecar/appsettings.json @@ -7,8 +7,8 @@ */ "AzureAd": { "Instance": "https://login.microsoftonline.com/", - "TenantId": "", // f645ad92-e38d-4d1a-b510-d1b09a74a8ca - "ClientId": "", //"556d438d-2f4b-4add-9713-ede4e5f5d7da" + "TenantId": "", // 10c419d4-4a50-45b2-aa4e-919fb84df24f + "ClientId": "", //"a021aff4-57ad-453a-bae8-e4192e5860f3" // "Scopes": "" // access_as_user diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/AspNetCore/ApplicationBuilderExtensions.cs b/src/Microsoft.Identity.Web.TokenAcquisition/AspNetCore/ApplicationBuilderExtensions.cs index bc6cf6145..a52054cc2 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/AspNetCore/ApplicationBuilderExtensions.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/AspNetCore/ApplicationBuilderExtensions.cs @@ -10,9 +10,8 @@ namespace Microsoft.Identity.Web /// Extension class on IApplicationBuilder to initialize the service provider of /// the TokenAcquirerFactory in ASP.NET Core. /// -#if NET6_0_OR_GREATER [RequiresUnreferencedCode("Microsoft.Identity.Web.TokenAcquirerFactory.GetDefaultInstance(String).")] -#endif + [RequiresDynamicCode("Microsoft.Identity.Web.TokenAcquirerFactory.GetDefaultInstance(String).")] public static class ApplicationBuilderExtensions { /// diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/AspNetCore/TokenAcquisition-AspnetCore.cs b/src/Microsoft.Identity.Web.TokenAcquisition/AspNetCore/TokenAcquisition-AspnetCore.cs index f1605bea7..a8a2d868b 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/AspNetCore/TokenAcquisition-AspnetCore.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/AspNetCore/TokenAcquisition-AspnetCore.cs @@ -109,7 +109,7 @@ private async Task ReplyForbiddenWithWwwAuthenticateHeaderAsync( MergedOptions mergedOptions = _tokenAcquisitionHost.GetOptions(authenticationScheme, out _); - var application = await GetOrBuildConfidentialClientApplicationAsync(mergedOptions); + var application = await GetOrBuildConfidentialClientApplicationAsync(mergedOptions, isTokenBinding: false); string consentUrl = $"{application.Authority}/oauth2/v2.0/authorize?client_id={mergedOptions.ClientId}" + $"&response_type=code&redirect_uri={application.AppConfig.RedirectUri}" diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/ConfidentialClientApplicationBuilderExtension.Logger.cs b/src/Microsoft.Identity.Web.TokenAcquisition/ConfidentialClientApplicationBuilderExtension.Logger.cs index 8352fd046..9b954ac61 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/ConfidentialClientApplicationBuilderExtension.Logger.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/ConfidentialClientApplicationBuilderExtension.Logger.cs @@ -67,13 +67,15 @@ internal static class Logger /// public static void AttemptToLoadCredentialsFailed( ILogger logger, - CredentialDescription certificateDescription, - Exception ex) => - s_credentialAttemptFailed( + CredentialDescription certificateDescription, + Exception ex) + { + s_credentialAttemptFailed( logger, certificateDescription.Id, certificateDescription.Skip.ToString(), ex); + } /// /// Logger for attempting to use a CredentialDescription with MSAL @@ -82,12 +84,14 @@ public static void AttemptToLoadCredentialsFailed( /// public static void AttemptToLoadCredentials( ILogger logger, - CredentialDescription certificateDescription) => - s_credentialAttempt( - logger, - certificateDescription.Id, - certificateDescription.Skip.ToString(), + CredentialDescription certificateDescription) + { + s_credentialAttempt( + logger, + certificateDescription.Id, + certificateDescription.Skip.ToString(), default!); + } /// /// Logger for attempting to use a CredentialDescription with MSAL @@ -96,12 +100,14 @@ public static void AttemptToLoadCredentials( /// public static void FailedToLoadCredentials( ILogger logger, - CredentialDescription certificateDescription) => - s_credentialAttemptFailed( + CredentialDescription certificateDescription) + { + s_credentialAttemptFailed( logger, certificateDescription.Id, certificateDescription.Skip.ToString(), default!); + } /// /// Logger for handling information specific to ConfidentialClientApplicationBuilderExtension. @@ -110,14 +116,20 @@ public static void FailedToLoadCredentials( /// Exception message. public static void NotUsingManagedIdentity( ILogger logger, - string message) => s_notManagedIdentity(logger, message, default!); + string message) + { + s_notManagedIdentity(logger, message, default!); + } /// /// Logger for handling information specific to ConfidentialClientApplicationBuilderExtension. /// /// ILogger. public static void UsingManagedIdentity( - ILogger logger) => s_usingManagedIdentity(logger, default!); + ILogger logger) + { + s_usingManagedIdentity(logger, default!); + } /// /// Logger for handling information specific to ConfidentialClientApplicationBuilderExtension. @@ -126,7 +138,10 @@ public static void UsingManagedIdentity( /// public static void UsingPodIdentityFile( ILogger logger, - string signedAssertionFileDiskPath) => s_usingPodIdentityFile(logger, signedAssertionFileDiskPath, default!); + string signedAssertionFileDiskPath) + { + s_usingPodIdentityFile(logger, signedAssertionFileDiskPath, default!); + } /// /// Logger for handling information specific to ConfidentialClientApplicationBuilderExtension. @@ -135,7 +150,10 @@ public static void UsingPodIdentityFile( /// public static void UsingSignedAssertionFromVault( ILogger logger, - string signedAssertionUri) => s_usingSignedAssertionFromVault(logger, signedAssertionUri, default!); + string signedAssertionUri) + { + s_usingSignedAssertionFromVault(logger, signedAssertionUri, default!); + } /// /// Logger for handling information specific to ConfidentialClientApplicationBuilderExtension. @@ -144,7 +162,10 @@ public static void UsingSignedAssertionFromVault( /// public static void UsingSignedAssertionFromCustomProvider( ILogger logger, - string signedAssertionUri) => s_usingSignedAssertionFromCustomProvider(logger, signedAssertionUri, default!); + string signedAssertionUri) + { + s_usingSignedAssertionFromCustomProvider(logger, signedAssertionUri, default!); + } /// /// Logger for handling information specific to ConfidentialClientApplicationBuilderExtension. @@ -153,7 +174,10 @@ public static void UsingSignedAssertionFromCustomProvider( /// public static void UsingCertThumbprint( ILogger logger, - string certThumbprint) => s_usingCertThumbprint(logger, certThumbprint, default!); + string? certThumbprint) + { + s_usingCertThumbprint(logger, certThumbprint ?? "null", default!); + } } } } diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/ConfidentialClientApplicationBuilderExtension.cs b/src/Microsoft.Identity.Web.TokenAcquisition/ConfidentialClientApplicationBuilderExtension.cs index 324f0e458..a03d6d508 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/ConfidentialClientApplicationBuilderExtension.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/ConfidentialClientApplicationBuilderExtension.cs @@ -21,8 +21,13 @@ public static ConfidentialClientApplicationBuilder WithClientCredentials( ICredentialsLoader credentialsLoader, CredentialSourceLoaderParameters credentialSourceLoaderParameters) { - return WithClientCredentialsAsync(builder, clientCredentials, logger, credentialsLoader, - credentialSourceLoaderParameters).GetAwaiter().GetResult(); + return WithClientCredentialsAsync( + builder, + clientCredentials, + logger, + credentialsLoader, + credentialSourceLoaderParameters, + isTokenBinding: false).GetAwaiter().GetResult(); } public static async Task WithClientCredentialsAsync( @@ -30,7 +35,8 @@ public static async Task WithClientCredent IEnumerable clientCredentials, ILogger logger, ICredentialsLoader credentialsLoader, - CredentialSourceLoaderParameters? credentialSourceLoaderParameters) + CredentialSourceLoaderParameters? credentialSourceLoaderParameters, + bool isTokenBinding) { var credential = await LoadCredentialForMsalOrFailAsync( clientCredentials, @@ -39,6 +45,17 @@ public static async Task WithClientCredent credentialSourceLoaderParameters) .ConfigureAwait(false); + if (isTokenBinding) + { + if (credential?.Certificate != null) + { + return builder.WithCertificate(credential.Certificate); + } + + logger.LogError("A certificate, which is required for token binding, is missing in loaded credentials."); + throw new InvalidOperationException(IDWebErrorMessage.MissingTokenBindingCertificate); + } + if (credential == null) { return builder; @@ -129,7 +146,7 @@ public static async Task WithClientCredent { if (credential.Certificate != null) { - Logger.UsingCertThumbprint(logger, credential.Certificate.Thumbprint); + Logger.UsingCertThumbprint(logger, credential.Certificate?.Thumbprint); return credential; } } diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/Constants.cs b/src/Microsoft.Identity.Web.TokenAcquisition/Constants.cs index 0ed55a3cb..d237125a3 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/Constants.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/Constants.cs @@ -1,6 +1,9 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; +using System.Collections.Generic; + namespace Microsoft.Identity.Web { /// @@ -131,6 +134,10 @@ public static class Constants internal const string SignedAssertionInvalidTimeRange = "AADSTS700024"; internal const string CertificateHasBeenRevoked = "AADSTS7000214"; internal const string CertificateIsOutsideValidityWindow = "AADSTS1000502"; + internal const string ClientAssertionContainsInvalidSignature = "AADSTS7000274"; + internal const string CertificateWasRevoked = "AADSTS7000277"; + internal const string ApplicationNotFound = "AADSTS700016"; + internal const string InvalidClientSecret = "AADSTS7000215"; internal const string CiamAuthoritySuffix = ".ciamlogin.com"; internal const string TestSlice = "dc"; internal const string ExtensionOptionsServiceProviderKey = "ID_WEB_INTERNAL_SERVICE_PROVIDER"; @@ -140,6 +147,28 @@ public static class Constants internal const string FmiPathForClientAssertion = "IDWEB_FMI_PATH_FOR_CLIENT_ASSERTION"; internal const string MicrosoftIdentityOptionsParameter = "IDWEB_FMI_MICROSOFT_IDENTITY_OPTIONS"; + /// + /// Error codes indicating certificate or signed assertion issues that warrant retry with a new certificate. + /// + internal static readonly HashSet s_certificateRelatedErrorCodes = new (StringComparer.OrdinalIgnoreCase) + { + InvalidKeyError, // AADSTS700027 - Client assertion contains an invalid signature + SignedAssertionInvalidTimeRange, // AADSTS700024 - Signed assertion invalid time range + CertificateHasBeenRevoked, // AADSTS7000214 - Certificate has been revoked + CertificateIsOutsideValidityWindow, // AADSTS1000502 - Certificate is outside validity window + ClientAssertionContainsInvalidSignature, // AADSTS7000274 - Client assertion contains an invalid signature + CertificateWasRevoked, // AADSTS7000277 - Certificate was revoked + }; + + /// + /// Error codes indicating permanent configuration errors that should not trigger retry. + /// + internal static readonly HashSet s_nonRetryableConfigErrorCodes = new (StringComparer.OrdinalIgnoreCase) + { + InvalidClientSecret, // AADSTS7000215 - Wrong client secret + ApplicationNotFound, // AADSTS700016 - Application with identifier not found + }; + /// /// Key for client assertion in token acquisition options. /// diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/DefaultAuthorizationHeaderProvider.cs b/src/Microsoft.Identity.Web.TokenAcquisition/DefaultAuthorizationHeaderProvider.cs index a3db6ad04..6ddc7cd76 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/DefaultAuthorizationHeaderProvider.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/DefaultAuthorizationHeaderProvider.cs @@ -11,10 +11,15 @@ namespace Microsoft.Identity.Web { - internal sealed class DefaultAuthorizationHeaderProvider : IAuthorizationHeaderProvider + internal sealed class DefaultAuthorizationHeaderProvider : IAuthorizationHeaderProvider, IBoundAuthorizationHeaderProvider { + private static readonly object s_boxedTrue = true; + private readonly ITokenAcquisition _tokenAcquisition; + private const string TokenBindingProtocolScheme = "MTLS_POP"; + private const string TokenBindingParameterName = "IsTokenBinding"; + public DefaultAuthorizationHeaderProvider(ITokenAcquisition tokenAcquisition) { _tokenAcquisition = tokenAcquisition; @@ -97,6 +102,55 @@ public async Task CreateAuthorizationHeaderAsync( return result.CreateAuthorizationHeader(); } + /// + public async Task> CreateBoundAuthorizationHeaderAsync( + DownstreamApiOptions downstreamApiOptions, + ClaimsPrincipal? claimsPrincipal = null, + CancellationToken cancellationToken = default) + { + if (!string.Equals(downstreamApiOptions?.ProtocolScheme, TokenBindingProtocolScheme, StringComparison.OrdinalIgnoreCase)) + { + var authorizationHeaderValue = await CreateAuthorizationHeaderAsync( + downstreamApiOptions?.Scopes ?? Enumerable.Empty(), + downstreamApiOptions, + claimsPrincipal, + cancellationToken).ConfigureAwait(false); + + var result = new AuthorizationHeaderInformation() + { + AuthorizationHeaderValue = authorizationHeaderValue, + BindingCertificate = null, + }; + + return new(result); + } + + // Token binding flow currently supports only app tokens. + if (!(downstreamApiOptions?.RequestAppToken ?? false)) + { + throw new ArgumentException(IDWebErrorMessage.TokenBindingRequiresEnabledAppTokenAcquisition, nameof(downstreamApiOptions.RequestAppToken)); + } + + var newTokenAcquisitionOptions = CreateTokenAcquisitionOptionsFromApiOptions(downstreamApiOptions, cancellationToken); + + var tokenAcquisitionResult = await _tokenAcquisition.GetAuthenticationResultForAppAsync( + downstreamApiOptions?.Scopes?.FirstOrDefault()!, + downstreamApiOptions?.AcquireTokenOptions.AuthenticationOptionsName, + downstreamApiOptions?.AcquireTokenOptions.Tenant, + newTokenAcquisitionOptions).ConfigureAwait(false); + + UpdateOriginalTokenAcquisitionOptions(downstreamApiOptions?.AcquireTokenOptions, newTokenAcquisitionOptions); + + var authorizationHeader = tokenAcquisitionResult.CreateAuthorizationHeader(); + var authorizationHeaderInformation = new AuthorizationHeaderInformation() + { + AuthorizationHeaderValue = authorizationHeader, + BindingCertificate = tokenAcquisitionResult.BindingCertificate + }; + + return new(authorizationHeaderInformation); + } + private static TokenAcquisitionOptions CreateTokenAcquisitionOptionsFromApiOptions( AuthorizationHeaderProviderOptions? downstreamApiOptions, CancellationToken cancellationToken) @@ -109,7 +163,7 @@ private static TokenAcquisitionOptions CreateTokenAcquisitionOptionsFromApiOptio CorrelationId = downstreamApiOptions?.AcquireTokenOptions.CorrelationId ?? Guid.Empty, ExtraHeadersParameters = downstreamApiOptions?.AcquireTokenOptions.ExtraHeadersParameters, ExtraQueryParameters = downstreamApiOptions?.AcquireTokenOptions.ExtraQueryParameters, - ExtraParameters = downstreamApiOptions?.AcquireTokenOptions.ExtraParameters, + ExtraParameters = GetExtraParameters(downstreamApiOptions), ForceRefresh = downstreamApiOptions?.AcquireTokenOptions.ForceRefresh ?? false, LongRunningWebApiSessionKey = downstreamApiOptions?.AcquireTokenOptions.LongRunningWebApiSessionKey, ManagedIdentity = downstreamApiOptions?.AcquireTokenOptions.ManagedIdentity, @@ -131,5 +185,23 @@ private void UpdateOriginalTokenAcquisitionOptions(AcquireTokenOptions? acquireT acquireTokenOptions.LongRunningWebApiSessionKey = newTokenAcquisitionOptions.LongRunningWebApiSessionKey; } } + + /// + /// Retrieves the collection of extra parameters to be included when acquiring a token, optionally adding + /// protocol-specific parameters based on the provided options. + /// + /// The options used to configure token acquisition. + /// A dictionary containing extra parameters to be sent during token acquisition or null. + private static IDictionary? GetExtraParameters(AuthorizationHeaderProviderOptions? downstreamApiOptions) + { + var extraParameters = downstreamApiOptions?.AcquireTokenOptions.ExtraParameters; + if (string.Equals(downstreamApiOptions?.ProtocolScheme, TokenBindingProtocolScheme, StringComparison.OrdinalIgnoreCase)) + { + extraParameters ??= new Dictionary(); + extraParameters[TokenBindingParameterName] = s_boxedTrue; + } + + return extraParameters; + } } } diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/DefaultTokenAcquirerFactoryImplementation.cs b/src/Microsoft.Identity.Web.TokenAcquisition/DefaultTokenAcquirerFactoryImplementation.cs index c4c76af60..cfb4fc57b 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/DefaultTokenAcquirerFactoryImplementation.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/DefaultTokenAcquirerFactoryImplementation.cs @@ -59,10 +59,10 @@ public ITokenAcquirer GetTokenAcquirer(IdentityApplicationOptions IdentityApplic _ = Throws.IfNull(IdentityApplicationOptions); // Compute the Azure region if the option is a MicrosoftIdentityApplicationOptions. - MicrosoftIdentityApplicationOptions? MicrosoftIdentityApplicationOptions = IdentityApplicationOptions as MicrosoftIdentityApplicationOptions; - if (MicrosoftIdentityApplicationOptions == null) + MicrosoftIdentityApplicationOptions? microsoftIdentityApplicationOptions = IdentityApplicationOptions as MicrosoftIdentityApplicationOptions; + if (microsoftIdentityApplicationOptions == null) { - MicrosoftIdentityApplicationOptions = new MicrosoftIdentityApplicationOptions + microsoftIdentityApplicationOptions = new MicrosoftIdentityApplicationOptions { AllowWebApiToBeAuthorizedByACL = IdentityApplicationOptions.AllowWebApiToBeAuthorizedByACL, Audience = IdentityApplicationOptions.Audience, @@ -73,9 +73,24 @@ public ITokenAcquirer GetTokenAcquirer(IdentityApplicationOptions IdentityApplic TokenDecryptionCredentials = IdentityApplicationOptions.TokenDecryptionCredentials, EnablePiiLogging = IdentityApplicationOptions.EnablePiiLogging, }; + + // If the IdentityApplicationOptions is of type MicrosoftEntraApplicationOptions, + // copy over those options too. + MicrosoftEntraApplicationOptions? microsoftEntraApplicationOptions = IdentityApplicationOptions as MicrosoftEntraApplicationOptions; + if (microsoftEntraApplicationOptions != null) + { + microsoftIdentityApplicationOptions.Authority = microsoftEntraApplicationOptions.Authority; + microsoftIdentityApplicationOptions.Name = microsoftEntraApplicationOptions.Name; + microsoftIdentityApplicationOptions.Instance = microsoftEntraApplicationOptions.Instance; + microsoftIdentityApplicationOptions.TenantId = microsoftEntraApplicationOptions.TenantId; + microsoftIdentityApplicationOptions.AppHomeTenantId = microsoftEntraApplicationOptions.AppHomeTenantId; + microsoftIdentityApplicationOptions.AzureRegion = microsoftEntraApplicationOptions.AzureRegion; + microsoftIdentityApplicationOptions.ClientCapabilities = microsoftEntraApplicationOptions.ClientCapabilities; + microsoftIdentityApplicationOptions.SendX5C = microsoftEntraApplicationOptions.SendX5C; + } } - string key = GetKey(IdentityApplicationOptions.Authority, IdentityApplicationOptions.ClientId, MicrosoftIdentityApplicationOptions.AzureRegion); + string key = GetKey(IdentityApplicationOptions.Authority, IdentityApplicationOptions.ClientId, microsoftIdentityApplicationOptions.AzureRegion); return _authSchemes.GetOrAdd(key, (key) => { @@ -83,7 +98,7 @@ public ITokenAcquirer GetTokenAcquirer(IdentityApplicationOptions IdentityApplic MergedOptions mergedOptions = optionsMonitor.Get(key); - MergedOptions.UpdateMergedOptionsFromMicrosoftIdentityApplicationOptions(MicrosoftIdentityApplicationOptions, mergedOptions); + MergedOptions.UpdateMergedOptionsFromMicrosoftIdentityApplicationOptions(microsoftIdentityApplicationOptions, mergedOptions); return MakeTokenAcquirer(key); }); } diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/DefaultTokenAcquisitionHost.cs b/src/Microsoft.Identity.Web.TokenAcquisition/DefaultTokenAcquisitionHost.cs index 3f0546754..4101fbe73 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/DefaultTokenAcquisitionHost.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/DefaultTokenAcquisitionHost.cs @@ -19,7 +19,7 @@ internal sealed class DefaultTokenAcquisitionHost : ITokenAcquisitionHost readonly IOptionsMonitor _MicrosoftIdentityApplicationOptionsMonitor; public DefaultTokenAcquisitionHost( - IOptionsMonitor optionsMonitor, + IOptionsMonitor optionsMonitor, IMergedOptionsStore mergedOptionsMonitor, IOptionsMonitor ccaOptionsMonitor, IOptionsMonitor microsoftIdentityApplicationOptionsMonitor) diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/IDWebErrorMessage.cs b/src/Microsoft.Identity.Web.TokenAcquisition/IDWebErrorMessage.cs index d01e300ae..56ee6fb43 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/IDWebErrorMessage.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/IDWebErrorMessage.cs @@ -21,6 +21,8 @@ internal static class IDWebErrorMessage public const string MissingRequiredScopesForAuthorizationFilter = "IDW10108: RequiredScope Attribute does not contain a value. The scopes need to be set on the controller, the page or action. See https://aka.ms/ms-id-web/required-scope-attribute. "; public const string ClientCertificatesHaveExpiredOrCannotBeLoaded = "IDW10109: No credential could be loaded. This can happen when certificates passed to the configuration have expired or can't be loaded and the code isn't running on Azure to be able to use Managed Identity, Pod Identity etc. Details: "; public const string ClientSecretAndCredentialsCannotBeCombined = "IDW10110: ClientSecret top level configuration cannot be combined with ClientCredentials. Instead, add a new entry in the ClientCredentials array describing the secret."; + public const string MissingTokenBindingCertificate = "IDW10115: A signing certificate, which is required for token binding, is missing in loaded credentials."; + public const string TokenBindingRequiresEnabledAppTokenAcquisition = "IDW10116: Token binding requires enabled app token acquisition."; // Authorization IDW10200 = "IDW10200:" public const string NeitherScopeOrRolesClaimFoundInToken = "IDW10201: Neither scope nor roles claim was found in the bearer token. Authentication scheme used: '{0}'. "; @@ -66,12 +68,15 @@ internal static class IDWebErrorMessage "StoreLocation must be one of 'CurrentUser', 'LocalMachine'. " + "StoreName must be empty or one of '{0}'. "; + // Configuration Validation IDW10700+ = "IDW10700+:" + public const string MissingIdentityConfiguration = "IDW10708: The identity configuration is incomplete. Provide either 'Instance' and 'TenantId', or 'Authority', or enable 'ManagedIdentity' in the configuration. Check your configuration keys for typos (e.g., trailing spaces). See https://aka.ms/ms-id-web/configuration. "; + // Obsolete messages IDW10800 = "IDW10800:" public const string AadIssuerValidatorGetIssuerValidatorIsObsolete = "IDW10800: Use MicrosoftIdentityIssuerValidatorFactory.GetAadIssuerValidator. See https://aka.ms/ms-id-web/1.2.0. "; public const string InitializeAsyncIsObsolete = "IDW10801: Use Initialize instead. See https://aka.ms/ms-id-web/1.9.0. "; public const string FromStoreWithThumprintIsObsolete = "IDW10803: Use FromStoreWithThumbprint instead, due to spelling error. "; public const string AadIssuerValidatorIsObsolete = "IDW10804: Use MicrosoftIdentityIssuerValidator. "; - + public const string WithClientCredentialsIsObsolete = "Use WithClientCredentialsAsync instead."; } } diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/LoggingEventId.cs b/src/Microsoft.Identity.Web.TokenAcquisition/LoggingEventId.cs index 2abac1a8b..d64f5a414 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/LoggingEventId.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/LoggingEventId.cs @@ -29,6 +29,9 @@ internal static class LoggingEventId public static readonly EventId CredentialLoadAttemptFailed = new EventId(406, "CredentialLoadAttemptFailed"); public static readonly EventId UsingSignedAssertionFromCustomProvider = new EventId(407, "UsingSignedAssertionFromCustomProvider"); + // MergedOptions EventIds 500+ + public static readonly EventId AuthorityIgnored = new EventId(500, "AuthorityIgnored"); + #pragma warning restore IDE1006 // Naming Styles } } diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/MergedOptions.cs b/src/Microsoft.Identity.Web.TokenAcquisition/MergedOptions.cs index 62606f3d5..94b2b0492 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/MergedOptions.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/MergedOptions.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using IdWebLogger = Microsoft.Extensions.Logging; using Microsoft.Identity.Abstractions; #if !NETSTANDARD2_0 && !NET462 && !NET472 @@ -22,6 +23,11 @@ internal sealed class MergedOptions : MicrosoftIdentityOptions { private ConfidentialClientApplicationOptions? _confidentialClientApplicationOptions; + /// + /// Logger instance for diagnostics. + /// + internal IdWebLogger.ILogger? Logger { get; set; } + public ConfidentialClientApplicationOptions ConfidentialClientApplicationOptions { get @@ -386,6 +392,8 @@ internal static void UpdateMergedOptionsFromConfidentialClientApplicationOptions internal static void UpdateConfidentialClientApplicationOptionsFromMergedOptions(MergedOptions mergedOptions, ConfidentialClientApplicationOptions confidentialClientApplicationOptions) { + ParseAuthorityIfNecessary(mergedOptions, mergedOptions.Logger); + confidentialClientApplicationOptions.AadAuthorityAudience = mergedOptions.AadAuthorityAudience; confidentialClientApplicationOptions.AzureCloudInstance = mergedOptions.AzureCloudInstance; if (string.IsNullOrEmpty(confidentialClientApplicationOptions.AzureRegion) && !string.IsNullOrEmpty(mergedOptions.AzureRegion)) @@ -416,8 +424,6 @@ internal static void UpdateConfidentialClientApplicationOptionsFromMergedOptions confidentialClientApplicationOptions.EnablePiiLogging = mergedOptions.EnablePiiLogging; - ParseAuthorityIfNecessary(mergedOptions); - if (string.IsNullOrEmpty(confidentialClientApplicationOptions.Instance) && !string.IsNullOrEmpty(mergedOptions.Instance)) { confidentialClientApplicationOptions.Instance = mergedOptions.Instance; @@ -438,8 +444,25 @@ internal static void UpdateConfidentialClientApplicationOptionsFromMergedOptions } } - internal static void ParseAuthorityIfNecessary(MergedOptions mergedOptions) + internal static void ParseAuthorityIfNecessary(MergedOptions mergedOptions, IdWebLogger.ILogger? logger = null) { + // Check if Authority is configured but being ignored due to Instance/TenantId taking precedence + if (!string.IsNullOrEmpty(mergedOptions.Authority) && + (!string.IsNullOrEmpty(mergedOptions.Instance) || !string.IsNullOrEmpty(mergedOptions.TenantId))) + { + // Log warning that Authority is being ignored + if (logger != null) + { + MergedOptionsLogging.AuthorityIgnored( + logger, + mergedOptions.Authority!, + mergedOptions.Instance ?? string.Empty, + mergedOptions.TenantId ?? string.Empty); + } + // Authority is ignored; Instance and TenantId take precedence + return; + } + if (string.IsNullOrEmpty(mergedOptions.TenantId) && string.IsNullOrEmpty(mergedOptions.Instance) && !string.IsNullOrEmpty(mergedOptions.Authority)) { ReadOnlySpan doubleSlash = "//".AsSpan(); @@ -473,6 +496,11 @@ internal static void UpdateMergedOptionsFromJwtBearerOptions(JwtBearerOptions jw public void PrepareAuthorityInstanceForMsal() { + if (string.IsNullOrEmpty(Instance) && string.IsNullOrEmpty(TenantId) && !string.IsNullOrEmpty(Authority)) + { + ParseAuthorityIfNecessary(this, this.Logger); + } + if (string.IsNullOrEmpty(Instance)) { return; diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/MergedOptionsLogging.cs b/src/Microsoft.Identity.Web.TokenAcquisition/MergedOptionsLogging.cs new file mode 100644 index 000000000..3e8a42176 --- /dev/null +++ b/src/Microsoft.Identity.Web.TokenAcquisition/MergedOptionsLogging.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Identity.Web +{ + /// + /// High-performance logging for MergedOptions operations. + /// + internal static partial class MergedOptionsLogging + { + private static readonly Action s_authorityIgnored = + LoggerMessage.Define( + LogLevel.Warning, + LoggingEventId.AuthorityIgnored, + "[MsIdWeb] Authority '{Authority}' is being ignored because Instance '{Instance}' and/or TenantId '{TenantId}' are already configured. To use Authority, remove Instance and TenantId from the configuration."); + + /// + /// Logs a warning when Authority is configured alongside Instance and/or TenantId. + /// + /// The logger instance. + /// The Authority value that is being ignored. + /// The Instance value that takes precedence. + /// The TenantId value that takes precedence. + public static void AuthorityIgnored( + ILogger logger, + string authority, + string instance, + string tenantId) + { + s_authorityIgnored(logger, authority, instance, tenantId, null); + } + } +} diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/MergedOptionsStore.cs b/src/Microsoft.Identity.Web.TokenAcquisition/MergedOptionsStore.cs index 4e4a815ba..8873b456e 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/MergedOptionsStore.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/MergedOptionsStore.cs @@ -1,22 +1,35 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; using System.Collections.Concurrent; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace Microsoft.Identity.Web { internal class MergedOptionsStore : IMergedOptionsStore { private readonly ConcurrentDictionary _options; + private readonly ILoggerFactory? _loggerFactory; - public MergedOptionsStore() + public MergedOptionsStore(IServiceProvider serviceProvider) { _options = new ConcurrentDictionary(); + _loggerFactory = serviceProvider?.GetService(); } public MergedOptions Get(string name) { - return _options.GetOrAdd(name, key => new MergedOptions()); + return _options.GetOrAdd(name, key => + { + var mergedOptions = new MergedOptions(); + if (_loggerFactory != null) + { + mergedOptions.Logger = _loggerFactory.CreateLogger(); + } + return mergedOptions; + }); } } } diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/Microsoft.Identity.Web.TokenAcquisition.csproj b/src/Microsoft.Identity.Web.TokenAcquisition/Microsoft.Identity.Web.TokenAcquisition.csproj index 34322a4d9..ebf5a85dd 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/Microsoft.Identity.Web.TokenAcquisition.csproj +++ b/src/Microsoft.Identity.Web.TokenAcquisition/Microsoft.Identity.Web.TokenAcquisition.csproj @@ -8,10 +8,9 @@ README.md - - + + true + @@ -35,7 +34,7 @@ \ - + @@ -52,6 +51,8 @@ + + @@ -60,5 +61,5 @@ - + diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/MicrosoftIdentityAppCallingWebApiAuthenticationBuilder.cs b/src/Microsoft.Identity.Web.TokenAcquisition/MicrosoftIdentityAppCallingWebApiAuthenticationBuilder.cs index 92488959e..8939dfbcc 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/MicrosoftIdentityAppCallingWebApiAuthenticationBuilder.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/MicrosoftIdentityAppCallingWebApiAuthenticationBuilder.cs @@ -19,9 +19,8 @@ namespace Microsoft.Identity.Web /// public class MicrosoftIdentityAppCallsWebApiAuthenticationBuilder : MicrosoftIdentityBaseAuthenticationBuilder { -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Calls Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder.MicrosoftIdentityBaseAuthenticationBuilder(IServiceCollection, IConfigurationSection)")] -#endif + [RequiresUnreferencedCode("Calls Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder.MicrosoftIdentityBaseAuthenticationBuilder(IServiceCollection, IConfigurationSection).")] + [RequiresDynamicCode("Calls Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder.MicrosoftIdentityBaseAuthenticationBuilder(IServiceCollection, IConfigurationSection).")] internal MicrosoftIdentityAppCallsWebApiAuthenticationBuilder( IServiceCollection services, IConfigurationSection? configurationSection = null) diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/MicrosoftIdentityBaseAuthenticationBuilder.cs b/src/Microsoft.Identity.Web.TokenAcquisition/MicrosoftIdentityBaseAuthenticationBuilder.cs index 306808905..0b5050cc4 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/MicrosoftIdentityBaseAuthenticationBuilder.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/MicrosoftIdentityBaseAuthenticationBuilder.cs @@ -23,9 +23,8 @@ public class MicrosoftIdentityBaseAuthenticationBuilder /// /// The services being configured. /// Optional configuration section. -#if NET6_0_OR_GREATER && !NET8_0_OR_GREATER [RequiresUnreferencedCode("Calls Microsoft.Extensions.Configuration.ConfigurationBinder.Bind(IConfiguration, Object).")] -#endif + [RequiresDynamicCode("Calls Microsoft.Extensions.Configuration.ConfigurationBinder.Bind(IConfiguration, Object).")] protected MicrosoftIdentityBaseAuthenticationBuilder( IServiceCollection services, IConfigurationSection? configurationSection = null) @@ -38,6 +37,8 @@ protected MicrosoftIdentityBaseAuthenticationBuilder( IdentityModelEventSource.ShowPII = logOptions.EnablePiiLogging; } + [RequiresUnreferencedCode("Calls Microsoft.Extensions.Configuration.ConfigurationBinder.GetValue")] + [RequiresDynamicCode("Calls Microsoft.Extensions.Configuration.ConfigurationBinder.GetValue")] internal static void SetIdentityModelLogger(IServiceProvider serviceProvider) { if (serviceProvider != null) diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/MicrosoftIdentityHttpClientBuilderExtensions.cs b/src/Microsoft.Identity.Web.TokenAcquisition/MicrosoftIdentityHttpClientBuilderExtensions.cs new file mode 100644 index 000000000..65e29fb2e --- /dev/null +++ b/src/Microsoft.Identity.Web.TokenAcquisition/MicrosoftIdentityHttpClientBuilderExtensions.cs @@ -0,0 +1,422 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Identity.Abstractions; + +namespace Microsoft.Identity.Web +{ + /// + /// Extension methods for to add + /// to the HTTP client pipeline with various configuration options. + /// + /// + /// + /// These extension methods provide a convenient way to configure HttpClient instances with automatic + /// Microsoft Identity authentication using . The handler + /// will automatically add authorization headers to outgoing HTTP requests based on the configured options. + /// + /// + /// Four overloads are provided to support different configuration scenarios: + /// + /// + /// Parameterless: For scenarios where options are set per-request + /// Options instance: For programmatic configuration with a pre-built options object + /// Action delegate: For inline configuration using a configuration delegate + /// IConfiguration: For configuration from appsettings.json or other configuration sources + /// + /// + /// + /// Basic usage with inline configuration: + /// + /// services.AddHttpClient("MyApiClient", client => + /// { + /// client.BaseAddress = new Uri("https://api.example.com"); + /// }) + /// .AddMicrosoftIdentityMessageHandler(options => + /// { + /// options.Scopes.Add("https://api.example.com/.default"); + /// }); + /// + /// + /// Configuration from appsettings.json: + /// + /// // In appsettings.json: + /// // { + /// // "DownstreamApi": { + /// // "Scopes": ["https://graph.microsoft.com/.default"] + /// // } + /// // } + /// + /// services.AddHttpClient("GraphClient") + /// .AddMicrosoftIdentityMessageHandler( + /// configuration.GetSection("DownstreamApi"), + /// "DownstreamApi"); + /// + /// + /// Parameterless for per-request configuration: + /// + /// services.AddHttpClient("FlexibleClient") + /// .AddMicrosoftIdentityMessageHandler(); + /// + /// // Later, in a service: + /// var request = new HttpRequestMessage(HttpMethod.Get, "/api/data") + /// .WithAuthenticationOptions(options => + /// { + /// options.Scopes.Add("custom.scope"); + /// }); + /// var response = await httpClient.SendAsync(request); + /// + /// + /// + /// + /// + public static class MicrosoftIdentityHttpClientBuilderExtensions + { + /// + /// Adds a to the HTTP client pipeline with no default options. + /// Options must be configured per-request using . + /// + /// The to configure. + /// The for method chaining. + /// Thrown when is . + /// + /// + /// This overload is useful when you need maximum flexibility to configure authentication options + /// on a per-request basis. Since no default options are provided, every request must include + /// authentication options via the WithAuthenticationOptions extension method. + /// + /// + /// The handler will resolve from the service provider + /// at runtime to acquire authorization headers for outgoing requests. + /// + /// + /// + /// + /// // Configure the HTTP client + /// services.AddHttpClient("ApiClient") + /// .AddMicrosoftIdentityMessageHandler(); + /// + /// // Use the client with per-request configuration + /// public class MyService + /// { + /// private readonly HttpClient _httpClient; + /// + /// public MyService(IHttpClientFactory factory) + /// { + /// _httpClient = factory.CreateClient("ApiClient"); + /// } + /// + /// public async Task<string> GetDataAsync() + /// { + /// var request = new HttpRequestMessage(HttpMethod.Get, "/api/data") + /// .WithAuthenticationOptions(options => + /// { + /// options.Scopes.Add("https://api.example.com/.default"); + /// }); + /// + /// var response = await _httpClient.SendAsync(request); + /// response.EnsureSuccessStatusCode(); + /// return await response.Content.ReadAsStringAsync(); + /// } + /// } + /// + /// + public static IHttpClientBuilder AddMicrosoftIdentityMessageHandler( + this IHttpClientBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + return builder.AddHttpMessageHandler(sp => + { + var headerProvider = sp.GetRequiredService(); + return new MicrosoftIdentityMessageHandler(headerProvider); + }); + } + + /// + /// Adds a to the HTTP client pipeline with the specified options. + /// + /// The to configure. + /// The authentication options to use for all requests made by this client. + /// The for method chaining. + /// + /// Thrown when or is . + /// + /// + /// + /// This overload is useful when you have a pre-configured + /// instance that should be used for all requests made by this HTTP client. Individual requests can still + /// override these default options using the per-request extension methods. + /// + /// + /// The handler will resolve from the service provider + /// at runtime to acquire authorization headers for outgoing requests. + /// + /// + /// + /// + /// // Pre-configure options + /// var options = new MicrosoftIdentityMessageHandlerOptions + /// { + /// Scopes = { "https://graph.microsoft.com/.default" } + /// }; + /// options.WithAgentIdentity("agent-application-id"); + /// + /// // Configure the HTTP client with the pre-built options + /// services.AddHttpClient("GraphClient", client => + /// { + /// client.BaseAddress = new Uri("https://graph.microsoft.com"); + /// }) + /// .AddMicrosoftIdentityMessageHandler(options); + /// + /// // Use the client - authentication is automatic + /// public class GraphService + /// { + /// private readonly HttpClient _httpClient; + /// + /// public GraphService(IHttpClientFactory factory) + /// { + /// _httpClient = factory.CreateClient("GraphClient"); + /// } + /// + /// public async Task<string> GetUserProfileAsync() + /// { + /// var response = await _httpClient.GetAsync("/v1.0/me"); + /// response.EnsureSuccessStatusCode(); + /// return await response.Content.ReadAsStringAsync(); + /// } + /// } + /// + /// + public static IHttpClientBuilder AddMicrosoftIdentityMessageHandler( + this IHttpClientBuilder builder, + MicrosoftIdentityMessageHandlerOptions options) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + return builder.AddHttpMessageHandler(sp => + { + var headerProvider = sp.GetRequiredService(); + return new MicrosoftIdentityMessageHandler(headerProvider, options); + }); + } + + /// + /// Adds a to the HTTP client pipeline with options configured via delegate. + /// + /// The to configure. + /// A delegate to configure the authentication options. + /// The for method chaining. + /// + /// Thrown when or is . + /// + /// + /// + /// This overload is useful for inline configuration of authentication options. The delegate is called + /// once during service configuration to create the default options for the HTTP client. + /// Individual requests can still override these default options using the per-request extension methods. + /// + /// + /// The handler will resolve from the service provider + /// at runtime to acquire authorization headers for outgoing requests. + /// + /// + /// + /// + /// // Configure the HTTP client with inline options configuration + /// services.AddHttpClient("MyApiClient", client => + /// { + /// client.BaseAddress = new Uri("https://api.example.com"); + /// }) + /// .AddMicrosoftIdentityMessageHandler(options => + /// { + /// options.Scopes.Add("https://api.example.com/.default"); + /// options.RequestAppToken = true; + /// }); + /// + /// // Use the client - authentication is automatic + /// public class ApiService + /// { + /// private readonly HttpClient _httpClient; + /// + /// public ApiService(IHttpClientFactory factory) + /// { + /// _httpClient = factory.CreateClient("MyApiClient"); + /// } + /// + /// public async Task<string> GetDataAsync() + /// { + /// var response = await _httpClient.GetAsync("/api/data"); + /// response.EnsureSuccessStatusCode(); + /// return await response.Content.ReadAsStringAsync(); + /// } + /// } + /// + /// + /// With agent identity: + /// + /// services.AddHttpClient("AgentClient") + /// .AddMicrosoftIdentityMessageHandler(options => + /// { + /// options.Scopes.Add("https://graph.microsoft.com/.default"); + /// options.WithAgentIdentity("agent-application-id"); + /// options.RequestAppToken = true; + /// }); + /// + /// + public static IHttpClientBuilder AddMicrosoftIdentityMessageHandler( + this IHttpClientBuilder builder, + Action configureOptions) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (configureOptions == null) + { + throw new ArgumentNullException(nameof(configureOptions)); + } + + return builder.AddHttpMessageHandler(sp => + { + var options = new MicrosoftIdentityMessageHandlerOptions(); + configureOptions(options); + + var headerProvider = sp.GetRequiredService(); + return new MicrosoftIdentityMessageHandler(headerProvider, options); + }); + } + + /// + /// Adds a to the HTTP client pipeline with options bound from IConfiguration. + /// + /// The to configure. + /// The configuration section containing the authentication options. + /// The name of the configuration section (used for diagnostics). + /// The for method chaining. + /// + /// Thrown when , , or is . + /// + /// + /// + /// This overload is useful when you want to configure authentication options from appsettings.json + /// or other configuration sources. The configuration section is bound to a new + /// instance using standard configuration binding. + /// Individual requests can still override these default options using the per-request extension methods. + /// + /// + /// The handler will resolve from the service provider + /// at runtime to acquire authorization headers for outgoing requests. + /// + /// + /// + /// Configuration in appsettings.json: + /// + /// { + /// "DownstreamApi": { + /// "Scopes": ["https://api.example.com/.default"] + /// }, + /// "GraphApi": { + /// "Scopes": ["https://graph.microsoft.com/.default", "User.Read"] + /// } + /// } + /// + /// + /// Configure the HTTP client: + /// + /// // In Program.cs or Startup.cs + /// services.AddHttpClient("DownstreamApiClient", client => + /// { + /// client.BaseAddress = new Uri("https://api.example.com"); + /// }) + /// .AddMicrosoftIdentityMessageHandler( + /// configuration.GetSection("DownstreamApi"), + /// "DownstreamApi"); + /// + /// services.AddHttpClient("GraphClient", client => + /// { + /// client.BaseAddress = new Uri("https://graph.microsoft.com"); + /// }) + /// .AddMicrosoftIdentityMessageHandler( + /// configuration.GetSection("GraphApi"), + /// "GraphApi"); + /// + /// + /// Use the clients: + /// + /// public class MyService + /// { + /// private readonly HttpClient _apiClient; + /// private readonly HttpClient _graphClient; + /// + /// public MyService(IHttpClientFactory factory) + /// { + /// _apiClient = factory.CreateClient("DownstreamApiClient"); + /// _graphClient = factory.CreateClient("GraphClient"); + /// } + /// + /// public async Task<string> GetApiDataAsync() + /// { + /// var response = await _apiClient.GetAsync("/api/data"); + /// response.EnsureSuccessStatusCode(); + /// return await response.Content.ReadAsStringAsync(); + /// } + /// + /// public async Task<string> GetUserProfileAsync() + /// { + /// var response = await _graphClient.GetAsync("/v1.0/me"); + /// response.EnsureSuccessStatusCode(); + /// return await response.Content.ReadAsStringAsync(); + /// } + /// } + /// + /// + [RequiresUnreferencedCode("Calls Microsoft.Extensions.Configuration.ConfigurationBinder.Bind(IConfiguration, Object).")] + [RequiresDynamicCode("Calls Microsoft.Extensions.Configuration.ConfigurationBinder.Bind(IConfiguration, Object).")] + public static IHttpClientBuilder AddMicrosoftIdentityMessageHandler( + this IHttpClientBuilder builder, + IConfiguration configuration, + string sectionName) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + if (sectionName == null) + { + throw new ArgumentNullException(nameof(sectionName)); + } + + return builder.AddHttpMessageHandler(sp => + { + var options = new MicrosoftIdentityMessageHandlerOptions(); + configuration.Bind(options); + + var headerProvider = sp.GetRequiredService(); + return new MicrosoftIdentityMessageHandler(headerProvider, options); + }); + } + } +} diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/MsalMtlsHttpClientFactory.cs b/src/Microsoft.Identity.Web.TokenAcquisition/MsalMtlsHttpClientFactory.cs new file mode 100644 index 000000000..7bf978af9 --- /dev/null +++ b/src/Microsoft.Identity.Web.TokenAcquisition/MsalMtlsHttpClientFactory.cs @@ -0,0 +1,178 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using Microsoft.Identity.Client; + +namespace Microsoft.Identity.Web +{ + /// + /// Provides a factory for creating HTTP clients configured for mTLS authentication with using binding certificate. + /// It uses a hybrid approach with leveraging IHttpClientFactory for non-mTLS HTTP clients and maintaining + /// a pool of mTLS clients with using certificate thumbprint as a key. + /// + public sealed class MsalMtlsHttpClientFactory : IMsalMtlsHttpClientFactory, IDisposable + { + private const long MaxMtlsHttpClientCountInPool = 1000; + private const long MaxResponseContentBufferSizeInBytes = 1024 * 1024; + + // Please see (https://aka.ms/msal-httpclient-info) for important information regarding the HttpClient. + private static readonly Dictionary s_mtlsHttpClientPool = new Dictionary(); + + private static readonly ReaderWriterLockSlim s_cacheLock = new ReaderWriterLockSlim(); + + private readonly IHttpClientFactory _httpClientFactory; + + /// + /// Initializes a new instance of the MsalMtlsHttpClientFactory class using the specified HTTP client factory. + /// + /// The factory used to create HttpClient instances for mutual TLS (mTLS) operations. Cannot be null. + public MsalMtlsHttpClientFactory(IHttpClientFactory httpClientFactory) + { + _httpClientFactory = httpClientFactory; + } + + /// + /// Creates and configures a new instance of with telemetry headers applied. + /// + /// + /// The returned includes a telemetry header for tracking or + /// diagnostics purposes. Callers are responsible for disposing the instance when it is + /// no longer needed. + /// + /// A new instance with telemetry information included in the default request headers. + public HttpClient GetHttpClient() + { + HttpClient httpClient = _httpClientFactory.CreateClient(); + httpClient.DefaultRequestHeaders.Add(Constants.TelemetryHeaderKey, IdHelper.CreateTelemetryInfo()); + return httpClient; + } + + /// + /// Returns an instance of configured to use the specified X.509 client certificate for + /// mutual TLS authentication. + /// + /// + /// The returned instance is pooled and reused for the given certificate. + /// The client includes a telemetry header in each request. Callers should not modify the default + /// request headers or dispose the returned instance. + /// + /// The X.509 certificate to use for client authentication. If , a default instance without client certificate authentication is returned. + /// A instance configured for mutual TLS authentication using the specified certificate or default instance. + public HttpClient GetHttpClient(X509Certificate2 x509Certificate2) + { + if (x509Certificate2 == null) + { + return GetHttpClient(); + } + + string key = x509Certificate2.Thumbprint; + + s_cacheLock.EnterUpgradeableReadLock(); + + try + { + if (s_mtlsHttpClientPool.TryGetValue(key, out HttpClient? existingHttpClient)) + { + return existingHttpClient; + } + + s_cacheLock.EnterWriteLock(); + + try + { + // Double-check pattern: another thread may have added the client while we were waiting for write lock. + if (s_mtlsHttpClientPool.TryGetValue(key, out HttpClient? httpClient)) + { + return httpClient; + } + + CheckAndManageCache(); + + httpClient = CreateMtlsHttpClient(x509Certificate2); + s_mtlsHttpClientPool[key] = httpClient; + + return httpClient; + } + finally + { + s_cacheLock.ExitWriteLock(); + } + } + finally + { + s_cacheLock.ExitUpgradeableReadLock(); + } + } + + /// + public void Dispose() + { + s_cacheLock.EnterWriteLock(); + try + { + DisposeAllClients(); + s_mtlsHttpClientPool.Clear(); + } + finally + { + s_cacheLock.ExitWriteLock(); + } + + // Note: s_cacheLock is static and shared across all instances, so it should not be disposed here + } + + private HttpClient CreateMtlsHttpClient(X509Certificate2 bindingCertificate) + { +#if NET462 + throw new NotSupportedException("mTLS is not supported on this platform."); +#else + if (bindingCertificate == null) + { + throw new ArgumentNullException(nameof(bindingCertificate), "A valid X509 certificate must be provided for mTLS."); + } + + HttpClientHandler handler = new(); + handler.ClientCertificates.Add(bindingCertificate); + + // HTTP client factory can't be used there because HTTP client handler needs to be configured + // before a HTTP client instance is created + var httpClient = new HttpClient(handler); + ConfigureRequestHeadersAndSize(httpClient); + return httpClient; +#endif + } + + private static void CheckAndManageCache() + { + // lock is held by caller + if (s_mtlsHttpClientPool.Count >= MaxMtlsHttpClientCountInPool) + { + DisposeAllClients(); + s_mtlsHttpClientPool.Clear(); + } + } + + private static void DisposeAllClients() + { + // lock is held by caller + foreach (var httpClient in s_mtlsHttpClientPool.Values) + { + httpClient?.Dispose(); + } + } + + private static void ConfigureRequestHeadersAndSize(HttpClient httpClient) + { + httpClient.MaxResponseContentBufferSize = MaxResponseContentBufferSizeInBytes; + httpClient.DefaultRequestHeaders.Accept.Clear(); + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + httpClient.DefaultRequestHeaders.Add(Constants.TelemetryHeaderKey, IdHelper.CreateTelemetryInfo()); + } + } +} diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/OnBehalfOfEventArgs.cs b/src/Microsoft.Identity.Web.TokenAcquisition/OnBehalfOfEventArgs.cs new file mode 100644 index 000000000..6f32712fc --- /dev/null +++ b/src/Microsoft.Identity.Web.TokenAcquisition/OnBehalfOfEventArgs.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Security.Claims; + +namespace Microsoft.Identity.Web +{ + /// + /// Event arguments for on-behalf-of token acquisition operations. + /// Contains user assertion token information about the user and additional data for the token request. + /// + public class OnBehalfOfEventArgs + { + /// + /// Gets or sets the claims principal representing the user for whom the token is being acquired. + /// This is the claims principal into the the api. + /// This value can be null. + /// + public ClaimsPrincipal? User { get; set; } + + /// + /// Gets or sets additional context information for the token acquisition operation. + /// If the incoming token is encrypted, it will be decrypted and made available in this property. + /// + public string? UserAssertionToken { get; set; } + } +} diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/InternalAPI.Shipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/InternalAPI.Shipped.txt index 190f77063..284c86210 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/InternalAPI.Shipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/InternalAPI.Shipped.txt @@ -3,13 +3,16 @@ const Microsoft.Identity.Web.Constants.AgentIdentityKey = "IDWEB_AGENT_IDENTITY" const Microsoft.Identity.Web.Constants.Aliases = "aliases" -> string! const Microsoft.Identity.Web.Constants.ApiVersion = "api-version" -> string! const Microsoft.Identity.Web.Constants.ApplicationJson = "application/json" -> string! +const Microsoft.Identity.Web.Constants.ApplicationNotFound = "AADSTS700016" -> string! const Microsoft.Identity.Web.Constants.Authorization = "Authorization" -> string! const Microsoft.Identity.Web.Constants.AzureADIssuerMetadataUrl = "https://login.microsoftonline.com/common/discovery/instance?authorization_endpoint=https://login.microsoftonline.com/common/oauth2/v2.0/authorize&api-version=1.1" -> string! const Microsoft.Identity.Web.Constants.BlazorChallengeUri = "MicrosoftIdentity/Account/Challenge?redirectUri=" -> string! const Microsoft.Identity.Web.Constants.CertificateHasBeenRevoked = "AADSTS7000214" -> string! const Microsoft.Identity.Web.Constants.CertificateIsOutsideValidityWindow = "AADSTS1000502" -> string! +const Microsoft.Identity.Web.Constants.CertificateWasRevoked = "AADSTS7000277" -> string! const Microsoft.Identity.Web.Constants.CiamAuthoritySuffix = ".ciamlogin.com" -> string! const Microsoft.Identity.Web.Constants.ClientAssertion = "IDWEB_CLIENT_ASSERTION" -> string! +const Microsoft.Identity.Web.Constants.ClientAssertionContainsInvalidSignature = "AADSTS7000274" -> string! const Microsoft.Identity.Web.Constants.ClientInfo = "client_info" -> string! const Microsoft.Identity.Web.Constants.Common = "common" -> string! const Microsoft.Identity.Web.Constants.Consent = "consent" -> string! @@ -23,6 +26,7 @@ const Microsoft.Identity.Web.Constants.FmiPathForClientAssertion = "IDWEB_FMI_PA const Microsoft.Identity.Web.Constants.GraphBaseUrlV1 = "https://graph.microsoft.com/v1.0" -> string! const Microsoft.Identity.Web.Constants.IDWebSku = "IDWeb." -> string! const Microsoft.Identity.Web.Constants.InvalidClient = "invalid_client" -> string! +const Microsoft.Identity.Web.Constants.InvalidClientSecret = "AADSTS7000215" -> string! const Microsoft.Identity.Web.Constants.InvalidKeyError = "AADSTS700027" -> string! const Microsoft.Identity.Web.Constants.ISessionStore = "ISessionStore" -> string! const Microsoft.Identity.Web.Constants.JwtSecurityTokenUsedToCallWebApi = "JwtSecurityTokenUsedToCallWebAPI" -> string! @@ -49,6 +53,7 @@ const Microsoft.Identity.Web.Constants.TestSlice = "dc" -> string! const Microsoft.Identity.Web.Constants.True = "True" -> string! const Microsoft.Identity.Web.Constants.Upn = "upn" -> string! const Microsoft.Identity.Web.Constants.UserAgent = "User-Agent" -> string! +const Microsoft.Identity.Web.Constants.UserIdKey = "IDWEB_USER_ID" -> string! const Microsoft.Identity.Web.Constants.UsernameKey = "IDWEB_USERNAME" -> string! const Microsoft.Identity.Web.Constants.UserReadScope = "user.read" -> string! const Microsoft.Identity.Web.Constants.V1 = "1.0" -> string! @@ -73,8 +78,8 @@ const Microsoft.Identity.Web.IDWebErrorMessage.ClientCredentialTenantShouldBeTen const Microsoft.Identity.Web.IDWebErrorMessage.ClientInfoReturnedFromServerIsNull = "IDW10402: Client info returned from the server is null. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.ClientSecretAndCredentialsCannotBeCombined = "IDW10110: ClientSecret top level configuration cannot be combined with ClientCredentials. Instead, add a new entry in the ClientCredentials array describing the secret." -> string! const Microsoft.Identity.Web.IDWebErrorMessage.ConfigurationOptionRequired = "IDW10106: The '{0}' option must be provided. " -> string! -const Microsoft.Identity.Web.IDWebErrorMessage.ExceptionAcquiringTokenForConfidentialClient = "IDW10501: Exception acquiring token for a confidential client. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.ExceptionAcquiringTokenForConfidentialClient = "IDW10501: Exception acquiring token for a confidential client: " -> string! +const Microsoft.Identity.Web.IDWebErrorMessage.ExceptionAcquiringTokenForConfidentialClient = "IDW10501: Exception acquiring token for a confidential client. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.FromStoreWithThumprintIsObsolete = "IDW10803: Use FromStoreWithThumbprint instead, due to spelling error. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.HttpContextAndHttpResponseAreNull = "IDW10002: Current HttpContext and HttpResponse arguments are null. Pass an HttpResponse argument. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.HttpContextIsNull = "IDW10001: HttpContext is null. " -> string! @@ -88,9 +93,11 @@ const Microsoft.Identity.Web.IDWebErrorMessage.InvalidSubAssertion = "IDW10505: const Microsoft.Identity.Web.IDWebErrorMessage.IssuerDoesNotMatchValidIssuers = "IDW10303: Issuer: '{0}', does not match any of the valid issuers provided for this application. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.IssuerMetadataUrlIsRequired = "IDW10301: Azure AD Issuer metadata address URL is required. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.MicrosoftIdentityWebChallengeUserException = "IDW10502: An MsalUiRequiredException was thrown due to a challenge for the user. See https://aka.ms/ms-id-web/ca_incremental-consent. " -> string! +const Microsoft.Identity.Web.IDWebErrorMessage.MissingIdentityConfiguration = "IDW10708: The identity configuration is incomplete. Provide either 'Instance' and 'TenantId', or 'Authority', or enable 'ManagedIdentity' in the configuration. Check your configuration keys for typos (e.g., trailing spaces). See https://aka.ms/ms-id-web/configuration. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.MissingRequiredScopesForAuthorizationFilter = "IDW10108: RequiredScope Attribute does not contain a value. The scopes need to be set on the controller, the page or action. See https://aka.ms/ms-id-web/required-scope-attribute. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.MissingRoles = "IDW10202: The 'roles' or 'role' claim does not contain roles '{0}' or was not found. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.MissingScopes = "IDW10203: The 'scope' or 'scp' claim does not contain scopes '{0}' or was not found. " -> string! +const Microsoft.Identity.Web.IDWebErrorMessage.MissingTokenBindingCertificate = "IDW10115: A signing certificate, which is required for token binding, is missing in loaded credentials." -> string! const Microsoft.Identity.Web.IDWebErrorMessage.NeitherScopeOrRolesClaimFoundInToken = "IDW10201: Neither scope nor roles claim was found in the bearer token. Authentication scheme used: '{0}'. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.NoMetadataDocumentRetrieverProvided = "IDW10302: No metadata document retriever is provided. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.NoScopesProvided = "IDW10103: No scopes provided in scopes... " -> string! @@ -101,6 +108,7 @@ const Microsoft.Identity.Web.IDWebErrorMessage.ScopeKeySectionIsProvidedButNotPr const Microsoft.Identity.Web.IDWebErrorMessage.ScopesNotConfiguredInConfigurationOrViaDelegate = "IDW10107: Scopes need to be passed-in either by configuration or by the delegate overriding it. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.ScopesRequiredToCallMicrosoftGraph = "IDW10208: You need to either pass-in scopes to AddMicrosoftGraph, in the appsettings.json file, or with .WithScopes() on the Graph queries. See https://aka.ms/ms-id-web/microsoftGraph. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.TenantIdClaimNotPresentInToken = "IDW10401: Neither `tid` nor `tenantId` claim is present in the token obtained from Microsoft identity platform. " -> string! +const Microsoft.Identity.Web.IDWebErrorMessage.TokenBindingRequiresEnabledAppTokenAcquisition = "IDW10116: Token binding requires enabled app token acquisition." -> string! const Microsoft.Identity.Web.IDWebErrorMessage.TokenIsNotJwtToken = "IDW10403: Token is not a JWT token. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.UnauthenticatedUser = "IDW10204: The user is unauthenticated. The HttpContext does not contain any claims. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.WithClientCredentialsIsObsolete = "Use WithClientCredentialsAsync instead." -> string! @@ -157,6 +165,7 @@ Microsoft.Identity.Web.DefaultAuthorizationHeaderProvider Microsoft.Identity.Web.DefaultAuthorizationHeaderProvider.CreateAuthorizationHeaderAsync(System.Collections.Generic.IEnumerable! scopes, Microsoft.Identity.Abstractions.AuthorizationHeaderProviderOptions? downstreamApiOptions = null, System.Security.Claims.ClaimsPrincipal? claimsPrincipal = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DefaultAuthorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync(string! scopes, Microsoft.Identity.Abstractions.AuthorizationHeaderProviderOptions? downstreamApiOptions = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DefaultAuthorizationHeaderProvider.CreateAuthorizationHeaderForUserAsync(System.Collections.Generic.IEnumerable! scopes, Microsoft.Identity.Abstractions.AuthorizationHeaderProviderOptions? downstreamApiOptions = null, System.Security.Claims.ClaimsPrincipal? claimsPrincipal = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.DefaultAuthorizationHeaderProvider.CreateBoundAuthorizationHeaderAsync(Microsoft.Identity.Abstractions.DownstreamApiOptions! downstreamApiOptions, System.Security.Claims.ClaimsPrincipal? claimsPrincipal = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task>! Microsoft.Identity.Web.DefaultAuthorizationHeaderProvider.DefaultAuthorizationHeaderProvider(Microsoft.Identity.Web.ITokenAcquisition! tokenAcquisition) -> void Microsoft.Identity.Web.DefaultTokenAcquirerFactoryImplementation Microsoft.Identity.Web.DefaultTokenAcquirerFactoryImplementation.DefaultTokenAcquirerFactoryImplementation(System.IServiceProvider! serviceProvider) -> void @@ -224,6 +233,10 @@ Microsoft.Identity.Web.MergedOptions.EnablePiiLogging.get -> bool Microsoft.Identity.Web.MergedOptions.EnablePiiLogging.set -> void Microsoft.Identity.Web.MergedOptions.IsDefaultPlatformLoggingEnabled.get -> bool Microsoft.Identity.Web.MergedOptions.IsDefaultPlatformLoggingEnabled.set -> void +Microsoft.Identity.Web.MergedOptions.IsTokenBinding.get -> bool +Microsoft.Identity.Web.MergedOptions.IsTokenBinding.set -> void +Microsoft.Identity.Web.MergedOptions.Logger.get -> Microsoft.Extensions.Logging.ILogger? +Microsoft.Identity.Web.MergedOptions.Logger.set -> void Microsoft.Identity.Web.MergedOptions.LogLevel.get -> Microsoft.Identity.Client.LogLevel Microsoft.Identity.Web.MergedOptions.LogLevel.set -> void Microsoft.Identity.Web.MergedOptions.MergedOptions() -> void @@ -236,13 +249,18 @@ Microsoft.Identity.Web.MergedOptions.PreserveAuthority.get -> bool Microsoft.Identity.Web.MergedOptions.PreserveAuthority.set -> void Microsoft.Identity.Web.MergedOptions.RedirectUri.get -> string? Microsoft.Identity.Web.MergedOptions.RedirectUri.set -> void +Microsoft.Identity.Web.MergedOptionsLogging Microsoft.Identity.Web.MergedOptionsStore Microsoft.Identity.Web.MergedOptionsStore.Get(string! name) -> Microsoft.Identity.Web.MergedOptions! Microsoft.Identity.Web.MergedOptionsStore.MergedOptionsStore() -> void +Microsoft.Identity.Web.MergedOptionsStore.MergedOptionsStore(System.IServiceProvider! serviceProvider) -> void Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder(Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfigurationSection? configurationSection = null) -> void Microsoft.Identity.Web.MicrosoftIdentityApplicationOptionsMerger Microsoft.Identity.Web.MicrosoftIdentityApplicationOptionsMerger.MicrosoftIdentityApplicationOptionsMerger(Microsoft.Identity.Web.IMergedOptionsStore! mergedOptions) -> void Microsoft.Identity.Web.MicrosoftIdentityApplicationOptionsMerger.PostConfigure(string? name, Microsoft.Identity.Abstractions.MicrosoftIdentityApplicationOptions! options) -> void +Microsoft.Identity.Web.MicrosoftIdentityApplicationOptionsToMergedOptionsMerger +Microsoft.Identity.Web.MicrosoftIdentityApplicationOptionsToMergedOptionsMerger.MicrosoftIdentityApplicationOptionsToMergedOptionsMerger(Microsoft.Identity.Web.IMergedOptionsStore! mergedOptionsStore) -> void +Microsoft.Identity.Web.MicrosoftIdentityApplicationOptionsToMergedOptionsMerger.PostConfigure(string? name, Microsoft.Identity.Abstractions.MicrosoftIdentityApplicationOptions! options) -> void Microsoft.Identity.Web.MicrosoftIdentityOptions.HasClientCredentials.get -> bool Microsoft.Identity.Web.MicrosoftIdentityOptions.IsB2C.get -> bool Microsoft.Identity.Web.MicrosoftIdentityOptionsMerger @@ -265,7 +283,7 @@ Microsoft.Identity.Web.TokenAcquisition.GetAccessTokenForUserAsync(System.Collec Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForAppAsync(string! scope, string? authenticationScheme = null, string? tenant = null, Microsoft.Identity.Web.TokenAcquisitionOptions? tokenAcquisitionOptions = null) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForUserAsync(System.Collections.Generic.IEnumerable! scopes, string? authenticationScheme = null, string? tenantId = null, string? userFlow = null, System.Security.Claims.ClaimsPrincipal? user = null, Microsoft.Identity.Web.TokenAcquisitionOptions? tokenAcquisitionOptions = null) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.TokenAcquisition.GetEffectiveAuthenticationScheme(string? authenticationScheme) -> string! -Microsoft.Identity.Web.TokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(Microsoft.Identity.Web.MergedOptions! mergedOptions) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.TokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(Microsoft.Identity.Web.MergedOptions! mergedOptions, bool isTokenBinding) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.TokenAcquisition.GetOrBuildManagedIdentityApplicationAsync(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Abstractions.ManagedIdentityOptions! managedIdentityOptions) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.TokenAcquisition.Logger Microsoft.Identity.Web.TokenAcquisition.RemoveAccountAsync(System.Security.Claims.ClaimsPrincipal! user, string? authenticationScheme = null) -> System.Threading.Tasks.Task! @@ -285,11 +303,17 @@ Microsoft.Identity.Web.TokenAcquisitionAspnetCoreHost.GetUserFromRequest() -> Sy Microsoft.Identity.Web.TokenAcquisitionAspnetCoreHost.SetHttpResponse(System.Net.HttpStatusCode statusCode, string! wwwAuthenticate) -> void Microsoft.Identity.Web.TokenAcquisitionAspnetCoreHost.SetSession(string! key, string! value) -> void Microsoft.Identity.Web.TokenAcquisitionAspnetCoreHost.TokenAcquisitionAspnetCoreHost(Microsoft.AspNetCore.Http.IHttpContextAccessor! httpContextAccessor, Microsoft.Identity.Web.IMergedOptionsStore! mergedOptionsMonitor, System.IServiceProvider! serviceProvider) -> void +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeOnBehalfOfInitializedAsync(Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForApp(Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions) -> void +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForOnBehalfOf(Microsoft.Identity.Client.AcquireTokenOnBehalfOfParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> void +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForOnBehalfOf(Microsoft.Identity.Client.AcquireTokenOnBehalfOfParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> void +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForOnBehalfOfAsync(Microsoft.Identity.Client.AcquireTokenOnBehalfOfParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForOnBehalfOfAsync(Microsoft.Identity.Client.AcquireTokenOnBehalfOfParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForTestUser(Microsoft.Identity.Client.AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> void Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForTestUserAsync(Microsoft.Identity.Client.AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.Util.Base64UrlHelpers readonly Microsoft.Identity.Web.TokenAcquisition._certificatesObserver -> Microsoft.Identity.Web.Experimental.ICertificatesObserver? +readonly Microsoft.Identity.Web.TokenAcquisition._certificatesObservers -> System.Collections.Generic.IReadOnlyList! readonly Microsoft.Identity.Web.TokenAcquisition._credentialsLoader -> Microsoft.Identity.Abstractions.ICredentialsLoader! readonly Microsoft.Identity.Web.TokenAcquisition._httpClientFactory -> Microsoft.Identity.Client.IMsalHttpClientFactory! readonly Microsoft.Identity.Web.TokenAcquisition._logger -> Microsoft.Extensions.Logging.ILogger! @@ -306,22 +330,25 @@ static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logg static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.FailedToLoadCredentials(Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Abstractions.CredentialDescription! certificateDescription) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.NotUsingManagedIdentity(Microsoft.Extensions.Logging.ILogger! logger, string! message) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingCertThumbprint(Microsoft.Extensions.Logging.ILogger! logger, string! certThumbprint) -> void +static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingCertThumbprint(Microsoft.Extensions.Logging.ILogger! logger, string? certThumbprint) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingManagedIdentity(Microsoft.Extensions.Logging.ILogger! logger) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingPodIdentityFile(Microsoft.Extensions.Logging.ILogger! logger, string! signedAssertionFileDiskPath) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingSignedAssertionFromCustomProvider(Microsoft.Extensions.Logging.ILogger! logger, string! signedAssertionUri) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingSignedAssertionFromVault(Microsoft.Extensions.Logging.ILogger! logger, string! signedAssertionUri) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.WithClientCredentials(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder! builder, System.Collections.Generic.IEnumerable! clientCredentials, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Abstractions.ICredentialsLoader! credentialsLoader, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters! credentialSourceLoaderParameters) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder! -static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.WithClientCredentialsAsync(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder! builder, System.Collections.Generic.IEnumerable! clientCredentials, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Abstractions.ICredentialsLoader! credentialsLoader, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters) -> System.Threading.Tasks.Task! +static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.WithClientCredentialsAsync(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder! builder, System.Collections.Generic.IEnumerable! clientCredentials, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Abstractions.ICredentialsLoader! credentialsLoader, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, bool isTokenBinding) -> System.Threading.Tasks.Task! static Microsoft.Identity.Web.DefaultTokenAcquirerFactoryImplementation.GetKey(string? authority, string? clientId, string? region) -> string! static Microsoft.Identity.Web.HttpContextExtensions.GetTokenUsedToCallWebAPI(this Microsoft.AspNetCore.Http.HttpContext! httpContext) -> Microsoft.IdentityModel.Tokens.SecurityToken? static Microsoft.Identity.Web.HttpContextExtensions.StoreTokenUsedToCallWebAPI(this Microsoft.AspNetCore.Http.HttpContext! httpContext, Microsoft.IdentityModel.Tokens.SecurityToken? token) -> void static Microsoft.Identity.Web.IdHelper.CreateTelemetryInfo() -> string! +static Microsoft.Identity.Web.MergedOptions.ParseAuthorityIfNecessary(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Extensions.Logging.ILogger? logger = null) -> void static Microsoft.Identity.Web.MergedOptions.ParseAuthorityIfNecessary(Microsoft.Identity.Web.MergedOptions! mergedOptions) -> void static Microsoft.Identity.Web.MergedOptions.UpdateConfidentialClientApplicationOptionsFromMergedOptions(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Client.ConfidentialClientApplicationOptions! confidentialClientApplicationOptions) -> void static Microsoft.Identity.Web.MergedOptions.UpdateMergedOptionsFromConfidentialClientApplicationOptions(Microsoft.Identity.Client.ConfidentialClientApplicationOptions! confidentialClientApplicationOptions, Microsoft.Identity.Web.MergedOptions! mergedOptions) -> void static Microsoft.Identity.Web.MergedOptions.UpdateMergedOptionsFromJwtBearerOptions(Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions! jwtBearerOptions, Microsoft.Identity.Web.MergedOptions! mergedOptions) -> void static Microsoft.Identity.Web.MergedOptions.UpdateMergedOptionsFromMicrosoftIdentityApplicationOptions(Microsoft.Identity.Abstractions.MicrosoftIdentityApplicationOptions! microsoftIdentityApplicationOptions, Microsoft.Identity.Web.MergedOptions! mergedOptions) -> void static Microsoft.Identity.Web.MergedOptions.UpdateMergedOptionsFromMicrosoftIdentityOptions(Microsoft.Identity.Web.MicrosoftIdentityOptions! microsoftIdentityOptions, Microsoft.Identity.Web.MergedOptions! mergedOptions) -> void +static Microsoft.Identity.Web.MergedOptionsLogging.AuthorityIgnored(Microsoft.Extensions.Logging.ILogger! logger, string! authority, string! instance, string! tenantId) -> void static Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder.SetIdentityModelLogger(System.IServiceProvider! serviceProvider) -> void static Microsoft.Identity.Web.MsAuth10AtPop.WithAtPop(this Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! builder, string! popPublicKey, string! jwkClaim) -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! static Microsoft.Identity.Web.MsAuth10AtPop.WithAtPop(this Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! builder, System.Security.Cryptography.X509Certificates.X509Certificate2! clientCertificate, string! popPublicKey, string! jwkClaim, string! clientId, bool sendX5C) -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! @@ -332,12 +359,16 @@ static Microsoft.Identity.Web.TokenAcquisition.GetCacheKeyForManagedId(Microsoft static Microsoft.Identity.Web.TokenAcquisition.Logger.TokenAcquisitionError(Microsoft.Extensions.Logging.ILogger! logger, string! msalErrorMessage, System.Exception? ex) -> void static Microsoft.Identity.Web.TokenAcquisition.Logger.TokenAcquisitionMsalAuthenticationResultTime(Microsoft.Extensions.Logging.ILogger! logger, long durationTotalInMs, long durationInHttpInMs, long durationInCacheInMs, string! tokenSource, string! correlationId, string! cacheRefreshReason, System.Exception? ex) -> void static Microsoft.Identity.Web.TokenAcquisition.MergeExtraQueryParameters(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Web.TokenAcquisitionOptions! tokenAcquisitionOptions) -> System.Collections.Generic.Dictionary? +static Microsoft.Identity.Web.TokenAcquisition.MergeExtraQueryParameters(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Web.TokenAcquisitionOptions? tokenAcquisitionOptions) -> System.Collections.Generic.Dictionary? static Microsoft.Identity.Web.TokenAcquisition.ResolveTenant(string? tenant, Microsoft.Identity.Web.MergedOptions! mergedOptions) -> string? static Microsoft.Identity.Web.Util.Base64UrlHelpers.Decode(string! arg) -> string? static Microsoft.Identity.Web.Util.Base64UrlHelpers.DecodeBytes(string? str) -> byte[]? static Microsoft.Identity.Web.Util.Base64UrlHelpers.Encode(byte[]? inArray) -> string? static Microsoft.Identity.Web.Util.Base64UrlHelpers.Encode(string? arg) -> string? static Microsoft.Identity.Web.Util.Base64UrlHelpers.EncodeString(string? str) -> string? +static readonly Microsoft.Identity.Web.Constants.s_certificateRelatedErrorCodes -> System.Collections.Generic.HashSet! +static readonly Microsoft.Identity.Web.Constants.s_nonRetryableConfigErrorCodes -> System.Collections.Generic.HashSet! +static readonly Microsoft.Identity.Web.LoggingEventId.AuthorityIgnored -> Microsoft.Extensions.Logging.EventId static readonly Microsoft.Identity.Web.LoggingEventId.CredentialLoadAttempt -> Microsoft.Extensions.Logging.EventId static readonly Microsoft.Identity.Web.LoggingEventId.CredentialLoadAttemptFailed -> Microsoft.Extensions.Logging.EventId static readonly Microsoft.Identity.Web.LoggingEventId.NotUsingManagedIdentity -> Microsoft.Extensions.Logging.EventId diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/InternalAPI.Unshipped.txt index ec8e30c95..7dc5c5811 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/InternalAPI.Unshipped.txt @@ -1,4 +1 @@ #nullable enable -const Microsoft.Identity.Web.Constants.UserIdKey = "IDWEB_USER_ID" -> string! -readonly Microsoft.Identity.Web.TokenAcquisition._certificatesObservers -> System.Collections.Generic.IReadOnlyList! -static Microsoft.Identity.Web.TokenAcquisition.MergeExtraQueryParameters(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Web.TokenAcquisitionOptions? tokenAcquisitionOptions) -> System.Collections.Generic.Dictionary? diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/PublicAPI.Shipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/PublicAPI.Shipped.txt index 63e8bdb52..fed3386fe 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/PublicAPI.Shipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/PublicAPI.Shipped.txt @@ -32,7 +32,11 @@ const Microsoft.Identity.Web.Constants.ReturnUrl = "ReturnUrl" -> string! const Microsoft.Identity.Web.Constants.Scope = "scope" -> string! const Microsoft.Identity.Web.Constants.SpaAuthCode = "SpaAuthCode" -> string! Microsoft.Identity.Web.ApplicationBuilderExtensions +Microsoft.Identity.Web.BeforeOnBehalfOfInitialized +Microsoft.Identity.Web.BeforeOnBehalfOfInitializedAsync Microsoft.Identity.Web.BeforeTokenAcquisitionForApp +Microsoft.Identity.Web.BeforeTokenAcquisitionForOnBehalfOf +Microsoft.Identity.Web.BeforeTokenAcquisitionForOnBehalfOfAsync Microsoft.Identity.Web.BeforeTokenAcquisitionForTestUser Microsoft.Identity.Web.BeforeTokenAcquisitionForTestUserAsync Microsoft.Identity.Web.ClaimConstants @@ -40,6 +44,7 @@ Microsoft.Identity.Web.Constants Microsoft.Identity.Web.Experimental.CerticateObserverAction Microsoft.Identity.Web.Experimental.CerticateObserverAction.Deselected = 1 -> Microsoft.Identity.Web.Experimental.CerticateObserverAction Microsoft.Identity.Web.Experimental.CerticateObserverAction.Selected = 0 -> Microsoft.Identity.Web.Experimental.CerticateObserverAction +Microsoft.Identity.Web.Experimental.CerticateObserverAction.SuccessfullyUsed = 2 -> Microsoft.Identity.Web.Experimental.CerticateObserverAction Microsoft.Identity.Web.Experimental.CertificateChangeEventArg Microsoft.Identity.Web.Experimental.CertificateChangeEventArg.Action.get -> Microsoft.Identity.Web.Experimental.CerticateObserverAction Microsoft.Identity.Web.Experimental.CertificateChangeEventArg.Action.set -> void @@ -54,6 +59,7 @@ Microsoft.Identity.Web.Experimental.ICertificatesObserver Microsoft.Identity.Web.Experimental.ICertificatesObserver.OnClientCertificateChanged(Microsoft.Identity.Web.Experimental.CertificateChangeEventArg! e) -> void Microsoft.Identity.Web.Extensibility.BaseAuthorizationHeaderProvider Microsoft.Identity.Web.Extensibility.BaseAuthorizationHeaderProvider.BaseAuthorizationHeaderProvider(System.IServiceProvider! serviceProvider) -> void +Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions Microsoft.Identity.Web.IAuthenticationSchemeInformationProvider Microsoft.Identity.Web.Internal.WebApiBuilders Microsoft.Identity.Web.ITokenAcquisition @@ -72,11 +78,21 @@ Microsoft.Identity.Web.ITokenAcquisition.ReplyForbiddenWithWwwAuthenticateHeader Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder.AddDistributedTokenCaches() -> Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder! Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder.AddInMemoryTokenCaches(System.Action? configureOptions = null, System.Action? memoryCacheOptions = null) -> Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder! +Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException +Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException.MicrosoftIdentityAuthenticationException(string! message, System.Exception! innerException) -> void +Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException.MicrosoftIdentityAuthenticationException(string! message) -> void Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder.ConfigurationSection.get -> Microsoft.Extensions.Configuration.IConfigurationSection? Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder.ConfigurationSection.set -> void Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder.MicrosoftIdentityBaseAuthenticationBuilder(Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfigurationSection? configurationSection = null) -> void Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder.Services.get -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions +Microsoft.Identity.Web.MicrosoftIdentityMessageHandler +Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.MicrosoftIdentityMessageHandler(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! headerProvider, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? defaultOptions = null, Microsoft.Extensions.Logging.ILogger? logger = null) -> void +Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions +Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.MicrosoftIdentityMessageHandlerOptions() -> void +Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.get -> System.Collections.Generic.IList! +Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.set -> void Microsoft.Identity.Web.MicrosoftIdentityOptions Microsoft.Identity.Web.MicrosoftIdentityOptions.AllowWebApiToBeAuthorizedByACL.get -> bool Microsoft.Identity.Web.MicrosoftIdentityOptions.AllowWebApiToBeAuthorizedByACL.set -> void @@ -126,6 +142,17 @@ Microsoft.Identity.Web.MicrosoftIdentityWebChallengeUserException.Scopes.get -> Microsoft.Identity.Web.MicrosoftIdentityWebChallengeUserException.Scopes.set -> void Microsoft.Identity.Web.MicrosoftIdentityWebChallengeUserException.Userflow.get -> string? Microsoft.Identity.Web.MicrosoftIdentityWebChallengeUserException.Userflow.set -> void +Microsoft.Identity.Web.MsalMtlsHttpClientFactory +Microsoft.Identity.Web.MsalMtlsHttpClientFactory.Dispose() -> void +Microsoft.Identity.Web.MsalMtlsHttpClientFactory.GetHttpClient() -> System.Net.Http.HttpClient! +Microsoft.Identity.Web.MsalMtlsHttpClientFactory.GetHttpClient(System.Security.Cryptography.X509Certificates.X509Certificate2! x509Certificate2) -> System.Net.Http.HttpClient! +Microsoft.Identity.Web.MsalMtlsHttpClientFactory.MsalMtlsHttpClientFactory(System.Net.Http.IHttpClientFactory! httpClientFactory) -> void +Microsoft.Identity.Web.OnBehalfOfEventArgs +Microsoft.Identity.Web.OnBehalfOfEventArgs.OnBehalfOfEventArgs() -> void +Microsoft.Identity.Web.OnBehalfOfEventArgs.User.get -> System.Security.Claims.ClaimsPrincipal? +Microsoft.Identity.Web.OnBehalfOfEventArgs.User.set -> void +Microsoft.Identity.Web.OnBehalfOfEventArgs.UserAssertionToken.get -> string? +Microsoft.Identity.Web.OnBehalfOfEventArgs.UserAssertionToken.set -> void Microsoft.Identity.Web.PrincipalExtensionsForSecurityTokens Microsoft.Identity.Web.ServiceCollectionExtensions Microsoft.Identity.Web.TestOnly.TokenAcquirerFactoryTesting @@ -142,7 +169,11 @@ Microsoft.Identity.Web.TokenAcquirerFactory.ServiceProvider.set -> void Microsoft.Identity.Web.TokenAcquirerFactory.Services.get -> Microsoft.Extensions.DependencyInjection.ServiceCollection! Microsoft.Identity.Web.TokenAcquirerFactory.TokenAcquirerFactory() -> void Microsoft.Identity.Web.TokenAcquisitionExtensionOptions +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeOnBehalfOfInitialized -> Microsoft.Identity.Web.BeforeOnBehalfOfInitialized? +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeOnBehalfOfInitializedAsync -> Microsoft.Identity.Web.BeforeOnBehalfOfInitializedAsync? Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeTokenAcquisitionForApp -> Microsoft.Identity.Web.BeforeTokenAcquisitionForApp? +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeTokenAcquisitionForOnBehalfOf -> Microsoft.Identity.Web.BeforeTokenAcquisitionForOnBehalfOf? +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeTokenAcquisitionForOnBehalfOfAsync -> Microsoft.Identity.Web.BeforeTokenAcquisitionForOnBehalfOfAsync? Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeTokenAcquisitionForTestUser -> Microsoft.Identity.Web.BeforeTokenAcquisitionForTestUser? Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeTokenAcquisitionForTestUserAsync -> Microsoft.Identity.Web.BeforeTokenAcquisitionForTestUserAsync? Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.TokenAcquisitionExtensionOptions() -> void @@ -153,8 +184,16 @@ Microsoft.Identity.Web.TokenAcquisitionOptions.Clone() -> Microsoft.Identity.Web Microsoft.Identity.Web.TokenAcquisitionOptions.PoPConfiguration.get -> Microsoft.Identity.Client.AppConfig.PoPAuthenticationConfiguration? Microsoft.Identity.Web.TokenAcquisitionOptions.PoPConfiguration.set -> void Microsoft.Identity.Web.TokenAcquisitionOptions.TokenAcquisitionOptions() -> void +override Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.SendAsync(System.Net.Http.HttpRequestMessage! request, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! static Microsoft.Identity.Web.ApplicationBuilderExtensions.UseTokenAcquirerFactory(this Microsoft.AspNetCore.Builder.IApplicationBuilder! applicationBuilder) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! +static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.GetAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request) -> Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? +static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! options) -> System.Net.Http.HttpRequestMessage! +static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, System.Action! configure) -> System.Net.Http.HttpRequestMessage! static Microsoft.Identity.Web.Internal.WebApiBuilders.EnableTokenAcquisition(System.Action! configureConfidentialClientApplicationOptions, string! authenticationScheme, Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfigurationSection? configuration) -> Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder, Microsoft.Extensions.Configuration.IConfiguration! configuration, string! sectionName) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! options) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! static Microsoft.Identity.Web.PrincipalExtensionsForSecurityTokens.GetBootstrapToken(this System.Security.Principal.IPrincipal! claimsPrincipal) -> Microsoft.IdentityModel.Tokens.SecurityToken? static Microsoft.Identity.Web.ServiceCollectionExtensions.AddTokenAcquisition(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, bool isTokenAcquisitionSingleton = false) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.Identity.Web.TestOnly.TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest() -> void @@ -162,7 +201,11 @@ static Microsoft.Identity.Web.TokenAcquirerExtensions.GetFicTokenAsync(this Micr static Microsoft.Identity.Web.TokenAcquirerExtensions.WithClientAssertion(this Microsoft.Identity.Abstractions.AcquireTokenOptions! options, string! clientAssertion) -> Microsoft.Identity.Abstractions.AcquireTokenOptions! static Microsoft.Identity.Web.TokenAcquirerFactory.GetDefaultInstance(string! configSection = "AzureAd") -> Microsoft.Identity.Web.TokenAcquirerFactory! static Microsoft.Identity.Web.TokenAcquirerFactory.GetDefaultInstance(string! configSection = "AzureAd") -> T! +virtual Microsoft.Identity.Web.BeforeOnBehalfOfInitialized.Invoke(Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> void +virtual Microsoft.Identity.Web.BeforeOnBehalfOfInitializedAsync.Invoke(Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> System.Threading.Tasks.Task! virtual Microsoft.Identity.Web.BeforeTokenAcquisitionForApp.Invoke(Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions) -> void +virtual Microsoft.Identity.Web.BeforeTokenAcquisitionForOnBehalfOf.Invoke(Microsoft.Identity.Client.AcquireTokenOnBehalfOfParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> void +virtual Microsoft.Identity.Web.BeforeTokenAcquisitionForOnBehalfOfAsync.Invoke(Microsoft.Identity.Client.AcquireTokenOnBehalfOfParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> System.Threading.Tasks.Task! virtual Microsoft.Identity.Web.BeforeTokenAcquisitionForTestUser.Invoke(Microsoft.Identity.Client.AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> void virtual Microsoft.Identity.Web.BeforeTokenAcquisitionForTestUserAsync.Invoke(Microsoft.Identity.Client.AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> System.Threading.Tasks.Task! virtual Microsoft.Identity.Web.Extensibility.BaseAuthorizationHeaderProvider.CreateAuthorizationHeaderAsync(System.Collections.Generic.IEnumerable! scopes, Microsoft.Identity.Abstractions.AuthorizationHeaderProviderOptions? authorizationHeaderProviderOptions = null, System.Security.Claims.ClaimsPrincipal? claimsPrincipal = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/PublicAPI.Unshipped.txt index ec706b293..7dc5c5811 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net10.0/PublicAPI.Unshipped.txt @@ -1,16 +1 @@ #nullable enable -Microsoft.Identity.Web.Experimental.CerticateObserverAction.SuccessfullyUsed = 2 -> Microsoft.Identity.Web.Experimental.CerticateObserverAction -Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions -Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException -Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException.MicrosoftIdentityAuthenticationException(string! message) -> void -Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException.MicrosoftIdentityAuthenticationException(string! message, System.Exception! innerException) -> void -Microsoft.Identity.Web.MicrosoftIdentityMessageHandler -Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.MicrosoftIdentityMessageHandler(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! headerProvider, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? defaultOptions = null, Microsoft.Extensions.Logging.ILogger? logger = null) -> void -Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions -Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.MicrosoftIdentityMessageHandlerOptions() -> void -Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.get -> System.Collections.Generic.IList! -Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.set -> void -override Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.SendAsync(System.Net.Http.HttpRequestMessage! request, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.GetAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request) -> Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? -static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! options) -> System.Net.Http.HttpRequestMessage! -static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, System.Action! configure) -> System.Net.Http.HttpRequestMessage! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/InternalAPI.Shipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/InternalAPI.Shipped.txt index 3f80e4a15..59e23a3ce 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/InternalAPI.Shipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/InternalAPI.Shipped.txt @@ -3,13 +3,16 @@ const Microsoft.Identity.Web.Constants.AgentIdentityKey = "IDWEB_AGENT_IDENTITY" const Microsoft.Identity.Web.Constants.Aliases = "aliases" -> string! const Microsoft.Identity.Web.Constants.ApiVersion = "api-version" -> string! const Microsoft.Identity.Web.Constants.ApplicationJson = "application/json" -> string! +const Microsoft.Identity.Web.Constants.ApplicationNotFound = "AADSTS700016" -> string! const Microsoft.Identity.Web.Constants.Authorization = "Authorization" -> string! const Microsoft.Identity.Web.Constants.AzureADIssuerMetadataUrl = "https://login.microsoftonline.com/common/discovery/instance?authorization_endpoint=https://login.microsoftonline.com/common/oauth2/v2.0/authorize&api-version=1.1" -> string! const Microsoft.Identity.Web.Constants.BlazorChallengeUri = "MicrosoftIdentity/Account/Challenge?redirectUri=" -> string! const Microsoft.Identity.Web.Constants.CertificateHasBeenRevoked = "AADSTS7000214" -> string! const Microsoft.Identity.Web.Constants.CertificateIsOutsideValidityWindow = "AADSTS1000502" -> string! +const Microsoft.Identity.Web.Constants.CertificateWasRevoked = "AADSTS7000277" -> string! const Microsoft.Identity.Web.Constants.CiamAuthoritySuffix = ".ciamlogin.com" -> string! const Microsoft.Identity.Web.Constants.ClientAssertion = "IDWEB_CLIENT_ASSERTION" -> string! +const Microsoft.Identity.Web.Constants.ClientAssertionContainsInvalidSignature = "AADSTS7000274" -> string! const Microsoft.Identity.Web.Constants.ClientInfo = "client_info" -> string! const Microsoft.Identity.Web.Constants.Common = "common" -> string! const Microsoft.Identity.Web.Constants.Consent = "consent" -> string! @@ -23,6 +26,7 @@ const Microsoft.Identity.Web.Constants.FmiPathForClientAssertion = "IDWEB_FMI_PA const Microsoft.Identity.Web.Constants.GraphBaseUrlV1 = "https://graph.microsoft.com/v1.0" -> string! const Microsoft.Identity.Web.Constants.IDWebSku = "IDWeb." -> string! const Microsoft.Identity.Web.Constants.InvalidClient = "invalid_client" -> string! +const Microsoft.Identity.Web.Constants.InvalidClientSecret = "AADSTS7000215" -> string! const Microsoft.Identity.Web.Constants.InvalidKeyError = "AADSTS700027" -> string! const Microsoft.Identity.Web.Constants.ISessionStore = "ISessionStore" -> string! const Microsoft.Identity.Web.Constants.JwtSecurityTokenUsedToCallWebApi = "JwtSecurityTokenUsedToCallWebAPI" -> string! @@ -89,9 +93,11 @@ const Microsoft.Identity.Web.IDWebErrorMessage.InvalidSubAssertion = "IDW10505: const Microsoft.Identity.Web.IDWebErrorMessage.IssuerDoesNotMatchValidIssuers = "IDW10303: Issuer: '{0}', does not match any of the valid issuers provided for this application. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.IssuerMetadataUrlIsRequired = "IDW10301: Azure AD Issuer metadata address URL is required. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.MicrosoftIdentityWebChallengeUserException = "IDW10502: An MsalUiRequiredException was thrown due to a challenge for the user. See https://aka.ms/ms-id-web/ca_incremental-consent. " -> string! +const Microsoft.Identity.Web.IDWebErrorMessage.MissingIdentityConfiguration = "IDW10708: The identity configuration is incomplete. Provide either 'Instance' and 'TenantId', or 'Authority', or enable 'ManagedIdentity' in the configuration. Check your configuration keys for typos (e.g., trailing spaces). See https://aka.ms/ms-id-web/configuration. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.MissingRequiredScopesForAuthorizationFilter = "IDW10108: RequiredScope Attribute does not contain a value. The scopes need to be set on the controller, the page or action. See https://aka.ms/ms-id-web/required-scope-attribute. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.MissingRoles = "IDW10202: The 'roles' or 'role' claim does not contain roles '{0}' or was not found. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.MissingScopes = "IDW10203: The 'scope' or 'scp' claim does not contain scopes '{0}' or was not found. " -> string! +const Microsoft.Identity.Web.IDWebErrorMessage.MissingTokenBindingCertificate = "IDW10115: A signing certificate, which is required for token binding, is missing in loaded credentials." -> string! const Microsoft.Identity.Web.IDWebErrorMessage.NeitherScopeOrRolesClaimFoundInToken = "IDW10201: Neither scope nor roles claim was found in the bearer token. Authentication scheme used: '{0}'. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.NoMetadataDocumentRetrieverProvided = "IDW10302: No metadata document retriever is provided. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.NoScopesProvided = "IDW10103: No scopes provided in scopes... " -> string! @@ -102,6 +108,7 @@ const Microsoft.Identity.Web.IDWebErrorMessage.ScopeKeySectionIsProvidedButNotPr const Microsoft.Identity.Web.IDWebErrorMessage.ScopesNotConfiguredInConfigurationOrViaDelegate = "IDW10107: Scopes need to be passed-in either by configuration or by the delegate overriding it. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.ScopesRequiredToCallMicrosoftGraph = "IDW10208: You need to either pass-in scopes to AddMicrosoftGraph, in the appsettings.json file, or with .WithScopes() on the Graph queries. See https://aka.ms/ms-id-web/microsoftGraph. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.TenantIdClaimNotPresentInToken = "IDW10401: Neither `tid` nor `tenantId` claim is present in the token obtained from Microsoft identity platform. " -> string! +const Microsoft.Identity.Web.IDWebErrorMessage.TokenBindingRequiresEnabledAppTokenAcquisition = "IDW10116: Token binding requires enabled app token acquisition." -> string! const Microsoft.Identity.Web.IDWebErrorMessage.TokenIsNotJwtToken = "IDW10403: Token is not a JWT token. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.UnauthenticatedUser = "IDW10204: The user is unauthenticated. The HttpContext does not contain any claims. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.WithClientCredentialsIsObsolete = "Use WithClientCredentialsAsync instead." -> string! @@ -157,6 +164,7 @@ Microsoft.Identity.Web.DefaultAuthorizationHeaderProvider Microsoft.Identity.Web.DefaultAuthorizationHeaderProvider.CreateAuthorizationHeaderAsync(System.Collections.Generic.IEnumerable! scopes, Microsoft.Identity.Abstractions.AuthorizationHeaderProviderOptions? downstreamApiOptions = null, System.Security.Claims.ClaimsPrincipal? claimsPrincipal = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DefaultAuthorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync(string! scopes, Microsoft.Identity.Abstractions.AuthorizationHeaderProviderOptions? downstreamApiOptions = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DefaultAuthorizationHeaderProvider.CreateAuthorizationHeaderForUserAsync(System.Collections.Generic.IEnumerable! scopes, Microsoft.Identity.Abstractions.AuthorizationHeaderProviderOptions? downstreamApiOptions = null, System.Security.Claims.ClaimsPrincipal? claimsPrincipal = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.DefaultAuthorizationHeaderProvider.CreateBoundAuthorizationHeaderAsync(Microsoft.Identity.Abstractions.DownstreamApiOptions! downstreamApiOptions, System.Security.Claims.ClaimsPrincipal? claimsPrincipal = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task>! Microsoft.Identity.Web.DefaultAuthorizationHeaderProvider.DefaultAuthorizationHeaderProvider(Microsoft.Identity.Web.ITokenAcquisition! tokenAcquisition) -> void Microsoft.Identity.Web.DefaultTokenAcquirerFactoryImplementation Microsoft.Identity.Web.DefaultTokenAcquirerFactoryImplementation.DefaultTokenAcquirerFactoryImplementation(System.IServiceProvider! serviceProvider) -> void @@ -220,6 +228,10 @@ Microsoft.Identity.Web.MergedOptions.EnablePiiLogging.get -> bool Microsoft.Identity.Web.MergedOptions.EnablePiiLogging.set -> void Microsoft.Identity.Web.MergedOptions.IsDefaultPlatformLoggingEnabled.get -> bool Microsoft.Identity.Web.MergedOptions.IsDefaultPlatformLoggingEnabled.set -> void +Microsoft.Identity.Web.MergedOptions.IsTokenBinding.get -> bool +Microsoft.Identity.Web.MergedOptions.IsTokenBinding.set -> void +Microsoft.Identity.Web.MergedOptions.Logger.get -> Microsoft.Extensions.Logging.ILogger? +Microsoft.Identity.Web.MergedOptions.Logger.set -> void Microsoft.Identity.Web.MergedOptions.LogLevel.get -> Microsoft.Identity.Client.LogLevel Microsoft.Identity.Web.MergedOptions.LogLevel.set -> void Microsoft.Identity.Web.MergedOptions.MergedOptions() -> void @@ -232,9 +244,11 @@ Microsoft.Identity.Web.MergedOptions.PreserveAuthority.get -> bool Microsoft.Identity.Web.MergedOptions.PreserveAuthority.set -> void Microsoft.Identity.Web.MergedOptions.RedirectUri.get -> string? Microsoft.Identity.Web.MergedOptions.RedirectUri.set -> void +Microsoft.Identity.Web.MergedOptionsLogging Microsoft.Identity.Web.MergedOptionsStore Microsoft.Identity.Web.MergedOptionsStore.Get(string! name) -> Microsoft.Identity.Web.MergedOptions! Microsoft.Identity.Web.MergedOptionsStore.MergedOptionsStore() -> void +Microsoft.Identity.Web.MergedOptionsStore.MergedOptionsStore(System.IServiceProvider! serviceProvider) -> void Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder(Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfigurationSection? configurationSection = null) -> void Microsoft.Identity.Web.MicrosoftIdentityApplicationOptionsMerger Microsoft.Identity.Web.MicrosoftIdentityApplicationOptionsMerger.MicrosoftIdentityApplicationOptionsMerger(Microsoft.Identity.Web.IMergedOptionsStore! mergedOptions) -> void @@ -261,12 +275,15 @@ Microsoft.Identity.Web.TokenAcquisition.GetAccessTokenForUserAsync(System.Collec Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForAppAsync(string! scope, string? authenticationScheme = null, string? tenant = null, Microsoft.Identity.Web.TokenAcquisitionOptions? tokenAcquisitionOptions = null) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForUserAsync(System.Collections.Generic.IEnumerable! scopes, string? authenticationScheme = null, string? tenantId = null, string? userFlow = null, System.Security.Claims.ClaimsPrincipal? user = null, Microsoft.Identity.Web.TokenAcquisitionOptions? tokenAcquisitionOptions = null) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.TokenAcquisition.GetEffectiveAuthenticationScheme(string? authenticationScheme) -> string! -Microsoft.Identity.Web.TokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(Microsoft.Identity.Web.MergedOptions! mergedOptions) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.TokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(Microsoft.Identity.Web.MergedOptions! mergedOptions, bool isTokenBinding) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.TokenAcquisition.GetOrBuildManagedIdentityApplicationAsync(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Abstractions.ManagedIdentityOptions! managedIdentityOptions) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.TokenAcquisition.Logger Microsoft.Identity.Web.TokenAcquisition.RemoveAccountAsync(System.Security.Claims.ClaimsPrincipal! user, string? authenticationScheme = null) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.TokenAcquisition.TokenAcquisition(Microsoft.Identity.Web.TokenCacheProviders.IMsalTokenCacheProvider! tokenCacheProvider, Microsoft.Identity.Web.ITokenAcquisitionHost! tokenAcquisitionHost, System.Net.Http.IHttpClientFactory! httpClientFactory, Microsoft.Extensions.Logging.ILogger! logger, System.IServiceProvider! serviceProvider, Microsoft.Identity.Abstractions.ICredentialsLoader! credentialsLoader) -> void +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeOnBehalfOfInitializedAsync(Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForApp(Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions) -> void +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForOnBehalfOf(Microsoft.Identity.Client.AcquireTokenOnBehalfOfParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> void +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForOnBehalfOfAsync(Microsoft.Identity.Client.AcquireTokenOnBehalfOfParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForTestUser(Microsoft.Identity.Client.AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> void Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForTestUserAsync(Microsoft.Identity.Client.AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.Util.Base64UrlHelpers @@ -288,19 +305,22 @@ static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logg static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.FailedToLoadCredentials(Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Abstractions.CredentialDescription! certificateDescription) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.NotUsingManagedIdentity(Microsoft.Extensions.Logging.ILogger! logger, string! message) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingCertThumbprint(Microsoft.Extensions.Logging.ILogger! logger, string! certThumbprint) -> void +static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingCertThumbprint(Microsoft.Extensions.Logging.ILogger! logger, string? certThumbprint) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingManagedIdentity(Microsoft.Extensions.Logging.ILogger! logger) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingPodIdentityFile(Microsoft.Extensions.Logging.ILogger! logger, string! signedAssertionFileDiskPath) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingSignedAssertionFromCustomProvider(Microsoft.Extensions.Logging.ILogger! logger, string! signedAssertionUri) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingSignedAssertionFromVault(Microsoft.Extensions.Logging.ILogger! logger, string! signedAssertionUri) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.WithClientCredentials(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder! builder, System.Collections.Generic.IEnumerable! clientCredentials, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Abstractions.ICredentialsLoader! credentialsLoader, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters! credentialSourceLoaderParameters) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder! -static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.WithClientCredentialsAsync(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder! builder, System.Collections.Generic.IEnumerable! clientCredentials, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Abstractions.ICredentialsLoader! credentialsLoader, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters) -> System.Threading.Tasks.Task! +static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.WithClientCredentialsAsync(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder! builder, System.Collections.Generic.IEnumerable! clientCredentials, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Abstractions.ICredentialsLoader! credentialsLoader, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, bool isTokenBinding) -> System.Threading.Tasks.Task! static Microsoft.Identity.Web.DefaultTokenAcquirerFactoryImplementation.GetKey(string? authority, string? clientId, string? region) -> string! static Microsoft.Identity.Web.IdHelper.CreateTelemetryInfo() -> string! +static Microsoft.Identity.Web.MergedOptions.ParseAuthorityIfNecessary(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Extensions.Logging.ILogger? logger = null) -> void static Microsoft.Identity.Web.MergedOptions.ParseAuthorityIfNecessary(Microsoft.Identity.Web.MergedOptions! mergedOptions) -> void static Microsoft.Identity.Web.MergedOptions.UpdateConfidentialClientApplicationOptionsFromMergedOptions(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Client.ConfidentialClientApplicationOptions! confidentialClientApplicationOptions) -> void static Microsoft.Identity.Web.MergedOptions.UpdateMergedOptionsFromConfidentialClientApplicationOptions(Microsoft.Identity.Client.ConfidentialClientApplicationOptions! confidentialClientApplicationOptions, Microsoft.Identity.Web.MergedOptions! mergedOptions) -> void static Microsoft.Identity.Web.MergedOptions.UpdateMergedOptionsFromMicrosoftIdentityApplicationOptions(Microsoft.Identity.Abstractions.MicrosoftIdentityApplicationOptions! microsoftIdentityApplicationOptions, Microsoft.Identity.Web.MergedOptions! mergedOptions) -> void static Microsoft.Identity.Web.MergedOptions.UpdateMergedOptionsFromMicrosoftIdentityOptions(Microsoft.Identity.Web.MicrosoftIdentityOptions! microsoftIdentityOptions, Microsoft.Identity.Web.MergedOptions! mergedOptions) -> void +static Microsoft.Identity.Web.MergedOptionsLogging.AuthorityIgnored(Microsoft.Extensions.Logging.ILogger! logger, string! authority, string! instance, string! tenantId) -> void static Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder.SetIdentityModelLogger(System.IServiceProvider! serviceProvider) -> void static Microsoft.Identity.Web.MsAuth10AtPop.WithAtPop(this Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! builder, string! popPublicKey, string! jwkClaim) -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! static Microsoft.Identity.Web.MsAuth10AtPop.WithAtPop(this Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! builder, System.Security.Cryptography.X509Certificates.X509Certificate2! clientCertificate, string! popPublicKey, string! jwkClaim, string! clientId, bool sendX5C) -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! @@ -311,12 +331,16 @@ static Microsoft.Identity.Web.TokenAcquisition.GetCacheKeyForManagedId(Microsoft static Microsoft.Identity.Web.TokenAcquisition.Logger.TokenAcquisitionError(Microsoft.Extensions.Logging.ILogger! logger, string! msalErrorMessage, System.Exception? ex) -> void static Microsoft.Identity.Web.TokenAcquisition.Logger.TokenAcquisitionMsalAuthenticationResultTime(Microsoft.Extensions.Logging.ILogger! logger, long durationTotalInMs, long durationInHttpInMs, long durationInCacheInMs, string! tokenSource, string! correlationId, string! cacheRefreshReason, System.Exception? ex) -> void static Microsoft.Identity.Web.TokenAcquisition.MergeExtraQueryParameters(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Web.TokenAcquisitionOptions! tokenAcquisitionOptions) -> System.Collections.Generic.Dictionary? +static Microsoft.Identity.Web.TokenAcquisition.MergeExtraQueryParameters(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Web.TokenAcquisitionOptions? tokenAcquisitionOptions) -> System.Collections.Generic.Dictionary? static Microsoft.Identity.Web.TokenAcquisition.ResolveTenant(string? tenant, Microsoft.Identity.Web.MergedOptions! mergedOptions) -> string? static Microsoft.Identity.Web.Util.Base64UrlHelpers.Decode(string! arg) -> string? static Microsoft.Identity.Web.Util.Base64UrlHelpers.DecodeBytes(string? str) -> byte[]? static Microsoft.Identity.Web.Util.Base64UrlHelpers.Encode(byte[]? inArray) -> string? static Microsoft.Identity.Web.Util.Base64UrlHelpers.Encode(string? arg) -> string? static Microsoft.Identity.Web.Util.Base64UrlHelpers.EncodeString(string? str) -> string? +static readonly Microsoft.Identity.Web.Constants.s_certificateRelatedErrorCodes -> System.Collections.Generic.HashSet! +static readonly Microsoft.Identity.Web.Constants.s_nonRetryableConfigErrorCodes -> System.Collections.Generic.HashSet! +static readonly Microsoft.Identity.Web.LoggingEventId.AuthorityIgnored -> Microsoft.Extensions.Logging.EventId static readonly Microsoft.Identity.Web.LoggingEventId.CredentialLoadAttempt -> Microsoft.Extensions.Logging.EventId static readonly Microsoft.Identity.Web.LoggingEventId.CredentialLoadAttemptFailed -> Microsoft.Extensions.Logging.EventId static readonly Microsoft.Identity.Web.LoggingEventId.NotUsingManagedIdentity -> Microsoft.Extensions.Logging.EventId diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/InternalAPI.Unshipped.txt index 62d5c481d..7dc5c5811 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/InternalAPI.Unshipped.txt @@ -1,2 +1 @@ #nullable enable -static Microsoft.Identity.Web.TokenAcquisition.MergeExtraQueryParameters(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Web.TokenAcquisitionOptions? tokenAcquisitionOptions) -> System.Collections.Generic.Dictionary? diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/PublicAPI.Shipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/PublicAPI.Shipped.txt index 8b941c47f..401ec8796 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/PublicAPI.Shipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/PublicAPI.Shipped.txt @@ -31,7 +31,11 @@ const Microsoft.Identity.Web.Constants.ResetPasswordPath = "/MicrosoftIdentity/A const Microsoft.Identity.Web.Constants.ReturnUrl = "ReturnUrl" -> string! const Microsoft.Identity.Web.Constants.Scope = "scope" -> string! const Microsoft.Identity.Web.Constants.SpaAuthCode = "SpaAuthCode" -> string! +Microsoft.Identity.Web.BeforeOnBehalfOfInitialized +Microsoft.Identity.Web.BeforeOnBehalfOfInitializedAsync Microsoft.Identity.Web.BeforeTokenAcquisitionForApp +Microsoft.Identity.Web.BeforeTokenAcquisitionForOnBehalfOf +Microsoft.Identity.Web.BeforeTokenAcquisitionForOnBehalfOfAsync Microsoft.Identity.Web.BeforeTokenAcquisitionForTestUser Microsoft.Identity.Web.BeforeTokenAcquisitionForTestUserAsync Microsoft.Identity.Web.ClaimConstants @@ -54,6 +58,7 @@ Microsoft.Identity.Web.Experimental.ICertificatesObserver Microsoft.Identity.Web.Experimental.ICertificatesObserver.OnClientCertificateChanged(Microsoft.Identity.Web.Experimental.CertificateChangeEventArg! e) -> void Microsoft.Identity.Web.Extensibility.BaseAuthorizationHeaderProvider Microsoft.Identity.Web.Extensibility.BaseAuthorizationHeaderProvider.BaseAuthorizationHeaderProvider(System.IServiceProvider! serviceProvider) -> void +Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions Microsoft.Identity.Web.IAuthenticationSchemeInformationProvider Microsoft.Identity.Web.Internal.WebApiBuilders Microsoft.Identity.Web.ITokenAcquisition @@ -65,11 +70,21 @@ Microsoft.Identity.Web.ITokenAcquisition.GetEffectiveAuthenticationScheme(string Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder.AddDistributedTokenCaches() -> Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder! Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder.AddInMemoryTokenCaches(System.Action? configureOptions = null, System.Action? memoryCacheOptions = null) -> Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder! +Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException +Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException.MicrosoftIdentityAuthenticationException(string! message, System.Exception! innerException) -> void +Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException.MicrosoftIdentityAuthenticationException(string! message) -> void Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder.ConfigurationSection.get -> Microsoft.Extensions.Configuration.IConfigurationSection? Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder.ConfigurationSection.set -> void Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder.MicrosoftIdentityBaseAuthenticationBuilder(Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfigurationSection? configurationSection = null) -> void Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder.Services.get -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions +Microsoft.Identity.Web.MicrosoftIdentityMessageHandler +Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.MicrosoftIdentityMessageHandler(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! headerProvider, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? defaultOptions = null, Microsoft.Extensions.Logging.ILogger? logger = null) -> void +Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions +Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.MicrosoftIdentityMessageHandlerOptions() -> void +Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.get -> System.Collections.Generic.IList! +Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.set -> void Microsoft.Identity.Web.MicrosoftIdentityOptions Microsoft.Identity.Web.MicrosoftIdentityOptions.AllowWebApiToBeAuthorizedByACL.get -> bool Microsoft.Identity.Web.MicrosoftIdentityOptions.AllowWebApiToBeAuthorizedByACL.set -> void @@ -119,6 +134,17 @@ Microsoft.Identity.Web.MicrosoftIdentityWebChallengeUserException.Scopes.get -> Microsoft.Identity.Web.MicrosoftIdentityWebChallengeUserException.Scopes.set -> void Microsoft.Identity.Web.MicrosoftIdentityWebChallengeUserException.Userflow.get -> string? Microsoft.Identity.Web.MicrosoftIdentityWebChallengeUserException.Userflow.set -> void +Microsoft.Identity.Web.MsalMtlsHttpClientFactory +Microsoft.Identity.Web.MsalMtlsHttpClientFactory.Dispose() -> void +Microsoft.Identity.Web.MsalMtlsHttpClientFactory.GetHttpClient() -> System.Net.Http.HttpClient! +Microsoft.Identity.Web.MsalMtlsHttpClientFactory.GetHttpClient(System.Security.Cryptography.X509Certificates.X509Certificate2! x509Certificate2) -> System.Net.Http.HttpClient! +Microsoft.Identity.Web.MsalMtlsHttpClientFactory.MsalMtlsHttpClientFactory(System.Net.Http.IHttpClientFactory! httpClientFactory) -> void +Microsoft.Identity.Web.OnBehalfOfEventArgs +Microsoft.Identity.Web.OnBehalfOfEventArgs.OnBehalfOfEventArgs() -> void +Microsoft.Identity.Web.OnBehalfOfEventArgs.User.get -> System.Security.Claims.ClaimsPrincipal? +Microsoft.Identity.Web.OnBehalfOfEventArgs.User.set -> void +Microsoft.Identity.Web.OnBehalfOfEventArgs.UserAssertionToken.get -> string? +Microsoft.Identity.Web.OnBehalfOfEventArgs.UserAssertionToken.set -> void Microsoft.Identity.Web.OpenIdConnectOptions Microsoft.Identity.Web.OpenIdConnectOptions.Authority.get -> string? Microsoft.Identity.Web.OpenIdConnectOptions.Authority.set -> void @@ -143,7 +169,11 @@ Microsoft.Identity.Web.TokenAcquirerFactory.ServiceProvider.set -> void Microsoft.Identity.Web.TokenAcquirerFactory.Services.get -> Microsoft.Extensions.DependencyInjection.ServiceCollection! Microsoft.Identity.Web.TokenAcquirerFactory.TokenAcquirerFactory() -> void Microsoft.Identity.Web.TokenAcquisitionExtensionOptions +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeOnBehalfOfInitialized -> Microsoft.Identity.Web.BeforeOnBehalfOfInitialized? +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeOnBehalfOfInitializedAsync -> Microsoft.Identity.Web.BeforeOnBehalfOfInitializedAsync? Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeTokenAcquisitionForApp -> Microsoft.Identity.Web.BeforeTokenAcquisitionForApp? +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeTokenAcquisitionForOnBehalfOf -> Microsoft.Identity.Web.BeforeTokenAcquisitionForOnBehalfOf? +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeTokenAcquisitionForOnBehalfOfAsync -> Microsoft.Identity.Web.BeforeTokenAcquisitionForOnBehalfOfAsync? Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeTokenAcquisitionForTestUser -> Microsoft.Identity.Web.BeforeTokenAcquisitionForTestUser? Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeTokenAcquisitionForTestUserAsync -> Microsoft.Identity.Web.BeforeTokenAcquisitionForTestUserAsync? Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.TokenAcquisitionExtensionOptions() -> void @@ -154,7 +184,15 @@ Microsoft.Identity.Web.TokenAcquisitionOptions.Clone() -> Microsoft.Identity.Web Microsoft.Identity.Web.TokenAcquisitionOptions.PoPConfiguration.get -> Microsoft.Identity.Client.AppConfig.PoPAuthenticationConfiguration? Microsoft.Identity.Web.TokenAcquisitionOptions.PoPConfiguration.set -> void Microsoft.Identity.Web.TokenAcquisitionOptions.TokenAcquisitionOptions() -> void +override Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.SendAsync(System.Net.Http.HttpRequestMessage! request, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.GetAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request) -> Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? +static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! options) -> System.Net.Http.HttpRequestMessage! +static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, System.Action! configure) -> System.Net.Http.HttpRequestMessage! static Microsoft.Identity.Web.Internal.WebApiBuilders.EnableTokenAcquisition(System.Action! configureConfidentialClientApplicationOptions, string! authenticationScheme, Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfigurationSection? configuration) -> Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder, Microsoft.Extensions.Configuration.IConfiguration! configuration, string! sectionName) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! options) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! static Microsoft.Identity.Web.PrincipalExtensionsForSecurityTokens.GetBootstrapToken(this System.Security.Principal.IPrincipal! claimsPrincipal) -> Microsoft.IdentityModel.Tokens.SecurityToken? static Microsoft.Identity.Web.ServiceCollectionExtensions.AddTokenAcquisition(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, bool isTokenAcquisitionSingleton = false) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.Identity.Web.TestOnly.TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest() -> void @@ -162,7 +200,11 @@ static Microsoft.Identity.Web.TokenAcquirerExtensions.GetFicTokenAsync(this Micr static Microsoft.Identity.Web.TokenAcquirerExtensions.WithClientAssertion(this Microsoft.Identity.Abstractions.AcquireTokenOptions! options, string! clientAssertion) -> Microsoft.Identity.Abstractions.AcquireTokenOptions! static Microsoft.Identity.Web.TokenAcquirerFactory.GetDefaultInstance(string! configSection = "AzureAd") -> Microsoft.Identity.Web.TokenAcquirerFactory! static Microsoft.Identity.Web.TokenAcquirerFactory.GetDefaultInstance(string! configSection = "AzureAd") -> T! +virtual Microsoft.Identity.Web.BeforeOnBehalfOfInitialized.Invoke(Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> void +virtual Microsoft.Identity.Web.BeforeOnBehalfOfInitializedAsync.Invoke(Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> System.Threading.Tasks.Task! virtual Microsoft.Identity.Web.BeforeTokenAcquisitionForApp.Invoke(Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions) -> void +virtual Microsoft.Identity.Web.BeforeTokenAcquisitionForOnBehalfOf.Invoke(Microsoft.Identity.Client.AcquireTokenOnBehalfOfParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> void +virtual Microsoft.Identity.Web.BeforeTokenAcquisitionForOnBehalfOfAsync.Invoke(Microsoft.Identity.Client.AcquireTokenOnBehalfOfParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> System.Threading.Tasks.Task! virtual Microsoft.Identity.Web.BeforeTokenAcquisitionForTestUser.Invoke(Microsoft.Identity.Client.AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> void virtual Microsoft.Identity.Web.BeforeTokenAcquisitionForTestUserAsync.Invoke(Microsoft.Identity.Client.AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> System.Threading.Tasks.Task! virtual Microsoft.Identity.Web.Extensibility.BaseAuthorizationHeaderProvider.CreateAuthorizationHeaderAsync(System.Collections.Generic.IEnumerable! scopes, Microsoft.Identity.Abstractions.AuthorizationHeaderProviderOptions? authorizationHeaderProviderOptions = null, System.Security.Claims.ClaimsPrincipal? claimsPrincipal = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/PublicAPI.Unshipped.txt index 13496576f..7dc5c5811 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net462/PublicAPI.Unshipped.txt @@ -1,15 +1 @@ #nullable enable -Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions -Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException -Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException.MicrosoftIdentityAuthenticationException(string! message) -> void -Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException.MicrosoftIdentityAuthenticationException(string! message, System.Exception! innerException) -> void -Microsoft.Identity.Web.MicrosoftIdentityMessageHandler -Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.MicrosoftIdentityMessageHandler(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! headerProvider, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? defaultOptions = null, Microsoft.Extensions.Logging.ILogger? logger = null) -> void -Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions -Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.MicrosoftIdentityMessageHandlerOptions() -> void -Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.get -> System.Collections.Generic.IList! -Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.set -> void -override Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.SendAsync(System.Net.Http.HttpRequestMessage! request, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.GetAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request) -> Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? -static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! options) -> System.Net.Http.HttpRequestMessage! -static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, System.Action! configure) -> System.Net.Http.HttpRequestMessage! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/InternalAPI.Shipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/InternalAPI.Shipped.txt index 3f80e4a15..59e23a3ce 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/InternalAPI.Shipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/InternalAPI.Shipped.txt @@ -3,13 +3,16 @@ const Microsoft.Identity.Web.Constants.AgentIdentityKey = "IDWEB_AGENT_IDENTITY" const Microsoft.Identity.Web.Constants.Aliases = "aliases" -> string! const Microsoft.Identity.Web.Constants.ApiVersion = "api-version" -> string! const Microsoft.Identity.Web.Constants.ApplicationJson = "application/json" -> string! +const Microsoft.Identity.Web.Constants.ApplicationNotFound = "AADSTS700016" -> string! const Microsoft.Identity.Web.Constants.Authorization = "Authorization" -> string! const Microsoft.Identity.Web.Constants.AzureADIssuerMetadataUrl = "https://login.microsoftonline.com/common/discovery/instance?authorization_endpoint=https://login.microsoftonline.com/common/oauth2/v2.0/authorize&api-version=1.1" -> string! const Microsoft.Identity.Web.Constants.BlazorChallengeUri = "MicrosoftIdentity/Account/Challenge?redirectUri=" -> string! const Microsoft.Identity.Web.Constants.CertificateHasBeenRevoked = "AADSTS7000214" -> string! const Microsoft.Identity.Web.Constants.CertificateIsOutsideValidityWindow = "AADSTS1000502" -> string! +const Microsoft.Identity.Web.Constants.CertificateWasRevoked = "AADSTS7000277" -> string! const Microsoft.Identity.Web.Constants.CiamAuthoritySuffix = ".ciamlogin.com" -> string! const Microsoft.Identity.Web.Constants.ClientAssertion = "IDWEB_CLIENT_ASSERTION" -> string! +const Microsoft.Identity.Web.Constants.ClientAssertionContainsInvalidSignature = "AADSTS7000274" -> string! const Microsoft.Identity.Web.Constants.ClientInfo = "client_info" -> string! const Microsoft.Identity.Web.Constants.Common = "common" -> string! const Microsoft.Identity.Web.Constants.Consent = "consent" -> string! @@ -23,6 +26,7 @@ const Microsoft.Identity.Web.Constants.FmiPathForClientAssertion = "IDWEB_FMI_PA const Microsoft.Identity.Web.Constants.GraphBaseUrlV1 = "https://graph.microsoft.com/v1.0" -> string! const Microsoft.Identity.Web.Constants.IDWebSku = "IDWeb." -> string! const Microsoft.Identity.Web.Constants.InvalidClient = "invalid_client" -> string! +const Microsoft.Identity.Web.Constants.InvalidClientSecret = "AADSTS7000215" -> string! const Microsoft.Identity.Web.Constants.InvalidKeyError = "AADSTS700027" -> string! const Microsoft.Identity.Web.Constants.ISessionStore = "ISessionStore" -> string! const Microsoft.Identity.Web.Constants.JwtSecurityTokenUsedToCallWebApi = "JwtSecurityTokenUsedToCallWebAPI" -> string! @@ -89,9 +93,11 @@ const Microsoft.Identity.Web.IDWebErrorMessage.InvalidSubAssertion = "IDW10505: const Microsoft.Identity.Web.IDWebErrorMessage.IssuerDoesNotMatchValidIssuers = "IDW10303: Issuer: '{0}', does not match any of the valid issuers provided for this application. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.IssuerMetadataUrlIsRequired = "IDW10301: Azure AD Issuer metadata address URL is required. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.MicrosoftIdentityWebChallengeUserException = "IDW10502: An MsalUiRequiredException was thrown due to a challenge for the user. See https://aka.ms/ms-id-web/ca_incremental-consent. " -> string! +const Microsoft.Identity.Web.IDWebErrorMessage.MissingIdentityConfiguration = "IDW10708: The identity configuration is incomplete. Provide either 'Instance' and 'TenantId', or 'Authority', or enable 'ManagedIdentity' in the configuration. Check your configuration keys for typos (e.g., trailing spaces). See https://aka.ms/ms-id-web/configuration. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.MissingRequiredScopesForAuthorizationFilter = "IDW10108: RequiredScope Attribute does not contain a value. The scopes need to be set on the controller, the page or action. See https://aka.ms/ms-id-web/required-scope-attribute. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.MissingRoles = "IDW10202: The 'roles' or 'role' claim does not contain roles '{0}' or was not found. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.MissingScopes = "IDW10203: The 'scope' or 'scp' claim does not contain scopes '{0}' or was not found. " -> string! +const Microsoft.Identity.Web.IDWebErrorMessage.MissingTokenBindingCertificate = "IDW10115: A signing certificate, which is required for token binding, is missing in loaded credentials." -> string! const Microsoft.Identity.Web.IDWebErrorMessage.NeitherScopeOrRolesClaimFoundInToken = "IDW10201: Neither scope nor roles claim was found in the bearer token. Authentication scheme used: '{0}'. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.NoMetadataDocumentRetrieverProvided = "IDW10302: No metadata document retriever is provided. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.NoScopesProvided = "IDW10103: No scopes provided in scopes... " -> string! @@ -102,6 +108,7 @@ const Microsoft.Identity.Web.IDWebErrorMessage.ScopeKeySectionIsProvidedButNotPr const Microsoft.Identity.Web.IDWebErrorMessage.ScopesNotConfiguredInConfigurationOrViaDelegate = "IDW10107: Scopes need to be passed-in either by configuration or by the delegate overriding it. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.ScopesRequiredToCallMicrosoftGraph = "IDW10208: You need to either pass-in scopes to AddMicrosoftGraph, in the appsettings.json file, or with .WithScopes() on the Graph queries. See https://aka.ms/ms-id-web/microsoftGraph. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.TenantIdClaimNotPresentInToken = "IDW10401: Neither `tid` nor `tenantId` claim is present in the token obtained from Microsoft identity platform. " -> string! +const Microsoft.Identity.Web.IDWebErrorMessage.TokenBindingRequiresEnabledAppTokenAcquisition = "IDW10116: Token binding requires enabled app token acquisition." -> string! const Microsoft.Identity.Web.IDWebErrorMessage.TokenIsNotJwtToken = "IDW10403: Token is not a JWT token. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.UnauthenticatedUser = "IDW10204: The user is unauthenticated. The HttpContext does not contain any claims. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.WithClientCredentialsIsObsolete = "Use WithClientCredentialsAsync instead." -> string! @@ -157,6 +164,7 @@ Microsoft.Identity.Web.DefaultAuthorizationHeaderProvider Microsoft.Identity.Web.DefaultAuthorizationHeaderProvider.CreateAuthorizationHeaderAsync(System.Collections.Generic.IEnumerable! scopes, Microsoft.Identity.Abstractions.AuthorizationHeaderProviderOptions? downstreamApiOptions = null, System.Security.Claims.ClaimsPrincipal? claimsPrincipal = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DefaultAuthorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync(string! scopes, Microsoft.Identity.Abstractions.AuthorizationHeaderProviderOptions? downstreamApiOptions = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DefaultAuthorizationHeaderProvider.CreateAuthorizationHeaderForUserAsync(System.Collections.Generic.IEnumerable! scopes, Microsoft.Identity.Abstractions.AuthorizationHeaderProviderOptions? downstreamApiOptions = null, System.Security.Claims.ClaimsPrincipal? claimsPrincipal = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.DefaultAuthorizationHeaderProvider.CreateBoundAuthorizationHeaderAsync(Microsoft.Identity.Abstractions.DownstreamApiOptions! downstreamApiOptions, System.Security.Claims.ClaimsPrincipal? claimsPrincipal = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task>! Microsoft.Identity.Web.DefaultAuthorizationHeaderProvider.DefaultAuthorizationHeaderProvider(Microsoft.Identity.Web.ITokenAcquisition! tokenAcquisition) -> void Microsoft.Identity.Web.DefaultTokenAcquirerFactoryImplementation Microsoft.Identity.Web.DefaultTokenAcquirerFactoryImplementation.DefaultTokenAcquirerFactoryImplementation(System.IServiceProvider! serviceProvider) -> void @@ -220,6 +228,10 @@ Microsoft.Identity.Web.MergedOptions.EnablePiiLogging.get -> bool Microsoft.Identity.Web.MergedOptions.EnablePiiLogging.set -> void Microsoft.Identity.Web.MergedOptions.IsDefaultPlatformLoggingEnabled.get -> bool Microsoft.Identity.Web.MergedOptions.IsDefaultPlatformLoggingEnabled.set -> void +Microsoft.Identity.Web.MergedOptions.IsTokenBinding.get -> bool +Microsoft.Identity.Web.MergedOptions.IsTokenBinding.set -> void +Microsoft.Identity.Web.MergedOptions.Logger.get -> Microsoft.Extensions.Logging.ILogger? +Microsoft.Identity.Web.MergedOptions.Logger.set -> void Microsoft.Identity.Web.MergedOptions.LogLevel.get -> Microsoft.Identity.Client.LogLevel Microsoft.Identity.Web.MergedOptions.LogLevel.set -> void Microsoft.Identity.Web.MergedOptions.MergedOptions() -> void @@ -232,9 +244,11 @@ Microsoft.Identity.Web.MergedOptions.PreserveAuthority.get -> bool Microsoft.Identity.Web.MergedOptions.PreserveAuthority.set -> void Microsoft.Identity.Web.MergedOptions.RedirectUri.get -> string? Microsoft.Identity.Web.MergedOptions.RedirectUri.set -> void +Microsoft.Identity.Web.MergedOptionsLogging Microsoft.Identity.Web.MergedOptionsStore Microsoft.Identity.Web.MergedOptionsStore.Get(string! name) -> Microsoft.Identity.Web.MergedOptions! Microsoft.Identity.Web.MergedOptionsStore.MergedOptionsStore() -> void +Microsoft.Identity.Web.MergedOptionsStore.MergedOptionsStore(System.IServiceProvider! serviceProvider) -> void Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder(Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfigurationSection? configurationSection = null) -> void Microsoft.Identity.Web.MicrosoftIdentityApplicationOptionsMerger Microsoft.Identity.Web.MicrosoftIdentityApplicationOptionsMerger.MicrosoftIdentityApplicationOptionsMerger(Microsoft.Identity.Web.IMergedOptionsStore! mergedOptions) -> void @@ -261,12 +275,15 @@ Microsoft.Identity.Web.TokenAcquisition.GetAccessTokenForUserAsync(System.Collec Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForAppAsync(string! scope, string? authenticationScheme = null, string? tenant = null, Microsoft.Identity.Web.TokenAcquisitionOptions? tokenAcquisitionOptions = null) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForUserAsync(System.Collections.Generic.IEnumerable! scopes, string? authenticationScheme = null, string? tenantId = null, string? userFlow = null, System.Security.Claims.ClaimsPrincipal? user = null, Microsoft.Identity.Web.TokenAcquisitionOptions? tokenAcquisitionOptions = null) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.TokenAcquisition.GetEffectiveAuthenticationScheme(string? authenticationScheme) -> string! -Microsoft.Identity.Web.TokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(Microsoft.Identity.Web.MergedOptions! mergedOptions) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.TokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(Microsoft.Identity.Web.MergedOptions! mergedOptions, bool isTokenBinding) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.TokenAcquisition.GetOrBuildManagedIdentityApplicationAsync(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Abstractions.ManagedIdentityOptions! managedIdentityOptions) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.TokenAcquisition.Logger Microsoft.Identity.Web.TokenAcquisition.RemoveAccountAsync(System.Security.Claims.ClaimsPrincipal! user, string? authenticationScheme = null) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.TokenAcquisition.TokenAcquisition(Microsoft.Identity.Web.TokenCacheProviders.IMsalTokenCacheProvider! tokenCacheProvider, Microsoft.Identity.Web.ITokenAcquisitionHost! tokenAcquisitionHost, System.Net.Http.IHttpClientFactory! httpClientFactory, Microsoft.Extensions.Logging.ILogger! logger, System.IServiceProvider! serviceProvider, Microsoft.Identity.Abstractions.ICredentialsLoader! credentialsLoader) -> void +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeOnBehalfOfInitializedAsync(Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForApp(Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions) -> void +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForOnBehalfOf(Microsoft.Identity.Client.AcquireTokenOnBehalfOfParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> void +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForOnBehalfOfAsync(Microsoft.Identity.Client.AcquireTokenOnBehalfOfParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForTestUser(Microsoft.Identity.Client.AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> void Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForTestUserAsync(Microsoft.Identity.Client.AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.Util.Base64UrlHelpers @@ -288,19 +305,22 @@ static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logg static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.FailedToLoadCredentials(Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Abstractions.CredentialDescription! certificateDescription) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.NotUsingManagedIdentity(Microsoft.Extensions.Logging.ILogger! logger, string! message) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingCertThumbprint(Microsoft.Extensions.Logging.ILogger! logger, string! certThumbprint) -> void +static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingCertThumbprint(Microsoft.Extensions.Logging.ILogger! logger, string? certThumbprint) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingManagedIdentity(Microsoft.Extensions.Logging.ILogger! logger) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingPodIdentityFile(Microsoft.Extensions.Logging.ILogger! logger, string! signedAssertionFileDiskPath) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingSignedAssertionFromCustomProvider(Microsoft.Extensions.Logging.ILogger! logger, string! signedAssertionUri) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingSignedAssertionFromVault(Microsoft.Extensions.Logging.ILogger! logger, string! signedAssertionUri) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.WithClientCredentials(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder! builder, System.Collections.Generic.IEnumerable! clientCredentials, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Abstractions.ICredentialsLoader! credentialsLoader, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters! credentialSourceLoaderParameters) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder! -static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.WithClientCredentialsAsync(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder! builder, System.Collections.Generic.IEnumerable! clientCredentials, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Abstractions.ICredentialsLoader! credentialsLoader, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters) -> System.Threading.Tasks.Task! +static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.WithClientCredentialsAsync(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder! builder, System.Collections.Generic.IEnumerable! clientCredentials, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Abstractions.ICredentialsLoader! credentialsLoader, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, bool isTokenBinding) -> System.Threading.Tasks.Task! static Microsoft.Identity.Web.DefaultTokenAcquirerFactoryImplementation.GetKey(string? authority, string? clientId, string? region) -> string! static Microsoft.Identity.Web.IdHelper.CreateTelemetryInfo() -> string! +static Microsoft.Identity.Web.MergedOptions.ParseAuthorityIfNecessary(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Extensions.Logging.ILogger? logger = null) -> void static Microsoft.Identity.Web.MergedOptions.ParseAuthorityIfNecessary(Microsoft.Identity.Web.MergedOptions! mergedOptions) -> void static Microsoft.Identity.Web.MergedOptions.UpdateConfidentialClientApplicationOptionsFromMergedOptions(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Client.ConfidentialClientApplicationOptions! confidentialClientApplicationOptions) -> void static Microsoft.Identity.Web.MergedOptions.UpdateMergedOptionsFromConfidentialClientApplicationOptions(Microsoft.Identity.Client.ConfidentialClientApplicationOptions! confidentialClientApplicationOptions, Microsoft.Identity.Web.MergedOptions! mergedOptions) -> void static Microsoft.Identity.Web.MergedOptions.UpdateMergedOptionsFromMicrosoftIdentityApplicationOptions(Microsoft.Identity.Abstractions.MicrosoftIdentityApplicationOptions! microsoftIdentityApplicationOptions, Microsoft.Identity.Web.MergedOptions! mergedOptions) -> void static Microsoft.Identity.Web.MergedOptions.UpdateMergedOptionsFromMicrosoftIdentityOptions(Microsoft.Identity.Web.MicrosoftIdentityOptions! microsoftIdentityOptions, Microsoft.Identity.Web.MergedOptions! mergedOptions) -> void +static Microsoft.Identity.Web.MergedOptionsLogging.AuthorityIgnored(Microsoft.Extensions.Logging.ILogger! logger, string! authority, string! instance, string! tenantId) -> void static Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder.SetIdentityModelLogger(System.IServiceProvider! serviceProvider) -> void static Microsoft.Identity.Web.MsAuth10AtPop.WithAtPop(this Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! builder, string! popPublicKey, string! jwkClaim) -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! static Microsoft.Identity.Web.MsAuth10AtPop.WithAtPop(this Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! builder, System.Security.Cryptography.X509Certificates.X509Certificate2! clientCertificate, string! popPublicKey, string! jwkClaim, string! clientId, bool sendX5C) -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! @@ -311,12 +331,16 @@ static Microsoft.Identity.Web.TokenAcquisition.GetCacheKeyForManagedId(Microsoft static Microsoft.Identity.Web.TokenAcquisition.Logger.TokenAcquisitionError(Microsoft.Extensions.Logging.ILogger! logger, string! msalErrorMessage, System.Exception? ex) -> void static Microsoft.Identity.Web.TokenAcquisition.Logger.TokenAcquisitionMsalAuthenticationResultTime(Microsoft.Extensions.Logging.ILogger! logger, long durationTotalInMs, long durationInHttpInMs, long durationInCacheInMs, string! tokenSource, string! correlationId, string! cacheRefreshReason, System.Exception? ex) -> void static Microsoft.Identity.Web.TokenAcquisition.MergeExtraQueryParameters(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Web.TokenAcquisitionOptions! tokenAcquisitionOptions) -> System.Collections.Generic.Dictionary? +static Microsoft.Identity.Web.TokenAcquisition.MergeExtraQueryParameters(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Web.TokenAcquisitionOptions? tokenAcquisitionOptions) -> System.Collections.Generic.Dictionary? static Microsoft.Identity.Web.TokenAcquisition.ResolveTenant(string? tenant, Microsoft.Identity.Web.MergedOptions! mergedOptions) -> string? static Microsoft.Identity.Web.Util.Base64UrlHelpers.Decode(string! arg) -> string? static Microsoft.Identity.Web.Util.Base64UrlHelpers.DecodeBytes(string? str) -> byte[]? static Microsoft.Identity.Web.Util.Base64UrlHelpers.Encode(byte[]? inArray) -> string? static Microsoft.Identity.Web.Util.Base64UrlHelpers.Encode(string? arg) -> string? static Microsoft.Identity.Web.Util.Base64UrlHelpers.EncodeString(string? str) -> string? +static readonly Microsoft.Identity.Web.Constants.s_certificateRelatedErrorCodes -> System.Collections.Generic.HashSet! +static readonly Microsoft.Identity.Web.Constants.s_nonRetryableConfigErrorCodes -> System.Collections.Generic.HashSet! +static readonly Microsoft.Identity.Web.LoggingEventId.AuthorityIgnored -> Microsoft.Extensions.Logging.EventId static readonly Microsoft.Identity.Web.LoggingEventId.CredentialLoadAttempt -> Microsoft.Extensions.Logging.EventId static readonly Microsoft.Identity.Web.LoggingEventId.CredentialLoadAttemptFailed -> Microsoft.Extensions.Logging.EventId static readonly Microsoft.Identity.Web.LoggingEventId.NotUsingManagedIdentity -> Microsoft.Extensions.Logging.EventId diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/InternalAPI.Unshipped.txt index 62d5c481d..7dc5c5811 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/InternalAPI.Unshipped.txt @@ -1,2 +1 @@ #nullable enable -static Microsoft.Identity.Web.TokenAcquisition.MergeExtraQueryParameters(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Web.TokenAcquisitionOptions? tokenAcquisitionOptions) -> System.Collections.Generic.Dictionary? diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/PublicAPI.Shipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/PublicAPI.Shipped.txt index 8b941c47f..401ec8796 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/PublicAPI.Shipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/PublicAPI.Shipped.txt @@ -31,7 +31,11 @@ const Microsoft.Identity.Web.Constants.ResetPasswordPath = "/MicrosoftIdentity/A const Microsoft.Identity.Web.Constants.ReturnUrl = "ReturnUrl" -> string! const Microsoft.Identity.Web.Constants.Scope = "scope" -> string! const Microsoft.Identity.Web.Constants.SpaAuthCode = "SpaAuthCode" -> string! +Microsoft.Identity.Web.BeforeOnBehalfOfInitialized +Microsoft.Identity.Web.BeforeOnBehalfOfInitializedAsync Microsoft.Identity.Web.BeforeTokenAcquisitionForApp +Microsoft.Identity.Web.BeforeTokenAcquisitionForOnBehalfOf +Microsoft.Identity.Web.BeforeTokenAcquisitionForOnBehalfOfAsync Microsoft.Identity.Web.BeforeTokenAcquisitionForTestUser Microsoft.Identity.Web.BeforeTokenAcquisitionForTestUserAsync Microsoft.Identity.Web.ClaimConstants @@ -54,6 +58,7 @@ Microsoft.Identity.Web.Experimental.ICertificatesObserver Microsoft.Identity.Web.Experimental.ICertificatesObserver.OnClientCertificateChanged(Microsoft.Identity.Web.Experimental.CertificateChangeEventArg! e) -> void Microsoft.Identity.Web.Extensibility.BaseAuthorizationHeaderProvider Microsoft.Identity.Web.Extensibility.BaseAuthorizationHeaderProvider.BaseAuthorizationHeaderProvider(System.IServiceProvider! serviceProvider) -> void +Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions Microsoft.Identity.Web.IAuthenticationSchemeInformationProvider Microsoft.Identity.Web.Internal.WebApiBuilders Microsoft.Identity.Web.ITokenAcquisition @@ -65,11 +70,21 @@ Microsoft.Identity.Web.ITokenAcquisition.GetEffectiveAuthenticationScheme(string Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder.AddDistributedTokenCaches() -> Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder! Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder.AddInMemoryTokenCaches(System.Action? configureOptions = null, System.Action? memoryCacheOptions = null) -> Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder! +Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException +Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException.MicrosoftIdentityAuthenticationException(string! message, System.Exception! innerException) -> void +Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException.MicrosoftIdentityAuthenticationException(string! message) -> void Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder.ConfigurationSection.get -> Microsoft.Extensions.Configuration.IConfigurationSection? Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder.ConfigurationSection.set -> void Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder.MicrosoftIdentityBaseAuthenticationBuilder(Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfigurationSection? configurationSection = null) -> void Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder.Services.get -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions +Microsoft.Identity.Web.MicrosoftIdentityMessageHandler +Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.MicrosoftIdentityMessageHandler(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! headerProvider, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? defaultOptions = null, Microsoft.Extensions.Logging.ILogger? logger = null) -> void +Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions +Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.MicrosoftIdentityMessageHandlerOptions() -> void +Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.get -> System.Collections.Generic.IList! +Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.set -> void Microsoft.Identity.Web.MicrosoftIdentityOptions Microsoft.Identity.Web.MicrosoftIdentityOptions.AllowWebApiToBeAuthorizedByACL.get -> bool Microsoft.Identity.Web.MicrosoftIdentityOptions.AllowWebApiToBeAuthorizedByACL.set -> void @@ -119,6 +134,17 @@ Microsoft.Identity.Web.MicrosoftIdentityWebChallengeUserException.Scopes.get -> Microsoft.Identity.Web.MicrosoftIdentityWebChallengeUserException.Scopes.set -> void Microsoft.Identity.Web.MicrosoftIdentityWebChallengeUserException.Userflow.get -> string? Microsoft.Identity.Web.MicrosoftIdentityWebChallengeUserException.Userflow.set -> void +Microsoft.Identity.Web.MsalMtlsHttpClientFactory +Microsoft.Identity.Web.MsalMtlsHttpClientFactory.Dispose() -> void +Microsoft.Identity.Web.MsalMtlsHttpClientFactory.GetHttpClient() -> System.Net.Http.HttpClient! +Microsoft.Identity.Web.MsalMtlsHttpClientFactory.GetHttpClient(System.Security.Cryptography.X509Certificates.X509Certificate2! x509Certificate2) -> System.Net.Http.HttpClient! +Microsoft.Identity.Web.MsalMtlsHttpClientFactory.MsalMtlsHttpClientFactory(System.Net.Http.IHttpClientFactory! httpClientFactory) -> void +Microsoft.Identity.Web.OnBehalfOfEventArgs +Microsoft.Identity.Web.OnBehalfOfEventArgs.OnBehalfOfEventArgs() -> void +Microsoft.Identity.Web.OnBehalfOfEventArgs.User.get -> System.Security.Claims.ClaimsPrincipal? +Microsoft.Identity.Web.OnBehalfOfEventArgs.User.set -> void +Microsoft.Identity.Web.OnBehalfOfEventArgs.UserAssertionToken.get -> string? +Microsoft.Identity.Web.OnBehalfOfEventArgs.UserAssertionToken.set -> void Microsoft.Identity.Web.OpenIdConnectOptions Microsoft.Identity.Web.OpenIdConnectOptions.Authority.get -> string? Microsoft.Identity.Web.OpenIdConnectOptions.Authority.set -> void @@ -143,7 +169,11 @@ Microsoft.Identity.Web.TokenAcquirerFactory.ServiceProvider.set -> void Microsoft.Identity.Web.TokenAcquirerFactory.Services.get -> Microsoft.Extensions.DependencyInjection.ServiceCollection! Microsoft.Identity.Web.TokenAcquirerFactory.TokenAcquirerFactory() -> void Microsoft.Identity.Web.TokenAcquisitionExtensionOptions +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeOnBehalfOfInitialized -> Microsoft.Identity.Web.BeforeOnBehalfOfInitialized? +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeOnBehalfOfInitializedAsync -> Microsoft.Identity.Web.BeforeOnBehalfOfInitializedAsync? Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeTokenAcquisitionForApp -> Microsoft.Identity.Web.BeforeTokenAcquisitionForApp? +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeTokenAcquisitionForOnBehalfOf -> Microsoft.Identity.Web.BeforeTokenAcquisitionForOnBehalfOf? +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeTokenAcquisitionForOnBehalfOfAsync -> Microsoft.Identity.Web.BeforeTokenAcquisitionForOnBehalfOfAsync? Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeTokenAcquisitionForTestUser -> Microsoft.Identity.Web.BeforeTokenAcquisitionForTestUser? Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeTokenAcquisitionForTestUserAsync -> Microsoft.Identity.Web.BeforeTokenAcquisitionForTestUserAsync? Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.TokenAcquisitionExtensionOptions() -> void @@ -154,7 +184,15 @@ Microsoft.Identity.Web.TokenAcquisitionOptions.Clone() -> Microsoft.Identity.Web Microsoft.Identity.Web.TokenAcquisitionOptions.PoPConfiguration.get -> Microsoft.Identity.Client.AppConfig.PoPAuthenticationConfiguration? Microsoft.Identity.Web.TokenAcquisitionOptions.PoPConfiguration.set -> void Microsoft.Identity.Web.TokenAcquisitionOptions.TokenAcquisitionOptions() -> void +override Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.SendAsync(System.Net.Http.HttpRequestMessage! request, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.GetAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request) -> Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? +static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! options) -> System.Net.Http.HttpRequestMessage! +static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, System.Action! configure) -> System.Net.Http.HttpRequestMessage! static Microsoft.Identity.Web.Internal.WebApiBuilders.EnableTokenAcquisition(System.Action! configureConfidentialClientApplicationOptions, string! authenticationScheme, Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfigurationSection? configuration) -> Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder, Microsoft.Extensions.Configuration.IConfiguration! configuration, string! sectionName) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! options) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! static Microsoft.Identity.Web.PrincipalExtensionsForSecurityTokens.GetBootstrapToken(this System.Security.Principal.IPrincipal! claimsPrincipal) -> Microsoft.IdentityModel.Tokens.SecurityToken? static Microsoft.Identity.Web.ServiceCollectionExtensions.AddTokenAcquisition(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, bool isTokenAcquisitionSingleton = false) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.Identity.Web.TestOnly.TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest() -> void @@ -162,7 +200,11 @@ static Microsoft.Identity.Web.TokenAcquirerExtensions.GetFicTokenAsync(this Micr static Microsoft.Identity.Web.TokenAcquirerExtensions.WithClientAssertion(this Microsoft.Identity.Abstractions.AcquireTokenOptions! options, string! clientAssertion) -> Microsoft.Identity.Abstractions.AcquireTokenOptions! static Microsoft.Identity.Web.TokenAcquirerFactory.GetDefaultInstance(string! configSection = "AzureAd") -> Microsoft.Identity.Web.TokenAcquirerFactory! static Microsoft.Identity.Web.TokenAcquirerFactory.GetDefaultInstance(string! configSection = "AzureAd") -> T! +virtual Microsoft.Identity.Web.BeforeOnBehalfOfInitialized.Invoke(Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> void +virtual Microsoft.Identity.Web.BeforeOnBehalfOfInitializedAsync.Invoke(Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> System.Threading.Tasks.Task! virtual Microsoft.Identity.Web.BeforeTokenAcquisitionForApp.Invoke(Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions) -> void +virtual Microsoft.Identity.Web.BeforeTokenAcquisitionForOnBehalfOf.Invoke(Microsoft.Identity.Client.AcquireTokenOnBehalfOfParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> void +virtual Microsoft.Identity.Web.BeforeTokenAcquisitionForOnBehalfOfAsync.Invoke(Microsoft.Identity.Client.AcquireTokenOnBehalfOfParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> System.Threading.Tasks.Task! virtual Microsoft.Identity.Web.BeforeTokenAcquisitionForTestUser.Invoke(Microsoft.Identity.Client.AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> void virtual Microsoft.Identity.Web.BeforeTokenAcquisitionForTestUserAsync.Invoke(Microsoft.Identity.Client.AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> System.Threading.Tasks.Task! virtual Microsoft.Identity.Web.Extensibility.BaseAuthorizationHeaderProvider.CreateAuthorizationHeaderAsync(System.Collections.Generic.IEnumerable! scopes, Microsoft.Identity.Abstractions.AuthorizationHeaderProviderOptions? authorizationHeaderProviderOptions = null, System.Security.Claims.ClaimsPrincipal? claimsPrincipal = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/PublicAPI.Unshipped.txt index 13496576f..7dc5c5811 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net472/PublicAPI.Unshipped.txt @@ -1,15 +1 @@ #nullable enable -Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions -Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException -Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException.MicrosoftIdentityAuthenticationException(string! message) -> void -Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException.MicrosoftIdentityAuthenticationException(string! message, System.Exception! innerException) -> void -Microsoft.Identity.Web.MicrosoftIdentityMessageHandler -Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.MicrosoftIdentityMessageHandler(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! headerProvider, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? defaultOptions = null, Microsoft.Extensions.Logging.ILogger? logger = null) -> void -Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions -Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.MicrosoftIdentityMessageHandlerOptions() -> void -Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.get -> System.Collections.Generic.IList! -Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.set -> void -override Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.SendAsync(System.Net.Http.HttpRequestMessage! request, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.GetAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request) -> Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? -static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! options) -> System.Net.Http.HttpRequestMessage! -static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, System.Action! configure) -> System.Net.Http.HttpRequestMessage! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/InternalAPI.Shipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/InternalAPI.Shipped.txt index 17650922c..1edc5a028 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/InternalAPI.Shipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/InternalAPI.Shipped.txt @@ -3,13 +3,16 @@ const Microsoft.Identity.Web.Constants.AgentIdentityKey = "IDWEB_AGENT_IDENTITY" const Microsoft.Identity.Web.Constants.Aliases = "aliases" -> string! const Microsoft.Identity.Web.Constants.ApiVersion = "api-version" -> string! const Microsoft.Identity.Web.Constants.ApplicationJson = "application/json" -> string! +const Microsoft.Identity.Web.Constants.ApplicationNotFound = "AADSTS700016" -> string! const Microsoft.Identity.Web.Constants.Authorization = "Authorization" -> string! const Microsoft.Identity.Web.Constants.AzureADIssuerMetadataUrl = "https://login.microsoftonline.com/common/discovery/instance?authorization_endpoint=https://login.microsoftonline.com/common/oauth2/v2.0/authorize&api-version=1.1" -> string! const Microsoft.Identity.Web.Constants.BlazorChallengeUri = "MicrosoftIdentity/Account/Challenge?redirectUri=" -> string! const Microsoft.Identity.Web.Constants.CertificateHasBeenRevoked = "AADSTS7000214" -> string! const Microsoft.Identity.Web.Constants.CertificateIsOutsideValidityWindow = "AADSTS1000502" -> string! +const Microsoft.Identity.Web.Constants.CertificateWasRevoked = "AADSTS7000277" -> string! const Microsoft.Identity.Web.Constants.CiamAuthoritySuffix = ".ciamlogin.com" -> string! const Microsoft.Identity.Web.Constants.ClientAssertion = "IDWEB_CLIENT_ASSERTION" -> string! +const Microsoft.Identity.Web.Constants.ClientAssertionContainsInvalidSignature = "AADSTS7000274" -> string! const Microsoft.Identity.Web.Constants.ClientInfo = "client_info" -> string! const Microsoft.Identity.Web.Constants.Common = "common" -> string! const Microsoft.Identity.Web.Constants.Consent = "consent" -> string! @@ -23,6 +26,7 @@ const Microsoft.Identity.Web.Constants.FmiPathForClientAssertion = "IDWEB_FMI_PA const Microsoft.Identity.Web.Constants.GraphBaseUrlV1 = "https://graph.microsoft.com/v1.0" -> string! const Microsoft.Identity.Web.Constants.IDWebSku = "IDWeb." -> string! const Microsoft.Identity.Web.Constants.InvalidClient = "invalid_client" -> string! +const Microsoft.Identity.Web.Constants.InvalidClientSecret = "AADSTS7000215" -> string! const Microsoft.Identity.Web.Constants.InvalidKeyError = "AADSTS700027" -> string! const Microsoft.Identity.Web.Constants.ISessionStore = "ISessionStore" -> string! const Microsoft.Identity.Web.Constants.JwtSecurityTokenUsedToCallWebApi = "JwtSecurityTokenUsedToCallWebAPI" -> string! @@ -89,9 +93,11 @@ const Microsoft.Identity.Web.IDWebErrorMessage.InvalidSubAssertion = "IDW10505: const Microsoft.Identity.Web.IDWebErrorMessage.IssuerDoesNotMatchValidIssuers = "IDW10303: Issuer: '{0}', does not match any of the valid issuers provided for this application. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.IssuerMetadataUrlIsRequired = "IDW10301: Azure AD Issuer metadata address URL is required. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.MicrosoftIdentityWebChallengeUserException = "IDW10502: An MsalUiRequiredException was thrown due to a challenge for the user. See https://aka.ms/ms-id-web/ca_incremental-consent. " -> string! +const Microsoft.Identity.Web.IDWebErrorMessage.MissingIdentityConfiguration = "IDW10708: The identity configuration is incomplete. Provide either 'Instance' and 'TenantId', or 'Authority', or enable 'ManagedIdentity' in the configuration. Check your configuration keys for typos (e.g., trailing spaces). See https://aka.ms/ms-id-web/configuration. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.MissingRequiredScopesForAuthorizationFilter = "IDW10108: RequiredScope Attribute does not contain a value. The scopes need to be set on the controller, the page or action. See https://aka.ms/ms-id-web/required-scope-attribute. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.MissingRoles = "IDW10202: The 'roles' or 'role' claim does not contain roles '{0}' or was not found. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.MissingScopes = "IDW10203: The 'scope' or 'scp' claim does not contain scopes '{0}' or was not found. " -> string! +const Microsoft.Identity.Web.IDWebErrorMessage.MissingTokenBindingCertificate = "IDW10115: A signing certificate, which is required for token binding, is missing in loaded credentials." -> string! const Microsoft.Identity.Web.IDWebErrorMessage.NeitherScopeOrRolesClaimFoundInToken = "IDW10201: Neither scope nor roles claim was found in the bearer token. Authentication scheme used: '{0}'. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.NoMetadataDocumentRetrieverProvided = "IDW10302: No metadata document retriever is provided. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.NoScopesProvided = "IDW10103: No scopes provided in scopes... " -> string! @@ -102,6 +108,7 @@ const Microsoft.Identity.Web.IDWebErrorMessage.ScopeKeySectionIsProvidedButNotPr const Microsoft.Identity.Web.IDWebErrorMessage.ScopesNotConfiguredInConfigurationOrViaDelegate = "IDW10107: Scopes need to be passed-in either by configuration or by the delegate overriding it. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.ScopesRequiredToCallMicrosoftGraph = "IDW10208: You need to either pass-in scopes to AddMicrosoftGraph, in the appsettings.json file, or with .WithScopes() on the Graph queries. See https://aka.ms/ms-id-web/microsoftGraph. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.TenantIdClaimNotPresentInToken = "IDW10401: Neither `tid` nor `tenantId` claim is present in the token obtained from Microsoft identity platform. " -> string! +const Microsoft.Identity.Web.IDWebErrorMessage.TokenBindingRequiresEnabledAppTokenAcquisition = "IDW10116: Token binding requires enabled app token acquisition." -> string! const Microsoft.Identity.Web.IDWebErrorMessage.TokenIsNotJwtToken = "IDW10403: Token is not a JWT token. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.UnauthenticatedUser = "IDW10204: The user is unauthenticated. The HttpContext does not contain any claims. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.WithClientCredentialsIsObsolete = "Use WithClientCredentialsAsync instead." -> string! @@ -158,6 +165,7 @@ Microsoft.Identity.Web.DefaultAuthorizationHeaderProvider Microsoft.Identity.Web.DefaultAuthorizationHeaderProvider.CreateAuthorizationHeaderAsync(System.Collections.Generic.IEnumerable! scopes, Microsoft.Identity.Abstractions.AuthorizationHeaderProviderOptions? downstreamApiOptions = null, System.Security.Claims.ClaimsPrincipal? claimsPrincipal = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DefaultAuthorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync(string! scopes, Microsoft.Identity.Abstractions.AuthorizationHeaderProviderOptions? downstreamApiOptions = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DefaultAuthorizationHeaderProvider.CreateAuthorizationHeaderForUserAsync(System.Collections.Generic.IEnumerable! scopes, Microsoft.Identity.Abstractions.AuthorizationHeaderProviderOptions? downstreamApiOptions = null, System.Security.Claims.ClaimsPrincipal? claimsPrincipal = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.DefaultAuthorizationHeaderProvider.CreateBoundAuthorizationHeaderAsync(Microsoft.Identity.Abstractions.DownstreamApiOptions! downstreamApiOptions, System.Security.Claims.ClaimsPrincipal? claimsPrincipal = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task>! Microsoft.Identity.Web.DefaultAuthorizationHeaderProvider.DefaultAuthorizationHeaderProvider(Microsoft.Identity.Web.ITokenAcquisition! tokenAcquisition) -> void Microsoft.Identity.Web.DefaultTokenAcquirerFactoryImplementation Microsoft.Identity.Web.DefaultTokenAcquirerFactoryImplementation.DefaultTokenAcquirerFactoryImplementation(System.IServiceProvider! serviceProvider) -> void @@ -225,6 +233,10 @@ Microsoft.Identity.Web.MergedOptions.EnablePiiLogging.get -> bool Microsoft.Identity.Web.MergedOptions.EnablePiiLogging.set -> void Microsoft.Identity.Web.MergedOptions.IsDefaultPlatformLoggingEnabled.get -> bool Microsoft.Identity.Web.MergedOptions.IsDefaultPlatformLoggingEnabled.set -> void +Microsoft.Identity.Web.MergedOptions.IsTokenBinding.get -> bool +Microsoft.Identity.Web.MergedOptions.IsTokenBinding.set -> void +Microsoft.Identity.Web.MergedOptions.Logger.get -> Microsoft.Extensions.Logging.ILogger? +Microsoft.Identity.Web.MergedOptions.Logger.set -> void Microsoft.Identity.Web.MergedOptions.LogLevel.get -> Microsoft.Identity.Client.LogLevel Microsoft.Identity.Web.MergedOptions.LogLevel.set -> void Microsoft.Identity.Web.MergedOptions.MergedOptions() -> void @@ -237,9 +249,11 @@ Microsoft.Identity.Web.MergedOptions.PreserveAuthority.get -> bool Microsoft.Identity.Web.MergedOptions.PreserveAuthority.set -> void Microsoft.Identity.Web.MergedOptions.RedirectUri.get -> string? Microsoft.Identity.Web.MergedOptions.RedirectUri.set -> void +Microsoft.Identity.Web.MergedOptionsLogging Microsoft.Identity.Web.MergedOptionsStore Microsoft.Identity.Web.MergedOptionsStore.Get(string! name) -> Microsoft.Identity.Web.MergedOptions! Microsoft.Identity.Web.MergedOptionsStore.MergedOptionsStore() -> void +Microsoft.Identity.Web.MergedOptionsStore.MergedOptionsStore(System.IServiceProvider! serviceProvider) -> void Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder(Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfigurationSection? configurationSection = null) -> void Microsoft.Identity.Web.MicrosoftIdentityApplicationOptionsMerger Microsoft.Identity.Web.MicrosoftIdentityApplicationOptionsMerger.MicrosoftIdentityApplicationOptionsMerger(Microsoft.Identity.Web.IMergedOptionsStore! mergedOptions) -> void @@ -266,7 +280,7 @@ Microsoft.Identity.Web.TokenAcquisition.GetAccessTokenForUserAsync(System.Collec Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForAppAsync(string! scope, string? authenticationScheme = null, string? tenant = null, Microsoft.Identity.Web.TokenAcquisitionOptions? tokenAcquisitionOptions = null) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForUserAsync(System.Collections.Generic.IEnumerable! scopes, string? authenticationScheme = null, string? tenantId = null, string? userFlow = null, System.Security.Claims.ClaimsPrincipal? user = null, Microsoft.Identity.Web.TokenAcquisitionOptions? tokenAcquisitionOptions = null) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.TokenAcquisition.GetEffectiveAuthenticationScheme(string? authenticationScheme) -> string! -Microsoft.Identity.Web.TokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(Microsoft.Identity.Web.MergedOptions! mergedOptions) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.TokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(Microsoft.Identity.Web.MergedOptions! mergedOptions, bool isTokenBinding) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.TokenAcquisition.GetOrBuildManagedIdentityApplicationAsync(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Abstractions.ManagedIdentityOptions! managedIdentityOptions) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.TokenAcquisition.Logger Microsoft.Identity.Web.TokenAcquisition.RemoveAccountAsync(System.Security.Claims.ClaimsPrincipal! user, string? authenticationScheme = null) -> System.Threading.Tasks.Task! @@ -286,7 +300,10 @@ Microsoft.Identity.Web.TokenAcquisitionAspnetCoreHost.GetUserFromRequest() -> Sy Microsoft.Identity.Web.TokenAcquisitionAspnetCoreHost.SetHttpResponse(System.Net.HttpStatusCode statusCode, string! wwwAuthenticate) -> void Microsoft.Identity.Web.TokenAcquisitionAspnetCoreHost.SetSession(string! key, string! value) -> void Microsoft.Identity.Web.TokenAcquisitionAspnetCoreHost.TokenAcquisitionAspnetCoreHost(Microsoft.AspNetCore.Http.IHttpContextAccessor! httpContextAccessor, Microsoft.Identity.Web.IMergedOptionsStore! mergedOptionsMonitor, System.IServiceProvider! serviceProvider) -> void +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeOnBehalfOfInitializedAsync(Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForApp(Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions) -> void +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForOnBehalfOf(Microsoft.Identity.Client.AcquireTokenOnBehalfOfParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> void +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForOnBehalfOfAsync(Microsoft.Identity.Client.AcquireTokenOnBehalfOfParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForTestUser(Microsoft.Identity.Client.AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> void Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForTestUserAsync(Microsoft.Identity.Client.AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.Util.Base64UrlHelpers @@ -308,22 +325,25 @@ static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logg static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.FailedToLoadCredentials(Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Abstractions.CredentialDescription! certificateDescription) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.NotUsingManagedIdentity(Microsoft.Extensions.Logging.ILogger! logger, string! message) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingCertThumbprint(Microsoft.Extensions.Logging.ILogger! logger, string! certThumbprint) -> void +static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingCertThumbprint(Microsoft.Extensions.Logging.ILogger! logger, string? certThumbprint) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingManagedIdentity(Microsoft.Extensions.Logging.ILogger! logger) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingPodIdentityFile(Microsoft.Extensions.Logging.ILogger! logger, string! signedAssertionFileDiskPath) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingSignedAssertionFromCustomProvider(Microsoft.Extensions.Logging.ILogger! logger, string! signedAssertionUri) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingSignedAssertionFromVault(Microsoft.Extensions.Logging.ILogger! logger, string! signedAssertionUri) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.WithClientCredentials(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder! builder, System.Collections.Generic.IEnumerable! clientCredentials, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Abstractions.ICredentialsLoader! credentialsLoader, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters! credentialSourceLoaderParameters) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder! -static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.WithClientCredentialsAsync(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder! builder, System.Collections.Generic.IEnumerable! clientCredentials, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Abstractions.ICredentialsLoader! credentialsLoader, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters) -> System.Threading.Tasks.Task! +static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.WithClientCredentialsAsync(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder! builder, System.Collections.Generic.IEnumerable! clientCredentials, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Abstractions.ICredentialsLoader! credentialsLoader, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, bool isTokenBinding) -> System.Threading.Tasks.Task! static Microsoft.Identity.Web.DefaultTokenAcquirerFactoryImplementation.GetKey(string? authority, string? clientId, string? region) -> string! static Microsoft.Identity.Web.HttpContextExtensions.GetTokenUsedToCallWebAPI(this Microsoft.AspNetCore.Http.HttpContext! httpContext) -> Microsoft.IdentityModel.Tokens.SecurityToken? static Microsoft.Identity.Web.HttpContextExtensions.StoreTokenUsedToCallWebAPI(this Microsoft.AspNetCore.Http.HttpContext! httpContext, Microsoft.IdentityModel.Tokens.SecurityToken? token) -> void static Microsoft.Identity.Web.IdHelper.CreateTelemetryInfo() -> string! +static Microsoft.Identity.Web.MergedOptions.ParseAuthorityIfNecessary(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Extensions.Logging.ILogger? logger = null) -> void static Microsoft.Identity.Web.MergedOptions.ParseAuthorityIfNecessary(Microsoft.Identity.Web.MergedOptions! mergedOptions) -> void static Microsoft.Identity.Web.MergedOptions.UpdateConfidentialClientApplicationOptionsFromMergedOptions(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Client.ConfidentialClientApplicationOptions! confidentialClientApplicationOptions) -> void static Microsoft.Identity.Web.MergedOptions.UpdateMergedOptionsFromConfidentialClientApplicationOptions(Microsoft.Identity.Client.ConfidentialClientApplicationOptions! confidentialClientApplicationOptions, Microsoft.Identity.Web.MergedOptions! mergedOptions) -> void static Microsoft.Identity.Web.MergedOptions.UpdateMergedOptionsFromJwtBearerOptions(Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions! jwtBearerOptions, Microsoft.Identity.Web.MergedOptions! mergedOptions) -> void static Microsoft.Identity.Web.MergedOptions.UpdateMergedOptionsFromMicrosoftIdentityApplicationOptions(Microsoft.Identity.Abstractions.MicrosoftIdentityApplicationOptions! microsoftIdentityApplicationOptions, Microsoft.Identity.Web.MergedOptions! mergedOptions) -> void static Microsoft.Identity.Web.MergedOptions.UpdateMergedOptionsFromMicrosoftIdentityOptions(Microsoft.Identity.Web.MicrosoftIdentityOptions! microsoftIdentityOptions, Microsoft.Identity.Web.MergedOptions! mergedOptions) -> void +static Microsoft.Identity.Web.MergedOptionsLogging.AuthorityIgnored(Microsoft.Extensions.Logging.ILogger! logger, string! authority, string! instance, string! tenantId) -> void static Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder.SetIdentityModelLogger(System.IServiceProvider! serviceProvider) -> void static Microsoft.Identity.Web.MsAuth10AtPop.WithAtPop(this Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! builder, string! popPublicKey, string! jwkClaim) -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! static Microsoft.Identity.Web.MsAuth10AtPop.WithAtPop(this Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! builder, System.Security.Cryptography.X509Certificates.X509Certificate2! clientCertificate, string! popPublicKey, string! jwkClaim, string! clientId, bool sendX5C) -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! @@ -334,12 +354,16 @@ static Microsoft.Identity.Web.TokenAcquisition.GetCacheKeyForManagedId(Microsoft static Microsoft.Identity.Web.TokenAcquisition.Logger.TokenAcquisitionError(Microsoft.Extensions.Logging.ILogger! logger, string! msalErrorMessage, System.Exception? ex) -> void static Microsoft.Identity.Web.TokenAcquisition.Logger.TokenAcquisitionMsalAuthenticationResultTime(Microsoft.Extensions.Logging.ILogger! logger, long durationTotalInMs, long durationInHttpInMs, long durationInCacheInMs, string! tokenSource, string! correlationId, string! cacheRefreshReason, System.Exception? ex) -> void static Microsoft.Identity.Web.TokenAcquisition.MergeExtraQueryParameters(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Web.TokenAcquisitionOptions! tokenAcquisitionOptions) -> System.Collections.Generic.Dictionary? +static Microsoft.Identity.Web.TokenAcquisition.MergeExtraQueryParameters(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Web.TokenAcquisitionOptions? tokenAcquisitionOptions) -> System.Collections.Generic.Dictionary? static Microsoft.Identity.Web.TokenAcquisition.ResolveTenant(string? tenant, Microsoft.Identity.Web.MergedOptions! mergedOptions) -> string? static Microsoft.Identity.Web.Util.Base64UrlHelpers.Decode(string! arg) -> string? static Microsoft.Identity.Web.Util.Base64UrlHelpers.DecodeBytes(string? str) -> byte[]? static Microsoft.Identity.Web.Util.Base64UrlHelpers.Encode(byte[]? inArray) -> string? static Microsoft.Identity.Web.Util.Base64UrlHelpers.Encode(string? arg) -> string? static Microsoft.Identity.Web.Util.Base64UrlHelpers.EncodeString(string? str) -> string? +static readonly Microsoft.Identity.Web.Constants.s_certificateRelatedErrorCodes -> System.Collections.Generic.HashSet! +static readonly Microsoft.Identity.Web.Constants.s_nonRetryableConfigErrorCodes -> System.Collections.Generic.HashSet! +static readonly Microsoft.Identity.Web.LoggingEventId.AuthorityIgnored -> Microsoft.Extensions.Logging.EventId static readonly Microsoft.Identity.Web.LoggingEventId.CredentialLoadAttempt -> Microsoft.Extensions.Logging.EventId static readonly Microsoft.Identity.Web.LoggingEventId.CredentialLoadAttemptFailed -> Microsoft.Extensions.Logging.EventId static readonly Microsoft.Identity.Web.LoggingEventId.NotUsingManagedIdentity -> Microsoft.Extensions.Logging.EventId diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/InternalAPI.Unshipped.txt index 62d5c481d..7dc5c5811 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/InternalAPI.Unshipped.txt @@ -1,2 +1 @@ #nullable enable -static Microsoft.Identity.Web.TokenAcquisition.MergeExtraQueryParameters(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Web.TokenAcquisitionOptions? tokenAcquisitionOptions) -> System.Collections.Generic.Dictionary? diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/PublicAPI.Shipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/PublicAPI.Shipped.txt index bcd69bef3..fed3386fe 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/PublicAPI.Shipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/PublicAPI.Shipped.txt @@ -32,7 +32,11 @@ const Microsoft.Identity.Web.Constants.ReturnUrl = "ReturnUrl" -> string! const Microsoft.Identity.Web.Constants.Scope = "scope" -> string! const Microsoft.Identity.Web.Constants.SpaAuthCode = "SpaAuthCode" -> string! Microsoft.Identity.Web.ApplicationBuilderExtensions +Microsoft.Identity.Web.BeforeOnBehalfOfInitialized +Microsoft.Identity.Web.BeforeOnBehalfOfInitializedAsync Microsoft.Identity.Web.BeforeTokenAcquisitionForApp +Microsoft.Identity.Web.BeforeTokenAcquisitionForOnBehalfOf +Microsoft.Identity.Web.BeforeTokenAcquisitionForOnBehalfOfAsync Microsoft.Identity.Web.BeforeTokenAcquisitionForTestUser Microsoft.Identity.Web.BeforeTokenAcquisitionForTestUserAsync Microsoft.Identity.Web.ClaimConstants @@ -55,6 +59,7 @@ Microsoft.Identity.Web.Experimental.ICertificatesObserver Microsoft.Identity.Web.Experimental.ICertificatesObserver.OnClientCertificateChanged(Microsoft.Identity.Web.Experimental.CertificateChangeEventArg! e) -> void Microsoft.Identity.Web.Extensibility.BaseAuthorizationHeaderProvider Microsoft.Identity.Web.Extensibility.BaseAuthorizationHeaderProvider.BaseAuthorizationHeaderProvider(System.IServiceProvider! serviceProvider) -> void +Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions Microsoft.Identity.Web.IAuthenticationSchemeInformationProvider Microsoft.Identity.Web.Internal.WebApiBuilders Microsoft.Identity.Web.ITokenAcquisition @@ -73,11 +78,21 @@ Microsoft.Identity.Web.ITokenAcquisition.ReplyForbiddenWithWwwAuthenticateHeader Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder.AddDistributedTokenCaches() -> Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder! Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder.AddInMemoryTokenCaches(System.Action? configureOptions = null, System.Action? memoryCacheOptions = null) -> Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder! +Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException +Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException.MicrosoftIdentityAuthenticationException(string! message, System.Exception! innerException) -> void +Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException.MicrosoftIdentityAuthenticationException(string! message) -> void Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder.ConfigurationSection.get -> Microsoft.Extensions.Configuration.IConfigurationSection? Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder.ConfigurationSection.set -> void Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder.MicrosoftIdentityBaseAuthenticationBuilder(Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfigurationSection? configurationSection = null) -> void Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder.Services.get -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions +Microsoft.Identity.Web.MicrosoftIdentityMessageHandler +Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.MicrosoftIdentityMessageHandler(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! headerProvider, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? defaultOptions = null, Microsoft.Extensions.Logging.ILogger? logger = null) -> void +Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions +Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.MicrosoftIdentityMessageHandlerOptions() -> void +Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.get -> System.Collections.Generic.IList! +Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.set -> void Microsoft.Identity.Web.MicrosoftIdentityOptions Microsoft.Identity.Web.MicrosoftIdentityOptions.AllowWebApiToBeAuthorizedByACL.get -> bool Microsoft.Identity.Web.MicrosoftIdentityOptions.AllowWebApiToBeAuthorizedByACL.set -> void @@ -127,6 +142,17 @@ Microsoft.Identity.Web.MicrosoftIdentityWebChallengeUserException.Scopes.get -> Microsoft.Identity.Web.MicrosoftIdentityWebChallengeUserException.Scopes.set -> void Microsoft.Identity.Web.MicrosoftIdentityWebChallengeUserException.Userflow.get -> string? Microsoft.Identity.Web.MicrosoftIdentityWebChallengeUserException.Userflow.set -> void +Microsoft.Identity.Web.MsalMtlsHttpClientFactory +Microsoft.Identity.Web.MsalMtlsHttpClientFactory.Dispose() -> void +Microsoft.Identity.Web.MsalMtlsHttpClientFactory.GetHttpClient() -> System.Net.Http.HttpClient! +Microsoft.Identity.Web.MsalMtlsHttpClientFactory.GetHttpClient(System.Security.Cryptography.X509Certificates.X509Certificate2! x509Certificate2) -> System.Net.Http.HttpClient! +Microsoft.Identity.Web.MsalMtlsHttpClientFactory.MsalMtlsHttpClientFactory(System.Net.Http.IHttpClientFactory! httpClientFactory) -> void +Microsoft.Identity.Web.OnBehalfOfEventArgs +Microsoft.Identity.Web.OnBehalfOfEventArgs.OnBehalfOfEventArgs() -> void +Microsoft.Identity.Web.OnBehalfOfEventArgs.User.get -> System.Security.Claims.ClaimsPrincipal? +Microsoft.Identity.Web.OnBehalfOfEventArgs.User.set -> void +Microsoft.Identity.Web.OnBehalfOfEventArgs.UserAssertionToken.get -> string? +Microsoft.Identity.Web.OnBehalfOfEventArgs.UserAssertionToken.set -> void Microsoft.Identity.Web.PrincipalExtensionsForSecurityTokens Microsoft.Identity.Web.ServiceCollectionExtensions Microsoft.Identity.Web.TestOnly.TokenAcquirerFactoryTesting @@ -143,7 +169,11 @@ Microsoft.Identity.Web.TokenAcquirerFactory.ServiceProvider.set -> void Microsoft.Identity.Web.TokenAcquirerFactory.Services.get -> Microsoft.Extensions.DependencyInjection.ServiceCollection! Microsoft.Identity.Web.TokenAcquirerFactory.TokenAcquirerFactory() -> void Microsoft.Identity.Web.TokenAcquisitionExtensionOptions +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeOnBehalfOfInitialized -> Microsoft.Identity.Web.BeforeOnBehalfOfInitialized? +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeOnBehalfOfInitializedAsync -> Microsoft.Identity.Web.BeforeOnBehalfOfInitializedAsync? Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeTokenAcquisitionForApp -> Microsoft.Identity.Web.BeforeTokenAcquisitionForApp? +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeTokenAcquisitionForOnBehalfOf -> Microsoft.Identity.Web.BeforeTokenAcquisitionForOnBehalfOf? +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeTokenAcquisitionForOnBehalfOfAsync -> Microsoft.Identity.Web.BeforeTokenAcquisitionForOnBehalfOfAsync? Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeTokenAcquisitionForTestUser -> Microsoft.Identity.Web.BeforeTokenAcquisitionForTestUser? Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeTokenAcquisitionForTestUserAsync -> Microsoft.Identity.Web.BeforeTokenAcquisitionForTestUserAsync? Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.TokenAcquisitionExtensionOptions() -> void @@ -154,8 +184,16 @@ Microsoft.Identity.Web.TokenAcquisitionOptions.Clone() -> Microsoft.Identity.Web Microsoft.Identity.Web.TokenAcquisitionOptions.PoPConfiguration.get -> Microsoft.Identity.Client.AppConfig.PoPAuthenticationConfiguration? Microsoft.Identity.Web.TokenAcquisitionOptions.PoPConfiguration.set -> void Microsoft.Identity.Web.TokenAcquisitionOptions.TokenAcquisitionOptions() -> void +override Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.SendAsync(System.Net.Http.HttpRequestMessage! request, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! static Microsoft.Identity.Web.ApplicationBuilderExtensions.UseTokenAcquirerFactory(this Microsoft.AspNetCore.Builder.IApplicationBuilder! applicationBuilder) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! +static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.GetAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request) -> Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? +static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! options) -> System.Net.Http.HttpRequestMessage! +static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, System.Action! configure) -> System.Net.Http.HttpRequestMessage! static Microsoft.Identity.Web.Internal.WebApiBuilders.EnableTokenAcquisition(System.Action! configureConfidentialClientApplicationOptions, string! authenticationScheme, Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfigurationSection? configuration) -> Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder, Microsoft.Extensions.Configuration.IConfiguration! configuration, string! sectionName) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! options) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! static Microsoft.Identity.Web.PrincipalExtensionsForSecurityTokens.GetBootstrapToken(this System.Security.Principal.IPrincipal! claimsPrincipal) -> Microsoft.IdentityModel.Tokens.SecurityToken? static Microsoft.Identity.Web.ServiceCollectionExtensions.AddTokenAcquisition(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, bool isTokenAcquisitionSingleton = false) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.Identity.Web.TestOnly.TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest() -> void @@ -163,7 +201,11 @@ static Microsoft.Identity.Web.TokenAcquirerExtensions.GetFicTokenAsync(this Micr static Microsoft.Identity.Web.TokenAcquirerExtensions.WithClientAssertion(this Microsoft.Identity.Abstractions.AcquireTokenOptions! options, string! clientAssertion) -> Microsoft.Identity.Abstractions.AcquireTokenOptions! static Microsoft.Identity.Web.TokenAcquirerFactory.GetDefaultInstance(string! configSection = "AzureAd") -> Microsoft.Identity.Web.TokenAcquirerFactory! static Microsoft.Identity.Web.TokenAcquirerFactory.GetDefaultInstance(string! configSection = "AzureAd") -> T! +virtual Microsoft.Identity.Web.BeforeOnBehalfOfInitialized.Invoke(Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> void +virtual Microsoft.Identity.Web.BeforeOnBehalfOfInitializedAsync.Invoke(Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> System.Threading.Tasks.Task! virtual Microsoft.Identity.Web.BeforeTokenAcquisitionForApp.Invoke(Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions) -> void +virtual Microsoft.Identity.Web.BeforeTokenAcquisitionForOnBehalfOf.Invoke(Microsoft.Identity.Client.AcquireTokenOnBehalfOfParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> void +virtual Microsoft.Identity.Web.BeforeTokenAcquisitionForOnBehalfOfAsync.Invoke(Microsoft.Identity.Client.AcquireTokenOnBehalfOfParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> System.Threading.Tasks.Task! virtual Microsoft.Identity.Web.BeforeTokenAcquisitionForTestUser.Invoke(Microsoft.Identity.Client.AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> void virtual Microsoft.Identity.Web.BeforeTokenAcquisitionForTestUserAsync.Invoke(Microsoft.Identity.Client.AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> System.Threading.Tasks.Task! virtual Microsoft.Identity.Web.Extensibility.BaseAuthorizationHeaderProvider.CreateAuthorizationHeaderAsync(System.Collections.Generic.IEnumerable! scopes, Microsoft.Identity.Abstractions.AuthorizationHeaderProviderOptions? authorizationHeaderProviderOptions = null, System.Security.Claims.ClaimsPrincipal? claimsPrincipal = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/PublicAPI.Unshipped.txt index 13496576f..7dc5c5811 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -1,15 +1 @@ #nullable enable -Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions -Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException -Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException.MicrosoftIdentityAuthenticationException(string! message) -> void -Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException.MicrosoftIdentityAuthenticationException(string! message, System.Exception! innerException) -> void -Microsoft.Identity.Web.MicrosoftIdentityMessageHandler -Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.MicrosoftIdentityMessageHandler(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! headerProvider, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? defaultOptions = null, Microsoft.Extensions.Logging.ILogger? logger = null) -> void -Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions -Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.MicrosoftIdentityMessageHandlerOptions() -> void -Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.get -> System.Collections.Generic.IList! -Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.set -> void -override Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.SendAsync(System.Net.Http.HttpRequestMessage! request, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.GetAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request) -> Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? -static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! options) -> System.Net.Http.HttpRequestMessage! -static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, System.Action! configure) -> System.Net.Http.HttpRequestMessage! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/InternalAPI.Shipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/InternalAPI.Shipped.txt index 17650922c..ec23fff4b 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/InternalAPI.Shipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/InternalAPI.Shipped.txt @@ -3,13 +3,16 @@ const Microsoft.Identity.Web.Constants.AgentIdentityKey = "IDWEB_AGENT_IDENTITY" const Microsoft.Identity.Web.Constants.Aliases = "aliases" -> string! const Microsoft.Identity.Web.Constants.ApiVersion = "api-version" -> string! const Microsoft.Identity.Web.Constants.ApplicationJson = "application/json" -> string! +const Microsoft.Identity.Web.Constants.ApplicationNotFound = "AADSTS700016" -> string! const Microsoft.Identity.Web.Constants.Authorization = "Authorization" -> string! const Microsoft.Identity.Web.Constants.AzureADIssuerMetadataUrl = "https://login.microsoftonline.com/common/discovery/instance?authorization_endpoint=https://login.microsoftonline.com/common/oauth2/v2.0/authorize&api-version=1.1" -> string! const Microsoft.Identity.Web.Constants.BlazorChallengeUri = "MicrosoftIdentity/Account/Challenge?redirectUri=" -> string! const Microsoft.Identity.Web.Constants.CertificateHasBeenRevoked = "AADSTS7000214" -> string! const Microsoft.Identity.Web.Constants.CertificateIsOutsideValidityWindow = "AADSTS1000502" -> string! +const Microsoft.Identity.Web.Constants.CertificateWasRevoked = "AADSTS7000277" -> string! const Microsoft.Identity.Web.Constants.CiamAuthoritySuffix = ".ciamlogin.com" -> string! const Microsoft.Identity.Web.Constants.ClientAssertion = "IDWEB_CLIENT_ASSERTION" -> string! +const Microsoft.Identity.Web.Constants.ClientAssertionContainsInvalidSignature = "AADSTS7000274" -> string! const Microsoft.Identity.Web.Constants.ClientInfo = "client_info" -> string! const Microsoft.Identity.Web.Constants.Common = "common" -> string! const Microsoft.Identity.Web.Constants.Consent = "consent" -> string! @@ -23,6 +26,7 @@ const Microsoft.Identity.Web.Constants.FmiPathForClientAssertion = "IDWEB_FMI_PA const Microsoft.Identity.Web.Constants.GraphBaseUrlV1 = "https://graph.microsoft.com/v1.0" -> string! const Microsoft.Identity.Web.Constants.IDWebSku = "IDWeb." -> string! const Microsoft.Identity.Web.Constants.InvalidClient = "invalid_client" -> string! +const Microsoft.Identity.Web.Constants.InvalidClientSecret = "AADSTS7000215" -> string! const Microsoft.Identity.Web.Constants.InvalidKeyError = "AADSTS700027" -> string! const Microsoft.Identity.Web.Constants.ISessionStore = "ISessionStore" -> string! const Microsoft.Identity.Web.Constants.JwtSecurityTokenUsedToCallWebApi = "JwtSecurityTokenUsedToCallWebAPI" -> string! @@ -89,9 +93,11 @@ const Microsoft.Identity.Web.IDWebErrorMessage.InvalidSubAssertion = "IDW10505: const Microsoft.Identity.Web.IDWebErrorMessage.IssuerDoesNotMatchValidIssuers = "IDW10303: Issuer: '{0}', does not match any of the valid issuers provided for this application. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.IssuerMetadataUrlIsRequired = "IDW10301: Azure AD Issuer metadata address URL is required. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.MicrosoftIdentityWebChallengeUserException = "IDW10502: An MsalUiRequiredException was thrown due to a challenge for the user. See https://aka.ms/ms-id-web/ca_incremental-consent. " -> string! +const Microsoft.Identity.Web.IDWebErrorMessage.MissingIdentityConfiguration = "IDW10708: The identity configuration is incomplete. Provide either 'Instance' and 'TenantId', or 'Authority', or enable 'ManagedIdentity' in the configuration. Check your configuration keys for typos (e.g., trailing spaces). See https://aka.ms/ms-id-web/configuration. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.MissingRequiredScopesForAuthorizationFilter = "IDW10108: RequiredScope Attribute does not contain a value. The scopes need to be set on the controller, the page or action. See https://aka.ms/ms-id-web/required-scope-attribute. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.MissingRoles = "IDW10202: The 'roles' or 'role' claim does not contain roles '{0}' or was not found. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.MissingScopes = "IDW10203: The 'scope' or 'scp' claim does not contain scopes '{0}' or was not found. " -> string! +const Microsoft.Identity.Web.IDWebErrorMessage.MissingTokenBindingCertificate = "IDW10115: A signing certificate, which is required for token binding, is missing in loaded credentials." -> string! const Microsoft.Identity.Web.IDWebErrorMessage.NeitherScopeOrRolesClaimFoundInToken = "IDW10201: Neither scope nor roles claim was found in the bearer token. Authentication scheme used: '{0}'. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.NoMetadataDocumentRetrieverProvided = "IDW10302: No metadata document retriever is provided. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.NoScopesProvided = "IDW10103: No scopes provided in scopes... " -> string! @@ -102,6 +108,7 @@ const Microsoft.Identity.Web.IDWebErrorMessage.ScopeKeySectionIsProvidedButNotPr const Microsoft.Identity.Web.IDWebErrorMessage.ScopesNotConfiguredInConfigurationOrViaDelegate = "IDW10107: Scopes need to be passed-in either by configuration or by the delegate overriding it. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.ScopesRequiredToCallMicrosoftGraph = "IDW10208: You need to either pass-in scopes to AddMicrosoftGraph, in the appsettings.json file, or with .WithScopes() on the Graph queries. See https://aka.ms/ms-id-web/microsoftGraph. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.TenantIdClaimNotPresentInToken = "IDW10401: Neither `tid` nor `tenantId` claim is present in the token obtained from Microsoft identity platform. " -> string! +const Microsoft.Identity.Web.IDWebErrorMessage.TokenBindingRequiresEnabledAppTokenAcquisition = "IDW10116: Token binding requires enabled app token acquisition." -> string! const Microsoft.Identity.Web.IDWebErrorMessage.TokenIsNotJwtToken = "IDW10403: Token is not a JWT token. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.UnauthenticatedUser = "IDW10204: The user is unauthenticated. The HttpContext does not contain any claims. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.WithClientCredentialsIsObsolete = "Use WithClientCredentialsAsync instead." -> string! @@ -158,6 +165,7 @@ Microsoft.Identity.Web.DefaultAuthorizationHeaderProvider Microsoft.Identity.Web.DefaultAuthorizationHeaderProvider.CreateAuthorizationHeaderAsync(System.Collections.Generic.IEnumerable! scopes, Microsoft.Identity.Abstractions.AuthorizationHeaderProviderOptions? downstreamApiOptions = null, System.Security.Claims.ClaimsPrincipal? claimsPrincipal = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DefaultAuthorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync(string! scopes, Microsoft.Identity.Abstractions.AuthorizationHeaderProviderOptions? downstreamApiOptions = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DefaultAuthorizationHeaderProvider.CreateAuthorizationHeaderForUserAsync(System.Collections.Generic.IEnumerable! scopes, Microsoft.Identity.Abstractions.AuthorizationHeaderProviderOptions? downstreamApiOptions = null, System.Security.Claims.ClaimsPrincipal? claimsPrincipal = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.DefaultAuthorizationHeaderProvider.CreateBoundAuthorizationHeaderAsync(Microsoft.Identity.Abstractions.DownstreamApiOptions! downstreamApiOptions, System.Security.Claims.ClaimsPrincipal? claimsPrincipal = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task>! Microsoft.Identity.Web.DefaultAuthorizationHeaderProvider.DefaultAuthorizationHeaderProvider(Microsoft.Identity.Web.ITokenAcquisition! tokenAcquisition) -> void Microsoft.Identity.Web.DefaultTokenAcquirerFactoryImplementation Microsoft.Identity.Web.DefaultTokenAcquirerFactoryImplementation.DefaultTokenAcquirerFactoryImplementation(System.IServiceProvider! serviceProvider) -> void @@ -180,6 +188,7 @@ Microsoft.Identity.Web.IdHelper Microsoft.Identity.Web.IDWebErrorMessage Microsoft.Identity.Web.IMergedOptionsStore Microsoft.Identity.Web.IMergedOptionsStore.Get(string! name) -> Microsoft.Identity.Web.MergedOptions! +Microsoft.Identity.Web.Internal.MicrosoftIdentityOptionsBinder Microsoft.Identity.Web.ITokenAcquisitionHost Microsoft.Identity.Web.ITokenAcquisitionHost.GetAuthenticatedUserAsync(System.Security.Claims.ClaimsPrincipal? user) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.ITokenAcquisitionHost.GetCurrentRedirectUri(Microsoft.Identity.Web.MergedOptions! mergedOptions) -> string? @@ -225,6 +234,10 @@ Microsoft.Identity.Web.MergedOptions.EnablePiiLogging.get -> bool Microsoft.Identity.Web.MergedOptions.EnablePiiLogging.set -> void Microsoft.Identity.Web.MergedOptions.IsDefaultPlatformLoggingEnabled.get -> bool Microsoft.Identity.Web.MergedOptions.IsDefaultPlatformLoggingEnabled.set -> void +Microsoft.Identity.Web.MergedOptions.IsTokenBinding.get -> bool +Microsoft.Identity.Web.MergedOptions.IsTokenBinding.set -> void +Microsoft.Identity.Web.MergedOptions.Logger.get -> Microsoft.Extensions.Logging.ILogger? +Microsoft.Identity.Web.MergedOptions.Logger.set -> void Microsoft.Identity.Web.MergedOptions.LogLevel.get -> Microsoft.Identity.Client.LogLevel Microsoft.Identity.Web.MergedOptions.LogLevel.set -> void Microsoft.Identity.Web.MergedOptions.MergedOptions() -> void @@ -237,9 +250,11 @@ Microsoft.Identity.Web.MergedOptions.PreserveAuthority.get -> bool Microsoft.Identity.Web.MergedOptions.PreserveAuthority.set -> void Microsoft.Identity.Web.MergedOptions.RedirectUri.get -> string? Microsoft.Identity.Web.MergedOptions.RedirectUri.set -> void +Microsoft.Identity.Web.MergedOptionsLogging Microsoft.Identity.Web.MergedOptionsStore Microsoft.Identity.Web.MergedOptionsStore.Get(string! name) -> Microsoft.Identity.Web.MergedOptions! Microsoft.Identity.Web.MergedOptionsStore.MergedOptionsStore() -> void +Microsoft.Identity.Web.MergedOptionsStore.MergedOptionsStore(System.IServiceProvider! serviceProvider) -> void Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder(Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfigurationSection? configurationSection = null) -> void Microsoft.Identity.Web.MicrosoftIdentityApplicationOptionsMerger Microsoft.Identity.Web.MicrosoftIdentityApplicationOptionsMerger.MicrosoftIdentityApplicationOptionsMerger(Microsoft.Identity.Web.IMergedOptionsStore! mergedOptions) -> void @@ -266,7 +281,7 @@ Microsoft.Identity.Web.TokenAcquisition.GetAccessTokenForUserAsync(System.Collec Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForAppAsync(string! scope, string? authenticationScheme = null, string? tenant = null, Microsoft.Identity.Web.TokenAcquisitionOptions? tokenAcquisitionOptions = null) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForUserAsync(System.Collections.Generic.IEnumerable! scopes, string? authenticationScheme = null, string? tenantId = null, string? userFlow = null, System.Security.Claims.ClaimsPrincipal? user = null, Microsoft.Identity.Web.TokenAcquisitionOptions? tokenAcquisitionOptions = null) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.TokenAcquisition.GetEffectiveAuthenticationScheme(string? authenticationScheme) -> string! -Microsoft.Identity.Web.TokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(Microsoft.Identity.Web.MergedOptions! mergedOptions) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.TokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(Microsoft.Identity.Web.MergedOptions! mergedOptions, bool isTokenBinding) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.TokenAcquisition.GetOrBuildManagedIdentityApplicationAsync(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Abstractions.ManagedIdentityOptions! managedIdentityOptions) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.TokenAcquisition.Logger Microsoft.Identity.Web.TokenAcquisition.RemoveAccountAsync(System.Security.Claims.ClaimsPrincipal! user, string? authenticationScheme = null) -> System.Threading.Tasks.Task! @@ -286,7 +301,12 @@ Microsoft.Identity.Web.TokenAcquisitionAspnetCoreHost.GetUserFromRequest() -> Sy Microsoft.Identity.Web.TokenAcquisitionAspnetCoreHost.SetHttpResponse(System.Net.HttpStatusCode statusCode, string! wwwAuthenticate) -> void Microsoft.Identity.Web.TokenAcquisitionAspnetCoreHost.SetSession(string! key, string! value) -> void Microsoft.Identity.Web.TokenAcquisitionAspnetCoreHost.TokenAcquisitionAspnetCoreHost(Microsoft.AspNetCore.Http.IHttpContextAccessor! httpContextAccessor, Microsoft.Identity.Web.IMergedOptionsStore! mergedOptionsMonitor, System.IServiceProvider! serviceProvider) -> void +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeOnBehalfOfInitializedAsync(Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForApp(Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions) -> void +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForOnBehalfOf(Microsoft.Identity.Client.AcquireTokenOnBehalfOfParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> void +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForOnBehalfOf(Microsoft.Identity.Client.AcquireTokenOnBehalfOfParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> void +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForOnBehalfOfAsync(Microsoft.Identity.Client.AcquireTokenOnBehalfOfParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForOnBehalfOfAsync(Microsoft.Identity.Client.AcquireTokenOnBehalfOfParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForTestUser(Microsoft.Identity.Client.AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> void Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForTestUserAsync(Microsoft.Identity.Client.AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.Util.Base64UrlHelpers @@ -308,22 +328,25 @@ static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logg static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.FailedToLoadCredentials(Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Abstractions.CredentialDescription! certificateDescription) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.NotUsingManagedIdentity(Microsoft.Extensions.Logging.ILogger! logger, string! message) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingCertThumbprint(Microsoft.Extensions.Logging.ILogger! logger, string! certThumbprint) -> void +static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingCertThumbprint(Microsoft.Extensions.Logging.ILogger! logger, string? certThumbprint) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingManagedIdentity(Microsoft.Extensions.Logging.ILogger! logger) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingPodIdentityFile(Microsoft.Extensions.Logging.ILogger! logger, string! signedAssertionFileDiskPath) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingSignedAssertionFromCustomProvider(Microsoft.Extensions.Logging.ILogger! logger, string! signedAssertionUri) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingSignedAssertionFromVault(Microsoft.Extensions.Logging.ILogger! logger, string! signedAssertionUri) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.WithClientCredentials(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder! builder, System.Collections.Generic.IEnumerable! clientCredentials, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Abstractions.ICredentialsLoader! credentialsLoader, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters! credentialSourceLoaderParameters) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder! -static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.WithClientCredentialsAsync(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder! builder, System.Collections.Generic.IEnumerable! clientCredentials, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Abstractions.ICredentialsLoader! credentialsLoader, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters) -> System.Threading.Tasks.Task! +static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.WithClientCredentialsAsync(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder! builder, System.Collections.Generic.IEnumerable! clientCredentials, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Abstractions.ICredentialsLoader! credentialsLoader, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, bool isTokenBinding) -> System.Threading.Tasks.Task! static Microsoft.Identity.Web.DefaultTokenAcquirerFactoryImplementation.GetKey(string? authority, string? clientId, string? region) -> string! static Microsoft.Identity.Web.HttpContextExtensions.GetTokenUsedToCallWebAPI(this Microsoft.AspNetCore.Http.HttpContext! httpContext) -> Microsoft.IdentityModel.Tokens.SecurityToken? static Microsoft.Identity.Web.HttpContextExtensions.StoreTokenUsedToCallWebAPI(this Microsoft.AspNetCore.Http.HttpContext! httpContext, Microsoft.IdentityModel.Tokens.SecurityToken? token) -> void static Microsoft.Identity.Web.IdHelper.CreateTelemetryInfo() -> string! +static Microsoft.Identity.Web.MergedOptions.ParseAuthorityIfNecessary(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Extensions.Logging.ILogger? logger = null) -> void static Microsoft.Identity.Web.MergedOptions.ParseAuthorityIfNecessary(Microsoft.Identity.Web.MergedOptions! mergedOptions) -> void static Microsoft.Identity.Web.MergedOptions.UpdateConfidentialClientApplicationOptionsFromMergedOptions(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Client.ConfidentialClientApplicationOptions! confidentialClientApplicationOptions) -> void static Microsoft.Identity.Web.MergedOptions.UpdateMergedOptionsFromConfidentialClientApplicationOptions(Microsoft.Identity.Client.ConfidentialClientApplicationOptions! confidentialClientApplicationOptions, Microsoft.Identity.Web.MergedOptions! mergedOptions) -> void static Microsoft.Identity.Web.MergedOptions.UpdateMergedOptionsFromJwtBearerOptions(Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions! jwtBearerOptions, Microsoft.Identity.Web.MergedOptions! mergedOptions) -> void static Microsoft.Identity.Web.MergedOptions.UpdateMergedOptionsFromMicrosoftIdentityApplicationOptions(Microsoft.Identity.Abstractions.MicrosoftIdentityApplicationOptions! microsoftIdentityApplicationOptions, Microsoft.Identity.Web.MergedOptions! mergedOptions) -> void static Microsoft.Identity.Web.MergedOptions.UpdateMergedOptionsFromMicrosoftIdentityOptions(Microsoft.Identity.Web.MicrosoftIdentityOptions! microsoftIdentityOptions, Microsoft.Identity.Web.MergedOptions! mergedOptions) -> void +static Microsoft.Identity.Web.MergedOptionsLogging.AuthorityIgnored(Microsoft.Extensions.Logging.ILogger! logger, string! authority, string! instance, string! tenantId) -> void static Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder.SetIdentityModelLogger(System.IServiceProvider! serviceProvider) -> void static Microsoft.Identity.Web.MsAuth10AtPop.WithAtPop(this Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! builder, string! popPublicKey, string! jwkClaim) -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! static Microsoft.Identity.Web.MsAuth10AtPop.WithAtPop(this Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! builder, System.Security.Cryptography.X509Certificates.X509Certificate2! clientCertificate, string! popPublicKey, string! jwkClaim, string! clientId, bool sendX5C) -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! @@ -334,12 +357,16 @@ static Microsoft.Identity.Web.TokenAcquisition.GetCacheKeyForManagedId(Microsoft static Microsoft.Identity.Web.TokenAcquisition.Logger.TokenAcquisitionError(Microsoft.Extensions.Logging.ILogger! logger, string! msalErrorMessage, System.Exception? ex) -> void static Microsoft.Identity.Web.TokenAcquisition.Logger.TokenAcquisitionMsalAuthenticationResultTime(Microsoft.Extensions.Logging.ILogger! logger, long durationTotalInMs, long durationInHttpInMs, long durationInCacheInMs, string! tokenSource, string! correlationId, string! cacheRefreshReason, System.Exception? ex) -> void static Microsoft.Identity.Web.TokenAcquisition.MergeExtraQueryParameters(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Web.TokenAcquisitionOptions! tokenAcquisitionOptions) -> System.Collections.Generic.Dictionary? +static Microsoft.Identity.Web.TokenAcquisition.MergeExtraQueryParameters(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Web.TokenAcquisitionOptions? tokenAcquisitionOptions) -> System.Collections.Generic.Dictionary? static Microsoft.Identity.Web.TokenAcquisition.ResolveTenant(string? tenant, Microsoft.Identity.Web.MergedOptions! mergedOptions) -> string? static Microsoft.Identity.Web.Util.Base64UrlHelpers.Decode(string! arg) -> string? static Microsoft.Identity.Web.Util.Base64UrlHelpers.DecodeBytes(string? str) -> byte[]? static Microsoft.Identity.Web.Util.Base64UrlHelpers.Encode(byte[]? inArray) -> string? static Microsoft.Identity.Web.Util.Base64UrlHelpers.Encode(string? arg) -> string? static Microsoft.Identity.Web.Util.Base64UrlHelpers.EncodeString(string? str) -> string? +static readonly Microsoft.Identity.Web.Constants.s_certificateRelatedErrorCodes -> System.Collections.Generic.HashSet! +static readonly Microsoft.Identity.Web.Constants.s_nonRetryableConfigErrorCodes -> System.Collections.Generic.HashSet! +static readonly Microsoft.Identity.Web.LoggingEventId.AuthorityIgnored -> Microsoft.Extensions.Logging.EventId static readonly Microsoft.Identity.Web.LoggingEventId.CredentialLoadAttempt -> Microsoft.Extensions.Logging.EventId static readonly Microsoft.Identity.Web.LoggingEventId.CredentialLoadAttemptFailed -> Microsoft.Extensions.Logging.EventId static readonly Microsoft.Identity.Web.LoggingEventId.NotUsingManagedIdentity -> Microsoft.Extensions.Logging.EventId diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/InternalAPI.Unshipped.txt index 62d5c481d..7dc5c5811 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/InternalAPI.Unshipped.txt @@ -1,2 +1 @@ #nullable enable -static Microsoft.Identity.Web.TokenAcquisition.MergeExtraQueryParameters(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Web.TokenAcquisitionOptions? tokenAcquisitionOptions) -> System.Collections.Generic.Dictionary? diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/PublicAPI.Shipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/PublicAPI.Shipped.txt index bcd69bef3..fed3386fe 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/PublicAPI.Shipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/PublicAPI.Shipped.txt @@ -32,7 +32,11 @@ const Microsoft.Identity.Web.Constants.ReturnUrl = "ReturnUrl" -> string! const Microsoft.Identity.Web.Constants.Scope = "scope" -> string! const Microsoft.Identity.Web.Constants.SpaAuthCode = "SpaAuthCode" -> string! Microsoft.Identity.Web.ApplicationBuilderExtensions +Microsoft.Identity.Web.BeforeOnBehalfOfInitialized +Microsoft.Identity.Web.BeforeOnBehalfOfInitializedAsync Microsoft.Identity.Web.BeforeTokenAcquisitionForApp +Microsoft.Identity.Web.BeforeTokenAcquisitionForOnBehalfOf +Microsoft.Identity.Web.BeforeTokenAcquisitionForOnBehalfOfAsync Microsoft.Identity.Web.BeforeTokenAcquisitionForTestUser Microsoft.Identity.Web.BeforeTokenAcquisitionForTestUserAsync Microsoft.Identity.Web.ClaimConstants @@ -55,6 +59,7 @@ Microsoft.Identity.Web.Experimental.ICertificatesObserver Microsoft.Identity.Web.Experimental.ICertificatesObserver.OnClientCertificateChanged(Microsoft.Identity.Web.Experimental.CertificateChangeEventArg! e) -> void Microsoft.Identity.Web.Extensibility.BaseAuthorizationHeaderProvider Microsoft.Identity.Web.Extensibility.BaseAuthorizationHeaderProvider.BaseAuthorizationHeaderProvider(System.IServiceProvider! serviceProvider) -> void +Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions Microsoft.Identity.Web.IAuthenticationSchemeInformationProvider Microsoft.Identity.Web.Internal.WebApiBuilders Microsoft.Identity.Web.ITokenAcquisition @@ -73,11 +78,21 @@ Microsoft.Identity.Web.ITokenAcquisition.ReplyForbiddenWithWwwAuthenticateHeader Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder.AddDistributedTokenCaches() -> Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder! Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder.AddInMemoryTokenCaches(System.Action? configureOptions = null, System.Action? memoryCacheOptions = null) -> Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder! +Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException +Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException.MicrosoftIdentityAuthenticationException(string! message, System.Exception! innerException) -> void +Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException.MicrosoftIdentityAuthenticationException(string! message) -> void Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder.ConfigurationSection.get -> Microsoft.Extensions.Configuration.IConfigurationSection? Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder.ConfigurationSection.set -> void Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder.MicrosoftIdentityBaseAuthenticationBuilder(Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfigurationSection? configurationSection = null) -> void Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder.Services.get -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions +Microsoft.Identity.Web.MicrosoftIdentityMessageHandler +Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.MicrosoftIdentityMessageHandler(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! headerProvider, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? defaultOptions = null, Microsoft.Extensions.Logging.ILogger? logger = null) -> void +Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions +Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.MicrosoftIdentityMessageHandlerOptions() -> void +Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.get -> System.Collections.Generic.IList! +Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.set -> void Microsoft.Identity.Web.MicrosoftIdentityOptions Microsoft.Identity.Web.MicrosoftIdentityOptions.AllowWebApiToBeAuthorizedByACL.get -> bool Microsoft.Identity.Web.MicrosoftIdentityOptions.AllowWebApiToBeAuthorizedByACL.set -> void @@ -127,6 +142,17 @@ Microsoft.Identity.Web.MicrosoftIdentityWebChallengeUserException.Scopes.get -> Microsoft.Identity.Web.MicrosoftIdentityWebChallengeUserException.Scopes.set -> void Microsoft.Identity.Web.MicrosoftIdentityWebChallengeUserException.Userflow.get -> string? Microsoft.Identity.Web.MicrosoftIdentityWebChallengeUserException.Userflow.set -> void +Microsoft.Identity.Web.MsalMtlsHttpClientFactory +Microsoft.Identity.Web.MsalMtlsHttpClientFactory.Dispose() -> void +Microsoft.Identity.Web.MsalMtlsHttpClientFactory.GetHttpClient() -> System.Net.Http.HttpClient! +Microsoft.Identity.Web.MsalMtlsHttpClientFactory.GetHttpClient(System.Security.Cryptography.X509Certificates.X509Certificate2! x509Certificate2) -> System.Net.Http.HttpClient! +Microsoft.Identity.Web.MsalMtlsHttpClientFactory.MsalMtlsHttpClientFactory(System.Net.Http.IHttpClientFactory! httpClientFactory) -> void +Microsoft.Identity.Web.OnBehalfOfEventArgs +Microsoft.Identity.Web.OnBehalfOfEventArgs.OnBehalfOfEventArgs() -> void +Microsoft.Identity.Web.OnBehalfOfEventArgs.User.get -> System.Security.Claims.ClaimsPrincipal? +Microsoft.Identity.Web.OnBehalfOfEventArgs.User.set -> void +Microsoft.Identity.Web.OnBehalfOfEventArgs.UserAssertionToken.get -> string? +Microsoft.Identity.Web.OnBehalfOfEventArgs.UserAssertionToken.set -> void Microsoft.Identity.Web.PrincipalExtensionsForSecurityTokens Microsoft.Identity.Web.ServiceCollectionExtensions Microsoft.Identity.Web.TestOnly.TokenAcquirerFactoryTesting @@ -143,7 +169,11 @@ Microsoft.Identity.Web.TokenAcquirerFactory.ServiceProvider.set -> void Microsoft.Identity.Web.TokenAcquirerFactory.Services.get -> Microsoft.Extensions.DependencyInjection.ServiceCollection! Microsoft.Identity.Web.TokenAcquirerFactory.TokenAcquirerFactory() -> void Microsoft.Identity.Web.TokenAcquisitionExtensionOptions +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeOnBehalfOfInitialized -> Microsoft.Identity.Web.BeforeOnBehalfOfInitialized? +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeOnBehalfOfInitializedAsync -> Microsoft.Identity.Web.BeforeOnBehalfOfInitializedAsync? Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeTokenAcquisitionForApp -> Microsoft.Identity.Web.BeforeTokenAcquisitionForApp? +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeTokenAcquisitionForOnBehalfOf -> Microsoft.Identity.Web.BeforeTokenAcquisitionForOnBehalfOf? +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeTokenAcquisitionForOnBehalfOfAsync -> Microsoft.Identity.Web.BeforeTokenAcquisitionForOnBehalfOfAsync? Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeTokenAcquisitionForTestUser -> Microsoft.Identity.Web.BeforeTokenAcquisitionForTestUser? Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeTokenAcquisitionForTestUserAsync -> Microsoft.Identity.Web.BeforeTokenAcquisitionForTestUserAsync? Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.TokenAcquisitionExtensionOptions() -> void @@ -154,8 +184,16 @@ Microsoft.Identity.Web.TokenAcquisitionOptions.Clone() -> Microsoft.Identity.Web Microsoft.Identity.Web.TokenAcquisitionOptions.PoPConfiguration.get -> Microsoft.Identity.Client.AppConfig.PoPAuthenticationConfiguration? Microsoft.Identity.Web.TokenAcquisitionOptions.PoPConfiguration.set -> void Microsoft.Identity.Web.TokenAcquisitionOptions.TokenAcquisitionOptions() -> void +override Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.SendAsync(System.Net.Http.HttpRequestMessage! request, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! static Microsoft.Identity.Web.ApplicationBuilderExtensions.UseTokenAcquirerFactory(this Microsoft.AspNetCore.Builder.IApplicationBuilder! applicationBuilder) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! +static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.GetAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request) -> Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? +static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! options) -> System.Net.Http.HttpRequestMessage! +static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, System.Action! configure) -> System.Net.Http.HttpRequestMessage! static Microsoft.Identity.Web.Internal.WebApiBuilders.EnableTokenAcquisition(System.Action! configureConfidentialClientApplicationOptions, string! authenticationScheme, Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfigurationSection? configuration) -> Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder, Microsoft.Extensions.Configuration.IConfiguration! configuration, string! sectionName) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! options) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! static Microsoft.Identity.Web.PrincipalExtensionsForSecurityTokens.GetBootstrapToken(this System.Security.Principal.IPrincipal! claimsPrincipal) -> Microsoft.IdentityModel.Tokens.SecurityToken? static Microsoft.Identity.Web.ServiceCollectionExtensions.AddTokenAcquisition(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, bool isTokenAcquisitionSingleton = false) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.Identity.Web.TestOnly.TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest() -> void @@ -163,7 +201,11 @@ static Microsoft.Identity.Web.TokenAcquirerExtensions.GetFicTokenAsync(this Micr static Microsoft.Identity.Web.TokenAcquirerExtensions.WithClientAssertion(this Microsoft.Identity.Abstractions.AcquireTokenOptions! options, string! clientAssertion) -> Microsoft.Identity.Abstractions.AcquireTokenOptions! static Microsoft.Identity.Web.TokenAcquirerFactory.GetDefaultInstance(string! configSection = "AzureAd") -> Microsoft.Identity.Web.TokenAcquirerFactory! static Microsoft.Identity.Web.TokenAcquirerFactory.GetDefaultInstance(string! configSection = "AzureAd") -> T! +virtual Microsoft.Identity.Web.BeforeOnBehalfOfInitialized.Invoke(Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> void +virtual Microsoft.Identity.Web.BeforeOnBehalfOfInitializedAsync.Invoke(Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> System.Threading.Tasks.Task! virtual Microsoft.Identity.Web.BeforeTokenAcquisitionForApp.Invoke(Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions) -> void +virtual Microsoft.Identity.Web.BeforeTokenAcquisitionForOnBehalfOf.Invoke(Microsoft.Identity.Client.AcquireTokenOnBehalfOfParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> void +virtual Microsoft.Identity.Web.BeforeTokenAcquisitionForOnBehalfOfAsync.Invoke(Microsoft.Identity.Client.AcquireTokenOnBehalfOfParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> System.Threading.Tasks.Task! virtual Microsoft.Identity.Web.BeforeTokenAcquisitionForTestUser.Invoke(Microsoft.Identity.Client.AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> void virtual Microsoft.Identity.Web.BeforeTokenAcquisitionForTestUserAsync.Invoke(Microsoft.Identity.Client.AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> System.Threading.Tasks.Task! virtual Microsoft.Identity.Web.Extensibility.BaseAuthorizationHeaderProvider.CreateAuthorizationHeaderAsync(System.Collections.Generic.IEnumerable! scopes, Microsoft.Identity.Abstractions.AuthorizationHeaderProviderOptions? authorizationHeaderProviderOptions = null, System.Security.Claims.ClaimsPrincipal? claimsPrincipal = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/PublicAPI.Unshipped.txt index 13496576f..7dc5c5811 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/net9.0/PublicAPI.Unshipped.txt @@ -1,15 +1 @@ #nullable enable -Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions -Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException -Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException.MicrosoftIdentityAuthenticationException(string! message) -> void -Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException.MicrosoftIdentityAuthenticationException(string! message, System.Exception! innerException) -> void -Microsoft.Identity.Web.MicrosoftIdentityMessageHandler -Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.MicrosoftIdentityMessageHandler(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! headerProvider, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? defaultOptions = null, Microsoft.Extensions.Logging.ILogger? logger = null) -> void -Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions -Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.MicrosoftIdentityMessageHandlerOptions() -> void -Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.get -> System.Collections.Generic.IList! -Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.set -> void -override Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.SendAsync(System.Net.Http.HttpRequestMessage! request, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.GetAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request) -> Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? -static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! options) -> System.Net.Http.HttpRequestMessage! -static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, System.Action! configure) -> System.Net.Http.HttpRequestMessage! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/InternalAPI.Shipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/InternalAPI.Shipped.txt index 3f80e4a15..59e23a3ce 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/InternalAPI.Shipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/InternalAPI.Shipped.txt @@ -3,13 +3,16 @@ const Microsoft.Identity.Web.Constants.AgentIdentityKey = "IDWEB_AGENT_IDENTITY" const Microsoft.Identity.Web.Constants.Aliases = "aliases" -> string! const Microsoft.Identity.Web.Constants.ApiVersion = "api-version" -> string! const Microsoft.Identity.Web.Constants.ApplicationJson = "application/json" -> string! +const Microsoft.Identity.Web.Constants.ApplicationNotFound = "AADSTS700016" -> string! const Microsoft.Identity.Web.Constants.Authorization = "Authorization" -> string! const Microsoft.Identity.Web.Constants.AzureADIssuerMetadataUrl = "https://login.microsoftonline.com/common/discovery/instance?authorization_endpoint=https://login.microsoftonline.com/common/oauth2/v2.0/authorize&api-version=1.1" -> string! const Microsoft.Identity.Web.Constants.BlazorChallengeUri = "MicrosoftIdentity/Account/Challenge?redirectUri=" -> string! const Microsoft.Identity.Web.Constants.CertificateHasBeenRevoked = "AADSTS7000214" -> string! const Microsoft.Identity.Web.Constants.CertificateIsOutsideValidityWindow = "AADSTS1000502" -> string! +const Microsoft.Identity.Web.Constants.CertificateWasRevoked = "AADSTS7000277" -> string! const Microsoft.Identity.Web.Constants.CiamAuthoritySuffix = ".ciamlogin.com" -> string! const Microsoft.Identity.Web.Constants.ClientAssertion = "IDWEB_CLIENT_ASSERTION" -> string! +const Microsoft.Identity.Web.Constants.ClientAssertionContainsInvalidSignature = "AADSTS7000274" -> string! const Microsoft.Identity.Web.Constants.ClientInfo = "client_info" -> string! const Microsoft.Identity.Web.Constants.Common = "common" -> string! const Microsoft.Identity.Web.Constants.Consent = "consent" -> string! @@ -23,6 +26,7 @@ const Microsoft.Identity.Web.Constants.FmiPathForClientAssertion = "IDWEB_FMI_PA const Microsoft.Identity.Web.Constants.GraphBaseUrlV1 = "https://graph.microsoft.com/v1.0" -> string! const Microsoft.Identity.Web.Constants.IDWebSku = "IDWeb." -> string! const Microsoft.Identity.Web.Constants.InvalidClient = "invalid_client" -> string! +const Microsoft.Identity.Web.Constants.InvalidClientSecret = "AADSTS7000215" -> string! const Microsoft.Identity.Web.Constants.InvalidKeyError = "AADSTS700027" -> string! const Microsoft.Identity.Web.Constants.ISessionStore = "ISessionStore" -> string! const Microsoft.Identity.Web.Constants.JwtSecurityTokenUsedToCallWebApi = "JwtSecurityTokenUsedToCallWebAPI" -> string! @@ -89,9 +93,11 @@ const Microsoft.Identity.Web.IDWebErrorMessage.InvalidSubAssertion = "IDW10505: const Microsoft.Identity.Web.IDWebErrorMessage.IssuerDoesNotMatchValidIssuers = "IDW10303: Issuer: '{0}', does not match any of the valid issuers provided for this application. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.IssuerMetadataUrlIsRequired = "IDW10301: Azure AD Issuer metadata address URL is required. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.MicrosoftIdentityWebChallengeUserException = "IDW10502: An MsalUiRequiredException was thrown due to a challenge for the user. See https://aka.ms/ms-id-web/ca_incremental-consent. " -> string! +const Microsoft.Identity.Web.IDWebErrorMessage.MissingIdentityConfiguration = "IDW10708: The identity configuration is incomplete. Provide either 'Instance' and 'TenantId', or 'Authority', or enable 'ManagedIdentity' in the configuration. Check your configuration keys for typos (e.g., trailing spaces). See https://aka.ms/ms-id-web/configuration. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.MissingRequiredScopesForAuthorizationFilter = "IDW10108: RequiredScope Attribute does not contain a value. The scopes need to be set on the controller, the page or action. See https://aka.ms/ms-id-web/required-scope-attribute. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.MissingRoles = "IDW10202: The 'roles' or 'role' claim does not contain roles '{0}' or was not found. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.MissingScopes = "IDW10203: The 'scope' or 'scp' claim does not contain scopes '{0}' or was not found. " -> string! +const Microsoft.Identity.Web.IDWebErrorMessage.MissingTokenBindingCertificate = "IDW10115: A signing certificate, which is required for token binding, is missing in loaded credentials." -> string! const Microsoft.Identity.Web.IDWebErrorMessage.NeitherScopeOrRolesClaimFoundInToken = "IDW10201: Neither scope nor roles claim was found in the bearer token. Authentication scheme used: '{0}'. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.NoMetadataDocumentRetrieverProvided = "IDW10302: No metadata document retriever is provided. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.NoScopesProvided = "IDW10103: No scopes provided in scopes... " -> string! @@ -102,6 +108,7 @@ const Microsoft.Identity.Web.IDWebErrorMessage.ScopeKeySectionIsProvidedButNotPr const Microsoft.Identity.Web.IDWebErrorMessage.ScopesNotConfiguredInConfigurationOrViaDelegate = "IDW10107: Scopes need to be passed-in either by configuration or by the delegate overriding it. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.ScopesRequiredToCallMicrosoftGraph = "IDW10208: You need to either pass-in scopes to AddMicrosoftGraph, in the appsettings.json file, or with .WithScopes() on the Graph queries. See https://aka.ms/ms-id-web/microsoftGraph. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.TenantIdClaimNotPresentInToken = "IDW10401: Neither `tid` nor `tenantId` claim is present in the token obtained from Microsoft identity platform. " -> string! +const Microsoft.Identity.Web.IDWebErrorMessage.TokenBindingRequiresEnabledAppTokenAcquisition = "IDW10116: Token binding requires enabled app token acquisition." -> string! const Microsoft.Identity.Web.IDWebErrorMessage.TokenIsNotJwtToken = "IDW10403: Token is not a JWT token. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.UnauthenticatedUser = "IDW10204: The user is unauthenticated. The HttpContext does not contain any claims. " -> string! const Microsoft.Identity.Web.IDWebErrorMessage.WithClientCredentialsIsObsolete = "Use WithClientCredentialsAsync instead." -> string! @@ -157,6 +164,7 @@ Microsoft.Identity.Web.DefaultAuthorizationHeaderProvider Microsoft.Identity.Web.DefaultAuthorizationHeaderProvider.CreateAuthorizationHeaderAsync(System.Collections.Generic.IEnumerable! scopes, Microsoft.Identity.Abstractions.AuthorizationHeaderProviderOptions? downstreamApiOptions = null, System.Security.Claims.ClaimsPrincipal? claimsPrincipal = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DefaultAuthorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync(string! scopes, Microsoft.Identity.Abstractions.AuthorizationHeaderProviderOptions? downstreamApiOptions = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.DefaultAuthorizationHeaderProvider.CreateAuthorizationHeaderForUserAsync(System.Collections.Generic.IEnumerable! scopes, Microsoft.Identity.Abstractions.AuthorizationHeaderProviderOptions? downstreamApiOptions = null, System.Security.Claims.ClaimsPrincipal? claimsPrincipal = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.DefaultAuthorizationHeaderProvider.CreateBoundAuthorizationHeaderAsync(Microsoft.Identity.Abstractions.DownstreamApiOptions! downstreamApiOptions, System.Security.Claims.ClaimsPrincipal? claimsPrincipal = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task>! Microsoft.Identity.Web.DefaultAuthorizationHeaderProvider.DefaultAuthorizationHeaderProvider(Microsoft.Identity.Web.ITokenAcquisition! tokenAcquisition) -> void Microsoft.Identity.Web.DefaultTokenAcquirerFactoryImplementation Microsoft.Identity.Web.DefaultTokenAcquirerFactoryImplementation.DefaultTokenAcquirerFactoryImplementation(System.IServiceProvider! serviceProvider) -> void @@ -220,6 +228,10 @@ Microsoft.Identity.Web.MergedOptions.EnablePiiLogging.get -> bool Microsoft.Identity.Web.MergedOptions.EnablePiiLogging.set -> void Microsoft.Identity.Web.MergedOptions.IsDefaultPlatformLoggingEnabled.get -> bool Microsoft.Identity.Web.MergedOptions.IsDefaultPlatformLoggingEnabled.set -> void +Microsoft.Identity.Web.MergedOptions.IsTokenBinding.get -> bool +Microsoft.Identity.Web.MergedOptions.IsTokenBinding.set -> void +Microsoft.Identity.Web.MergedOptions.Logger.get -> Microsoft.Extensions.Logging.ILogger? +Microsoft.Identity.Web.MergedOptions.Logger.set -> void Microsoft.Identity.Web.MergedOptions.LogLevel.get -> Microsoft.Identity.Client.LogLevel Microsoft.Identity.Web.MergedOptions.LogLevel.set -> void Microsoft.Identity.Web.MergedOptions.MergedOptions() -> void @@ -232,9 +244,11 @@ Microsoft.Identity.Web.MergedOptions.PreserveAuthority.get -> bool Microsoft.Identity.Web.MergedOptions.PreserveAuthority.set -> void Microsoft.Identity.Web.MergedOptions.RedirectUri.get -> string? Microsoft.Identity.Web.MergedOptions.RedirectUri.set -> void +Microsoft.Identity.Web.MergedOptionsLogging Microsoft.Identity.Web.MergedOptionsStore Microsoft.Identity.Web.MergedOptionsStore.Get(string! name) -> Microsoft.Identity.Web.MergedOptions! Microsoft.Identity.Web.MergedOptionsStore.MergedOptionsStore() -> void +Microsoft.Identity.Web.MergedOptionsStore.MergedOptionsStore(System.IServiceProvider! serviceProvider) -> void Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder(Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfigurationSection? configurationSection = null) -> void Microsoft.Identity.Web.MicrosoftIdentityApplicationOptionsMerger Microsoft.Identity.Web.MicrosoftIdentityApplicationOptionsMerger.MicrosoftIdentityApplicationOptionsMerger(Microsoft.Identity.Web.IMergedOptionsStore! mergedOptions) -> void @@ -261,12 +275,15 @@ Microsoft.Identity.Web.TokenAcquisition.GetAccessTokenForUserAsync(System.Collec Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForAppAsync(string! scope, string? authenticationScheme = null, string? tenant = null, Microsoft.Identity.Web.TokenAcquisitionOptions? tokenAcquisitionOptions = null) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForUserAsync(System.Collections.Generic.IEnumerable! scopes, string? authenticationScheme = null, string? tenantId = null, string? userFlow = null, System.Security.Claims.ClaimsPrincipal? user = null, Microsoft.Identity.Web.TokenAcquisitionOptions? tokenAcquisitionOptions = null) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.TokenAcquisition.GetEffectiveAuthenticationScheme(string? authenticationScheme) -> string! -Microsoft.Identity.Web.TokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(Microsoft.Identity.Web.MergedOptions! mergedOptions) -> System.Threading.Tasks.Task! +Microsoft.Identity.Web.TokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(Microsoft.Identity.Web.MergedOptions! mergedOptions, bool isTokenBinding) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.TokenAcquisition.GetOrBuildManagedIdentityApplicationAsync(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Abstractions.ManagedIdentityOptions! managedIdentityOptions) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.TokenAcquisition.Logger Microsoft.Identity.Web.TokenAcquisition.RemoveAccountAsync(System.Security.Claims.ClaimsPrincipal! user, string? authenticationScheme = null) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.TokenAcquisition.TokenAcquisition(Microsoft.Identity.Web.TokenCacheProviders.IMsalTokenCacheProvider! tokenCacheProvider, Microsoft.Identity.Web.ITokenAcquisitionHost! tokenAcquisitionHost, System.Net.Http.IHttpClientFactory! httpClientFactory, Microsoft.Extensions.Logging.ILogger! logger, System.IServiceProvider! serviceProvider, Microsoft.Identity.Abstractions.ICredentialsLoader! credentialsLoader) -> void +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeOnBehalfOfInitializedAsync(Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForApp(Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions) -> void +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForOnBehalfOf(Microsoft.Identity.Client.AcquireTokenOnBehalfOfParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> void +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForOnBehalfOfAsync(Microsoft.Identity.Client.AcquireTokenOnBehalfOfParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForTestUser(Microsoft.Identity.Client.AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> void Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.InvokeOnBeforeTokenAcquisitionForTestUserAsync(Microsoft.Identity.Client.AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> System.Threading.Tasks.Task! Microsoft.Identity.Web.Util.Base64UrlHelpers @@ -288,19 +305,22 @@ static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logg static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.FailedToLoadCredentials(Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Abstractions.CredentialDescription! certificateDescription) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.NotUsingManagedIdentity(Microsoft.Extensions.Logging.ILogger! logger, string! message) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingCertThumbprint(Microsoft.Extensions.Logging.ILogger! logger, string! certThumbprint) -> void +static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingCertThumbprint(Microsoft.Extensions.Logging.ILogger! logger, string? certThumbprint) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingManagedIdentity(Microsoft.Extensions.Logging.ILogger! logger) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingPodIdentityFile(Microsoft.Extensions.Logging.ILogger! logger, string! signedAssertionFileDiskPath) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingSignedAssertionFromCustomProvider(Microsoft.Extensions.Logging.ILogger! logger, string! signedAssertionUri) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.Logger.UsingSignedAssertionFromVault(Microsoft.Extensions.Logging.ILogger! logger, string! signedAssertionUri) -> void static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.WithClientCredentials(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder! builder, System.Collections.Generic.IEnumerable! clientCredentials, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Abstractions.ICredentialsLoader! credentialsLoader, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters! credentialSourceLoaderParameters) -> Microsoft.Identity.Client.ConfidentialClientApplicationBuilder! -static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.WithClientCredentialsAsync(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder! builder, System.Collections.Generic.IEnumerable! clientCredentials, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Abstractions.ICredentialsLoader! credentialsLoader, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters) -> System.Threading.Tasks.Task! +static Microsoft.Identity.Web.ConfidentialClientApplicationBuilderExtension.WithClientCredentialsAsync(this Microsoft.Identity.Client.ConfidentialClientApplicationBuilder! builder, System.Collections.Generic.IEnumerable! clientCredentials, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Identity.Abstractions.ICredentialsLoader! credentialsLoader, Microsoft.Identity.Abstractions.CredentialSourceLoaderParameters? credentialSourceLoaderParameters, bool isTokenBinding) -> System.Threading.Tasks.Task! static Microsoft.Identity.Web.DefaultTokenAcquirerFactoryImplementation.GetKey(string? authority, string? clientId, string? region) -> string! static Microsoft.Identity.Web.IdHelper.CreateTelemetryInfo() -> string! +static Microsoft.Identity.Web.MergedOptions.ParseAuthorityIfNecessary(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Extensions.Logging.ILogger? logger = null) -> void static Microsoft.Identity.Web.MergedOptions.ParseAuthorityIfNecessary(Microsoft.Identity.Web.MergedOptions! mergedOptions) -> void static Microsoft.Identity.Web.MergedOptions.UpdateConfidentialClientApplicationOptionsFromMergedOptions(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Client.ConfidentialClientApplicationOptions! confidentialClientApplicationOptions) -> void static Microsoft.Identity.Web.MergedOptions.UpdateMergedOptionsFromConfidentialClientApplicationOptions(Microsoft.Identity.Client.ConfidentialClientApplicationOptions! confidentialClientApplicationOptions, Microsoft.Identity.Web.MergedOptions! mergedOptions) -> void static Microsoft.Identity.Web.MergedOptions.UpdateMergedOptionsFromMicrosoftIdentityApplicationOptions(Microsoft.Identity.Abstractions.MicrosoftIdentityApplicationOptions! microsoftIdentityApplicationOptions, Microsoft.Identity.Web.MergedOptions! mergedOptions) -> void static Microsoft.Identity.Web.MergedOptions.UpdateMergedOptionsFromMicrosoftIdentityOptions(Microsoft.Identity.Web.MicrosoftIdentityOptions! microsoftIdentityOptions, Microsoft.Identity.Web.MergedOptions! mergedOptions) -> void +static Microsoft.Identity.Web.MergedOptionsLogging.AuthorityIgnored(Microsoft.Extensions.Logging.ILogger! logger, string! authority, string! instance, string! tenantId) -> void static Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder.SetIdentityModelLogger(System.IServiceProvider! serviceProvider) -> void static Microsoft.Identity.Web.MsAuth10AtPop.WithAtPop(this Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! builder, string! popPublicKey, string! jwkClaim) -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! static Microsoft.Identity.Web.MsAuth10AtPop.WithAtPop(this Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! builder, System.Security.Cryptography.X509Certificates.X509Certificate2! clientCertificate, string! popPublicKey, string! jwkClaim, string! clientId, bool sendX5C) -> Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! @@ -311,12 +331,16 @@ static Microsoft.Identity.Web.TokenAcquisition.GetCacheKeyForManagedId(Microsoft static Microsoft.Identity.Web.TokenAcquisition.Logger.TokenAcquisitionError(Microsoft.Extensions.Logging.ILogger! logger, string! msalErrorMessage, System.Exception? ex) -> void static Microsoft.Identity.Web.TokenAcquisition.Logger.TokenAcquisitionMsalAuthenticationResultTime(Microsoft.Extensions.Logging.ILogger! logger, long durationTotalInMs, long durationInHttpInMs, long durationInCacheInMs, string! tokenSource, string! correlationId, string! cacheRefreshReason, System.Exception? ex) -> void static Microsoft.Identity.Web.TokenAcquisition.MergeExtraQueryParameters(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Web.TokenAcquisitionOptions! tokenAcquisitionOptions) -> System.Collections.Generic.Dictionary? +static Microsoft.Identity.Web.TokenAcquisition.MergeExtraQueryParameters(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Web.TokenAcquisitionOptions? tokenAcquisitionOptions) -> System.Collections.Generic.Dictionary? static Microsoft.Identity.Web.TokenAcquisition.ResolveTenant(string? tenant, Microsoft.Identity.Web.MergedOptions! mergedOptions) -> string? static Microsoft.Identity.Web.Util.Base64UrlHelpers.Decode(string! arg) -> string? static Microsoft.Identity.Web.Util.Base64UrlHelpers.DecodeBytes(string? str) -> byte[]? static Microsoft.Identity.Web.Util.Base64UrlHelpers.Encode(byte[]? inArray) -> string? static Microsoft.Identity.Web.Util.Base64UrlHelpers.Encode(string? arg) -> string? static Microsoft.Identity.Web.Util.Base64UrlHelpers.EncodeString(string? str) -> string? +static readonly Microsoft.Identity.Web.Constants.s_certificateRelatedErrorCodes -> System.Collections.Generic.HashSet! +static readonly Microsoft.Identity.Web.Constants.s_nonRetryableConfigErrorCodes -> System.Collections.Generic.HashSet! +static readonly Microsoft.Identity.Web.LoggingEventId.AuthorityIgnored -> Microsoft.Extensions.Logging.EventId static readonly Microsoft.Identity.Web.LoggingEventId.CredentialLoadAttempt -> Microsoft.Extensions.Logging.EventId static readonly Microsoft.Identity.Web.LoggingEventId.CredentialLoadAttemptFailed -> Microsoft.Extensions.Logging.EventId static readonly Microsoft.Identity.Web.LoggingEventId.NotUsingManagedIdentity -> Microsoft.Extensions.Logging.EventId diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt index 62d5c481d..7dc5c5811 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/InternalAPI.Unshipped.txt @@ -1,2 +1 @@ #nullable enable -static Microsoft.Identity.Web.TokenAcquisition.MergeExtraQueryParameters(Microsoft.Identity.Web.MergedOptions! mergedOptions, Microsoft.Identity.Web.TokenAcquisitionOptions? tokenAcquisitionOptions) -> System.Collections.Generic.Dictionary? diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt index 8b941c47f..401ec8796 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt @@ -31,7 +31,11 @@ const Microsoft.Identity.Web.Constants.ResetPasswordPath = "/MicrosoftIdentity/A const Microsoft.Identity.Web.Constants.ReturnUrl = "ReturnUrl" -> string! const Microsoft.Identity.Web.Constants.Scope = "scope" -> string! const Microsoft.Identity.Web.Constants.SpaAuthCode = "SpaAuthCode" -> string! +Microsoft.Identity.Web.BeforeOnBehalfOfInitialized +Microsoft.Identity.Web.BeforeOnBehalfOfInitializedAsync Microsoft.Identity.Web.BeforeTokenAcquisitionForApp +Microsoft.Identity.Web.BeforeTokenAcquisitionForOnBehalfOf +Microsoft.Identity.Web.BeforeTokenAcquisitionForOnBehalfOfAsync Microsoft.Identity.Web.BeforeTokenAcquisitionForTestUser Microsoft.Identity.Web.BeforeTokenAcquisitionForTestUserAsync Microsoft.Identity.Web.ClaimConstants @@ -54,6 +58,7 @@ Microsoft.Identity.Web.Experimental.ICertificatesObserver Microsoft.Identity.Web.Experimental.ICertificatesObserver.OnClientCertificateChanged(Microsoft.Identity.Web.Experimental.CertificateChangeEventArg! e) -> void Microsoft.Identity.Web.Extensibility.BaseAuthorizationHeaderProvider Microsoft.Identity.Web.Extensibility.BaseAuthorizationHeaderProvider.BaseAuthorizationHeaderProvider(System.IServiceProvider! serviceProvider) -> void +Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions Microsoft.Identity.Web.IAuthenticationSchemeInformationProvider Microsoft.Identity.Web.Internal.WebApiBuilders Microsoft.Identity.Web.ITokenAcquisition @@ -65,11 +70,21 @@ Microsoft.Identity.Web.ITokenAcquisition.GetEffectiveAuthenticationScheme(string Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder.AddDistributedTokenCaches() -> Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder! Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder.AddInMemoryTokenCaches(System.Action? configureOptions = null, System.Action? memoryCacheOptions = null) -> Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder! +Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException +Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException.MicrosoftIdentityAuthenticationException(string! message, System.Exception! innerException) -> void +Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException.MicrosoftIdentityAuthenticationException(string! message) -> void Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder.ConfigurationSection.get -> Microsoft.Extensions.Configuration.IConfigurationSection? Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder.ConfigurationSection.set -> void Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder.MicrosoftIdentityBaseAuthenticationBuilder(Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfigurationSection? configurationSection = null) -> void Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder.Services.get -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions +Microsoft.Identity.Web.MicrosoftIdentityMessageHandler +Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.MicrosoftIdentityMessageHandler(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! headerProvider, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? defaultOptions = null, Microsoft.Extensions.Logging.ILogger? logger = null) -> void +Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions +Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.MicrosoftIdentityMessageHandlerOptions() -> void +Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.get -> System.Collections.Generic.IList! +Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.set -> void Microsoft.Identity.Web.MicrosoftIdentityOptions Microsoft.Identity.Web.MicrosoftIdentityOptions.AllowWebApiToBeAuthorizedByACL.get -> bool Microsoft.Identity.Web.MicrosoftIdentityOptions.AllowWebApiToBeAuthorizedByACL.set -> void @@ -119,6 +134,17 @@ Microsoft.Identity.Web.MicrosoftIdentityWebChallengeUserException.Scopes.get -> Microsoft.Identity.Web.MicrosoftIdentityWebChallengeUserException.Scopes.set -> void Microsoft.Identity.Web.MicrosoftIdentityWebChallengeUserException.Userflow.get -> string? Microsoft.Identity.Web.MicrosoftIdentityWebChallengeUserException.Userflow.set -> void +Microsoft.Identity.Web.MsalMtlsHttpClientFactory +Microsoft.Identity.Web.MsalMtlsHttpClientFactory.Dispose() -> void +Microsoft.Identity.Web.MsalMtlsHttpClientFactory.GetHttpClient() -> System.Net.Http.HttpClient! +Microsoft.Identity.Web.MsalMtlsHttpClientFactory.GetHttpClient(System.Security.Cryptography.X509Certificates.X509Certificate2! x509Certificate2) -> System.Net.Http.HttpClient! +Microsoft.Identity.Web.MsalMtlsHttpClientFactory.MsalMtlsHttpClientFactory(System.Net.Http.IHttpClientFactory! httpClientFactory) -> void +Microsoft.Identity.Web.OnBehalfOfEventArgs +Microsoft.Identity.Web.OnBehalfOfEventArgs.OnBehalfOfEventArgs() -> void +Microsoft.Identity.Web.OnBehalfOfEventArgs.User.get -> System.Security.Claims.ClaimsPrincipal? +Microsoft.Identity.Web.OnBehalfOfEventArgs.User.set -> void +Microsoft.Identity.Web.OnBehalfOfEventArgs.UserAssertionToken.get -> string? +Microsoft.Identity.Web.OnBehalfOfEventArgs.UserAssertionToken.set -> void Microsoft.Identity.Web.OpenIdConnectOptions Microsoft.Identity.Web.OpenIdConnectOptions.Authority.get -> string? Microsoft.Identity.Web.OpenIdConnectOptions.Authority.set -> void @@ -143,7 +169,11 @@ Microsoft.Identity.Web.TokenAcquirerFactory.ServiceProvider.set -> void Microsoft.Identity.Web.TokenAcquirerFactory.Services.get -> Microsoft.Extensions.DependencyInjection.ServiceCollection! Microsoft.Identity.Web.TokenAcquirerFactory.TokenAcquirerFactory() -> void Microsoft.Identity.Web.TokenAcquisitionExtensionOptions +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeOnBehalfOfInitialized -> Microsoft.Identity.Web.BeforeOnBehalfOfInitialized? +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeOnBehalfOfInitializedAsync -> Microsoft.Identity.Web.BeforeOnBehalfOfInitializedAsync? Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeTokenAcquisitionForApp -> Microsoft.Identity.Web.BeforeTokenAcquisitionForApp? +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeTokenAcquisitionForOnBehalfOf -> Microsoft.Identity.Web.BeforeTokenAcquisitionForOnBehalfOf? +Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeTokenAcquisitionForOnBehalfOfAsync -> Microsoft.Identity.Web.BeforeTokenAcquisitionForOnBehalfOfAsync? Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeTokenAcquisitionForTestUser -> Microsoft.Identity.Web.BeforeTokenAcquisitionForTestUser? Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.OnBeforeTokenAcquisitionForTestUserAsync -> Microsoft.Identity.Web.BeforeTokenAcquisitionForTestUserAsync? Microsoft.Identity.Web.TokenAcquisitionExtensionOptions.TokenAcquisitionExtensionOptions() -> void @@ -154,7 +184,15 @@ Microsoft.Identity.Web.TokenAcquisitionOptions.Clone() -> Microsoft.Identity.Web Microsoft.Identity.Web.TokenAcquisitionOptions.PoPConfiguration.get -> Microsoft.Identity.Client.AppConfig.PoPAuthenticationConfiguration? Microsoft.Identity.Web.TokenAcquisitionOptions.PoPConfiguration.set -> void Microsoft.Identity.Web.TokenAcquisitionOptions.TokenAcquisitionOptions() -> void +override Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.SendAsync(System.Net.Http.HttpRequestMessage! request, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.GetAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request) -> Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? +static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! options) -> System.Net.Http.HttpRequestMessage! +static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, System.Action! configure) -> System.Net.Http.HttpRequestMessage! static Microsoft.Identity.Web.Internal.WebApiBuilders.EnableTokenAcquisition(System.Action! configureConfidentialClientApplicationOptions, string! authenticationScheme, Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfigurationSection? configuration) -> Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder, Microsoft.Extensions.Configuration.IConfiguration! configuration, string! sectionName) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! options) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityHttpClientBuilderExtensions.AddMicrosoftIdentityMessageHandler(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! builder) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! static Microsoft.Identity.Web.PrincipalExtensionsForSecurityTokens.GetBootstrapToken(this System.Security.Principal.IPrincipal! claimsPrincipal) -> Microsoft.IdentityModel.Tokens.SecurityToken? static Microsoft.Identity.Web.ServiceCollectionExtensions.AddTokenAcquisition(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, bool isTokenAcquisitionSingleton = false) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.Identity.Web.TestOnly.TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest() -> void @@ -162,7 +200,11 @@ static Microsoft.Identity.Web.TokenAcquirerExtensions.GetFicTokenAsync(this Micr static Microsoft.Identity.Web.TokenAcquirerExtensions.WithClientAssertion(this Microsoft.Identity.Abstractions.AcquireTokenOptions! options, string! clientAssertion) -> Microsoft.Identity.Abstractions.AcquireTokenOptions! static Microsoft.Identity.Web.TokenAcquirerFactory.GetDefaultInstance(string! configSection = "AzureAd") -> Microsoft.Identity.Web.TokenAcquirerFactory! static Microsoft.Identity.Web.TokenAcquirerFactory.GetDefaultInstance(string! configSection = "AzureAd") -> T! +virtual Microsoft.Identity.Web.BeforeOnBehalfOfInitialized.Invoke(Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> void +virtual Microsoft.Identity.Web.BeforeOnBehalfOfInitializedAsync.Invoke(Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> System.Threading.Tasks.Task! virtual Microsoft.Identity.Web.BeforeTokenAcquisitionForApp.Invoke(Microsoft.Identity.Client.AcquireTokenForClientParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions) -> void +virtual Microsoft.Identity.Web.BeforeTokenAcquisitionForOnBehalfOf.Invoke(Microsoft.Identity.Client.AcquireTokenOnBehalfOfParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> void +virtual Microsoft.Identity.Web.BeforeTokenAcquisitionForOnBehalfOfAsync.Invoke(Microsoft.Identity.Client.AcquireTokenOnBehalfOfParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, Microsoft.Identity.Web.OnBehalfOfEventArgs! eventArgs) -> System.Threading.Tasks.Task! virtual Microsoft.Identity.Web.BeforeTokenAcquisitionForTestUser.Invoke(Microsoft.Identity.Client.AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> void virtual Microsoft.Identity.Web.BeforeTokenAcquisitionForTestUserAsync.Invoke(Microsoft.Identity.Client.AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder! builder, Microsoft.Identity.Abstractions.AcquireTokenOptions? acquireTokenOptions, System.Security.Claims.ClaimsPrincipal! user) -> System.Threading.Tasks.Task! virtual Microsoft.Identity.Web.Extensibility.BaseAuthorizationHeaderProvider.CreateAuthorizationHeaderAsync(System.Collections.Generic.IEnumerable! scopes, Microsoft.Identity.Abstractions.AuthorizationHeaderProviderOptions? authorizationHeaderProviderOptions = null, System.Security.Claims.ClaimsPrincipal? claimsPrincipal = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index 13496576f..7dc5c5811 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Web.TokenAcquisition/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -1,15 +1 @@ #nullable enable -Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions -Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException -Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException.MicrosoftIdentityAuthenticationException(string! message) -> void -Microsoft.Identity.Web.MicrosoftIdentityAuthenticationException.MicrosoftIdentityAuthenticationException(string! message, System.Exception! innerException) -> void -Microsoft.Identity.Web.MicrosoftIdentityMessageHandler -Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.MicrosoftIdentityMessageHandler(Microsoft.Identity.Abstractions.IAuthorizationHeaderProvider! headerProvider, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? defaultOptions = null, Microsoft.Extensions.Logging.ILogger? logger = null) -> void -Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions -Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.MicrosoftIdentityMessageHandlerOptions() -> void -Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.get -> System.Collections.Generic.IList! -Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions.Scopes.set -> void -override Microsoft.Identity.Web.MicrosoftIdentityMessageHandler.SendAsync(System.Net.Http.HttpRequestMessage! request, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.GetAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request) -> Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions? -static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, Microsoft.Identity.Web.MicrosoftIdentityMessageHandlerOptions! options) -> System.Net.Http.HttpRequestMessage! -static Microsoft.Identity.Web.HttpRequestMessageAuthenticationExtensions.WithAuthenticationOptions(this System.Net.Http.HttpRequestMessage! request, System.Action! configure) -> System.Net.Http.HttpRequestMessage! diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/ServiceCollectionExtensions.cs b/src/Microsoft.Identity.Web.TokenAcquisition/ServiceCollectionExtensions.cs index 71368cc29..49c8a50fc 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/ServiceCollectionExtensions.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/ServiceCollectionExtensions.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using System.Net.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; @@ -63,14 +64,14 @@ public static IServiceCollection AddTokenAcquisition( services.TryAddSingleton, ConfidentialClientApplicationOptionsMerger>(); } - ServiceDescriptor? tokenAcquisitionService = services.FirstOrDefault(s => s.ServiceType == typeof(ITokenAcquisition)); + ServiceDescriptor? tokenAcquisitionService = services.FirstOrDefault(s => s.ServiceType == typeof(ITokenAcquisition)); ServiceDescriptor? tokenAcquisitionInternalService = services.FirstOrDefault(s => s.ServiceType == typeof(ITokenAcquisitionInternal)); ServiceDescriptor? tokenAcquisitionhost = services.FirstOrDefault(s => s.ServiceType == typeof(ITokenAcquisitionHost)); ServiceDescriptor? authenticationHeaderCreator = services.FirstOrDefault(s => s.ServiceType == typeof(IAuthorizationHeaderProvider)); ServiceDescriptor? tokenAcquirerFactory = services.FirstOrDefault(s => s.ServiceType == typeof(ITokenAcquirerFactory)); ServiceDescriptor? authSchemeInfoProvider = services.FirstOrDefault(s => s.ServiceType == typeof(Abstractions.IAuthenticationSchemeInformationProvider)); - - if (tokenAcquisitionService != null && tokenAcquisitionInternalService != null && + + if (tokenAcquisitionService != null && tokenAcquisitionInternalService != null && tokenAcquisitionhost != null && authenticationHeaderCreator != null && authSchemeInfoProvider != null) { if (isTokenAcquisitionSingleton ^ (tokenAcquisitionService.Lifetime == ServiceLifetime.Singleton)) diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquirer.cs b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquirer.cs index 9d0d1ce1e..ab333dc78 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquirer.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquirer.cs @@ -28,15 +28,23 @@ async Task ITokenAcquirer.GetTokenForUserAsync( { string? authenticationScheme = tokenAcquisitionOptions?.AuthenticationOptionsName ?? _authenticationScheme; + var effectiveOptions = GetEffectiveTokenAcquisitionOptions(tokenAcquisitionOptions, authenticationScheme, cancellationToken); var result = await _tokenAcquisition.GetAuthenticationResultForUserAsync( scopes, authenticationScheme, tokenAcquisitionOptions?.Tenant, tokenAcquisitionOptions?.UserFlow, user, - GetEffectiveTokenAcquisitionOptions(tokenAcquisitionOptions, authenticationScheme, cancellationToken) + effectiveOptions ).ConfigureAwait(false); + // Propagate LongRunningWebApiSessionKey (possibly auto-generated) back to the caller + if (tokenAcquisitionOptions is not null && effectiveOptions is not null + && !string.IsNullOrEmpty(effectiveOptions.LongRunningWebApiSessionKey)) + { + tokenAcquisitionOptions.LongRunningWebApiSessionKey = effectiveOptions.LongRunningWebApiSessionKey; + } + return new AcquireTokenResult( result.AccessToken, result.ExpiresOn, @@ -65,7 +73,10 @@ async Task ITokenAcquirer.GetTokenForAppAsync(string scope, result.IdToken, result.Scopes, result.CorrelationId, - result.TokenType); + result.TokenType) + { + BindingCertificate = result.BindingCertificate + }; } private static TokenAcquisitionOptions? GetEffectiveTokenAcquisitionOptions(AcquireTokenOptions? tokenAcquisitionOptions, string? authenticationScheme, CancellationToken cancellationToken) diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquirerFactory.cs b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquirerFactory.cs index 3e476e7ed..a71230ad8 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquirerFactory.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquirerFactory.cs @@ -78,9 +78,8 @@ protected TokenAcquirerFactory() /// [!code-csharp[ConvertType](~/../tests/DevApps/aspnet-mvc/OwinWebApp/App_Start/Startup.Auth.cs?highlight=22)] /// ]]> /// -#if NET6_0_OR_GREATER && !NET8_0_OR_GREATER [RequiresUnreferencedCode("Calls Microsoft.Extensions.Configuration.ConfigurationBinder.Bind(IConfiguration, Object).")] -#endif + [RequiresDynamicCode("Calls Microsoft.Extensions.Configuration.ConfigurationBinder.Bind(IConfiguration, Object).")] static public T GetDefaultInstance(string configSection="AzureAd") where T : TokenAcquirerFactory, new() { T instance; @@ -121,9 +120,8 @@ protected TokenAcquirerFactory() /// [!code-csharp[ConvertType](~/../tests/DevApps/daemon-app/daemon-console-calling-msgraph/Program.cs?highlight=5)] /// ]]> /// -#if NET6_0_OR_GREATER && !NET8_0_OR_GREATER [RequiresUnreferencedCode("Calls Microsoft.Extensions.Configuration.ConfigurationBinder.Bind(IConfiguration, Object).")] -#endif + [RequiresDynamicCode("Calls Microsoft.Extensions.Configuration.ConfigurationBinder.Bind(IConfiguration, Object).")] static public TokenAcquirerFactory GetDefaultInstance(string configSection = "AzureAd") { TokenAcquirerFactory instance; diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.Logger.cs b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.Logger.cs index 3ab368f4f..3d4e7c19d 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.Logger.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.Logger.cs @@ -40,7 +40,10 @@ internal static class Logger public static void TokenAcquisitionError( ILogger logger, string msalErrorMessage, - Exception? ex) => s_tokenAcquisitionError(logger, msalErrorMessage, ex); + Exception? ex) + { + s_tokenAcquisitionError(logger, msalErrorMessage, ex); + } /// /// Logger for handling information specific to MSAL in token acquisition. @@ -61,7 +64,9 @@ public static void TokenAcquisitionMsalAuthenticationResultTime( string tokenSource, string correlationId, string cacheRefreshReason, - Exception? ex) => s_tokenAcquisitionMsalAuthenticationResultTime( + Exception? ex) + { + s_tokenAcquisitionMsalAuthenticationResultTime( logger, durationTotalInMs, durationInHttpInMs, @@ -70,6 +75,7 @@ public static void TokenAcquisitionMsalAuthenticationResultTime( correlationId, cacheRefreshReason, ex); + } } } } diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs index 45b0a4518..a0f4cf5d0 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs @@ -11,6 +11,7 @@ using System.Net.Http; using System.Security.Claims; using System.Security.Cryptography.X509Certificates; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; @@ -52,7 +53,8 @@ class OAuthConstants private readonly ConcurrentDictionary _applicationsByAuthorityClientId = new(); private readonly ConcurrentDictionary _appSemaphores = new(); - private bool _retryClientCertificate; + private const string TokenBindingParameterName = "IsTokenBinding"; + private const int MaxCertificateRetries = 1; protected readonly IMsalHttpClientFactory _httpClientFactory; protected readonly ILogger _logger; protected readonly IServiceProvider _serviceProvider; @@ -104,7 +106,7 @@ public TokenAcquisition( ICredentialsLoader credentialsLoader) { _tokenCacheProvider = tokenCacheProvider; - _httpClientFactory = serviceProvider.GetService() ?? new MsalAspNetCoreHttpClientFactory(httpClientFactory); + _httpClientFactory = serviceProvider.GetService() ?? new MsalMtlsHttpClientFactory(httpClientFactory); _logger = logger; _serviceProvider = serviceProvider; _tokenAcquisitionHost = tokenAcquisitionHost; @@ -122,6 +124,13 @@ public TokenAcquisition( #endif public async Task AddAccountToCacheFromAuthorizationCodeAsync( AuthCodeRedemptionParameters authCodeRedemptionParameters) + { + return await AddAccountToCacheFromAuthorizationCodeInternalAsync(authCodeRedemptionParameters, retryCount: 0).ConfigureAwait(false); + } + + private async Task AddAccountToCacheFromAuthorizationCodeInternalAsync( + AuthCodeRedemptionParameters authCodeRedemptionParameters, + int retryCount) { _ = Throws.IfNull(authCodeRedemptionParameters.Scopes); MergedOptions mergedOptions = _tokenAcquisitionHost.GetOptions(authCodeRedemptionParameters.AuthenticationScheme, out string effectiveAuthenticationScheme); @@ -129,7 +138,7 @@ public async Task AddAccountToCacheFromAuthorizationCodeAsyn IConfidentialClientApplication? application = null; try { - application = await GetOrBuildConfidentialClientApplicationAsync(mergedOptions); + application = await GetOrBuildConfidentialClientApplicationAsync(mergedOptions, isTokenBinding: false); // Do not share the access token with ASP.NET Core otherwise ASP.NET will cache it and will not send the OAuth 2.0 request in // case a further call to AcquireTokenByAuthorizationCodeAsync in the future is required for incremental consent (getting a code requesting more scopes) @@ -188,38 +197,38 @@ public async Task AddAccountToCacheFromAuthorizationCodeAsyn result.CorrelationId, result.TokenType); } - catch (MsalServiceException exMsal) when (IsInvalidClientCertificateOrSignedAssertionError(exMsal)) + catch (MsalServiceException exMsal) when (retryCount < MaxCertificateRetries && IsInvalidClientCertificateOrSignedAssertionError(exMsal)) { + Logger.TokenAcquisitionError( + _logger, + $"Certificate error detected. Retrying with next certificate (attempt {retryCount + 1}/{MaxCertificateRetries}). {exMsal.Message}", + exMsal); + string applicationKey = GetApplicationKey(mergedOptions); NotifyCertificateSelection(mergedOptions, application!, CerticateObserverAction.Deselected, exMsal); DefaultCertificateLoader.ResetCertificates(mergedOptions.ClientCredentials); _applicationsByAuthorityClientId[applicationKey] = null; - // Retry - _retryClientCertificate = true; - return await AddAccountToCacheFromAuthorizationCodeAsync(authCodeRedemptionParameters).ConfigureAwait(false); + // Retry with incremented counter + return await AddAccountToCacheFromAuthorizationCodeInternalAsync(authCodeRedemptionParameters, retryCount + 1).ConfigureAwait(false); } catch (MsalException ex) { Logger.TokenAcquisitionError(_logger, LogMessages.ExceptionOccurredWhenAddingAnAccountToTheCacheFromAuthCode, ex); throw; } - finally - { - _retryClientCertificate = false; - } } - /// /// Allows creation of confidential client applications targeting regional and global authorities /// when supporting managed identities. /// - /// Merged configuration options + /// Merged configuration options. /// Concatenated string of authority, cliend id and azure region private static string GetApplicationKey(MergedOptions mergedOptions) { string credentialId = string.Join("-", mergedOptions.ClientCredentials?.Select(c => c.Id) ?? Enumerable.Empty()); + return DefaultTokenAcquirerFactoryImplementation.GetKey(mergedOptions.Authority, mergedOptions.ClientId, mergedOptions.AzureRegion) + credentialId; } @@ -254,14 +263,32 @@ public async Task GetAuthenticationResultForUserAsync( string? userFlow = null, ClaimsPrincipal? user = null, TokenAcquisitionOptions? tokenAcquisitionOptions = null) + { + return await GetAuthenticationResultForUserInternalAsync( + scopes, + authenticationScheme, + tenantId, + userFlow, + user, + tokenAcquisitionOptions, + retryCount: 0).ConfigureAwait(false); + } + + private async Task GetAuthenticationResultForUserInternalAsync( + IEnumerable scopes, + string? authenticationScheme, + string? tenantId, + string? userFlow, + ClaimsPrincipal? user, + TokenAcquisitionOptions? tokenAcquisitionOptions, + int retryCount) { _ = Throws.IfNull(scopes); MergedOptions mergedOptions = GetMergedOptions(authenticationScheme, tokenAcquisitionOptions); - user ??= await _tokenAcquisitionHost.GetAuthenticatedUserAsync(user).ConfigureAwait(false); - var application = await GetOrBuildConfidentialClientApplicationAsync(mergedOptions); + var application = await GetOrBuildConfidentialClientApplicationAsync(mergedOptions, isTokenBinding: false); if (tokenAcquisitionOptions is not null) { @@ -316,22 +343,27 @@ public async Task GetAuthenticationResultForUserAsync( LogAuthResult(authenticationResult); return authenticationResult; } - catch (MsalServiceException exMsal) when (IsInvalidClientCertificateOrSignedAssertionError(exMsal)) + catch (MsalServiceException exMsal) when (retryCount < MaxCertificateRetries && IsInvalidClientCertificateOrSignedAssertionError(exMsal)) { + Logger.TokenAcquisitionError( + _logger, + $"Certificate error detected. Retrying with next certificate (attempt {retryCount + 1}/{MaxCertificateRetries}). {exMsal.Message}", + exMsal); + string applicationKey = GetApplicationKey(mergedOptions); NotifyCertificateSelection(mergedOptions, application, CerticateObserverAction.Deselected, exMsal); DefaultCertificateLoader.ResetCertificates(mergedOptions.ClientCredentials); _applicationsByAuthorityClientId[applicationKey] = null; - // Retry - _retryClientCertificate = true; - return await GetAuthenticationResultForUserAsync( + // Retry with incremented counter + return await GetAuthenticationResultForUserInternalAsync( scopes, - authenticationScheme: authenticationScheme, - tenantId: tenantId, - userFlow: userFlow, - user: user, - tokenAcquisitionOptions: tokenAcquisitionOptions).ConfigureAwait(false); + authenticationScheme, + tenantId, + userFlow, + user, + tokenAcquisitionOptions, + retryCount + 1).ConfigureAwait(false); } catch (MsalUiRequiredException ex) { @@ -342,10 +374,6 @@ public async Task GetAuthenticationResultForUserAsync( // AuthorizeForScopesAttribute exception filter so that the user can consent, do 2FA, etc ... throw new MicrosoftIdentityWebChallengeUserException(ex, scopes.ToArray(), userFlow); } - finally - { - _retryClientCertificate = false; - } } // This method mutate the user claims to include claims uid and utid to perform the silent flow for subsequent calls. @@ -427,6 +455,8 @@ public async Task GetAuthenticationResultForUserAsync( await addInOptions.InvokeOnBeforeTokenAcquisitionForTestUserAsync(builder, tokenAcquisitionOptions, user!).ConfigureAwait(false); } + builder.WithSendX5C(mergedOptions.SendX5C); + // Pass the token acquisition options to the builder if (tokenAcquisitionOptions != null) { @@ -445,6 +475,11 @@ public async Task GetAuthenticationResultForUserAsync( builder.WithCorrelationId(tokenAcquisitionOptions.CorrelationId.Value); } builder.WithClaims(tokenAcquisitionOptions.Claims); + var clientClaims = GetClientClaimsIfExist(tokenAcquisitionOptions); + if (clientClaims != null) + { + builder.WithExtraClientAssertionClaims(clientClaims); + } if (tokenAcquisitionOptions.PoPConfiguration != null) { builder.WithSignedHttpRequestProofOfPossession(tokenAcquisitionOptions.PoPConfiguration); @@ -533,6 +568,21 @@ public async Task GetAuthenticationResultForAppAsync( string? authenticationScheme = null, string? tenant = null, TokenAcquisitionOptions? tokenAcquisitionOptions = null) + { + return await GetAuthenticationResultForAppInternalAsync( + scope, + authenticationScheme, + tenant, + tokenAcquisitionOptions, + retryCount: 0).ConfigureAwait(false); + } + + private async Task GetAuthenticationResultForAppInternalAsync( + string scope, + string? authenticationScheme, + string? tenant, + TokenAcquisitionOptions? tokenAcquisitionOptions, + int retryCount) { _ = Throws.IfNull(scope); @@ -543,6 +593,10 @@ public async Task GetAuthenticationResultForAppAsync( MergedOptions mergedOptions = GetMergedOptions(authenticationScheme, tokenAcquisitionOptions); + bool isTokenBinding = tokenAcquisitionOptions?.ExtraParameters?.TryGetValue(TokenBindingParameterName, out var isTokenBindingObject) == true + && isTokenBindingObject is bool isTokenBindingValue + && isTokenBindingValue; + // If using managed identity if (tokenAcquisitionOptions != null && tokenAcquisitionOptions.ManagedIdentity != null) { @@ -560,6 +614,13 @@ public async Task GetAuthenticationResultForAppAsync( miBuilder.WithClaims(tokenAcquisitionOptions.Claims); } + //TODO: Should client assertion claims be supported for managed identity? + //var clientClaims = GetClientClaimsIfExist(tokenAcquisitionOptions); + //if (clientClaims != null) + //{ + // miBuilder.WithExtraClientAssertionClaims(clientClaims); + //} + return await miBuilder.ExecuteAsync().ConfigureAwait(false); } catch (Exception ex) @@ -569,8 +630,9 @@ public async Task GetAuthenticationResultForAppAsync( } } - // For non-managed identity flows, resolve the tenant - tenant = ResolveTenant(tenant, mergedOptions); + // For non-managed identity flows we only resolve tenant if the caller explicitly provided an override. + // This preserves the ability to use an authority-only configuration with meta-tenants like 'common'. + string? resolvedOverrideTenant = tenant != null ? ResolveTenant(tenant, mergedOptions) : null; if (tokenAcquisitionOptions is not null) { @@ -581,26 +643,31 @@ public async Task GetAuthenticationResultForAppAsync( TokenAcquisitionExtensionOptions? addInOptions = tokenAcquisitionExtensionOptionsMonitor?.CurrentValue; // Use MSAL to get the right token to call the API - var application = await GetOrBuildConfidentialClientApplicationAsync(mergedOptions); + var application = await GetOrBuildConfidentialClientApplicationAsync(mergedOptions, isTokenBinding); AcquireTokenForClientParameterBuilder builder = application .AcquireTokenForClient(new[] { scope }.Except(_scopesRequestedByMsal)) .WithSendX5C(mergedOptions.SendX5C); + if (isTokenBinding) + { + builder.WithMtlsProofOfPossession(); + } + if (addInOptions != null) { addInOptions.InvokeOnBeforeTokenAcquisitionForApp(builder, tokenAcquisitionOptions); } - // MSAL.net only allows .WithTenantId for AAD authorities. This makes sense as there should - // not be cross tenant operations with such an authority. - if (!mergedOptions.Instance.Contains(Constants.CiamAuthoritySuffix + // Apply tenant override only for AAD authorities and only if non-empty + if (!string.IsNullOrEmpty(mergedOptions.Instance) && + !mergedOptions.Instance.Contains(Constants.CiamAuthoritySuffix #if NET6_0_OR_GREATER , StringComparison.OrdinalIgnoreCase #endif - )) + ) && !string.IsNullOrEmpty(resolvedOverrideTenant)) { - builder.WithTenantId(tenant); + builder.WithTenantId(resolvedOverrideTenant); } if (tokenAcquisitionOptions != null) @@ -635,6 +702,13 @@ public async Task GetAuthenticationResultForAppAsync( } builder.WithForceRefresh(tokenAcquisitionOptions.ForceRefresh); builder.WithClaims(tokenAcquisitionOptions.Claims); + + var clientClaims = GetClientClaimsIfExist(tokenAcquisitionOptions); + if (clientClaims != null) + { + builder.WithExtraClientAssertionClaims(clientClaims); + } + if (!string.IsNullOrEmpty(tokenAcquisitionOptions.FmiPath)) { builder.WithFmiPath(tokenAcquisitionOptions.FmiPath); @@ -681,20 +755,25 @@ public async Task GetAuthenticationResultForAppAsync( NotifyCertificateSelection(mergedOptions, application, CerticateObserverAction.SuccessfullyUsed, null); return result; } - catch (MsalServiceException exMsal) when (IsInvalidClientCertificateOrSignedAssertionError(exMsal)) + catch (MsalServiceException exMsal) when (retryCount < MaxCertificateRetries && IsInvalidClientCertificateOrSignedAssertionError(exMsal)) { + Logger.TokenAcquisitionError( + _logger, + $"Certificate error detected. Retrying with next certificate (attempt {retryCount + 1}/{MaxCertificateRetries}). {exMsal.Message}", + exMsal); + string applicationKey = GetApplicationKey(mergedOptions); NotifyCertificateSelection(mergedOptions, application, CerticateObserverAction.Deselected, exMsal); DefaultCertificateLoader.ResetCertificates(mergedOptions.ClientCredentials); _applicationsByAuthorityClientId[applicationKey] = null; - // Retry - _retryClientCertificate = true; - return await GetAuthenticationResultForAppAsync( + // Retry with incremented counter + return await GetAuthenticationResultForAppInternalAsync( scope, - authenticationScheme: authenticationScheme, - tenant: tenant, - tokenAcquisitionOptions: tokenAcquisitionOptions); + authenticationScheme, + tenant, + tokenAcquisitionOptions, + retryCount + 1); } catch (MsalException ex) { @@ -703,10 +782,6 @@ public async Task GetAuthenticationResultForAppAsync( Logger.TokenAcquisitionError(_logger, ex.Message, ex); throw; } - finally - { - _retryClientCertificate = false; - } } private void AddExtraBodyParametersIfNeeded(TokenAcquisitionOptions tokenAcquisitionOptions, AcquireTokenForClientParameterBuilder builder) @@ -865,7 +940,7 @@ public async Task RemoveAccountAsync( { MergedOptions mergedOptions = _tokenAcquisitionHost.GetOptions(authenticationScheme, out _); - IConfidentialClientApplication app = await GetOrBuildConfidentialClientApplicationAsync(mergedOptions); + IConfidentialClientApplication app = await GetOrBuildConfidentialClientApplicationAsync(mergedOptions, isTokenBinding: false); if (mergedOptions.IsB2C) { @@ -887,18 +962,50 @@ public async Task RemoveAccountAsync( private bool IsInvalidClientCertificateOrSignedAssertionError(MsalServiceException exMsal) { - return !_retryClientCertificate && - string.Equals(exMsal.ErrorCode, Constants.InvalidClient, StringComparison.OrdinalIgnoreCase) && - !exMsal.ResponseBody.Contains("AADSTS7000215" // No retry when wrong client secret. + // Only check invalid_client errors + if (!string.Equals(exMsal.ErrorCode, Constants.InvalidClient, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + string responseBody = exMsal.ResponseBody; + #if NET6_0_OR_GREATER - , StringComparison.OrdinalIgnoreCase + foreach (var errorCode in Constants.s_certificateRelatedErrorCodes) + { + if (responseBody.Contains(errorCode, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + return false; +#else + foreach (var errorCode in Constants.s_certificateRelatedErrorCodes) + { + if (responseBody.IndexOf(errorCode, StringComparison.OrdinalIgnoreCase) >= 0) + { + return true; + } + } + return false; #endif - ); } + private static string? GetClientClaimsIfExist(TokenAcquisitionOptions? tokenAcquisitionOptions) + { + string? clientClaims = null; + if (tokenAcquisitionOptions is not null && tokenAcquisitionOptions.ExtraParameters is not null && + tokenAcquisitionOptions.ExtraParameters.ContainsKey("IDWEB_CLIENT_ASSERTION_CLAIMS")) + { + clientClaims = tokenAcquisitionOptions.ExtraParameters["IDWEB_CLIENT_ASSERTION_CLAIMS"] as string; + } + return clientClaims; + } +#pragma warning disable RS0051 // Add internal types and members to the declared API internal /* for testing */ async Task GetOrBuildConfidentialClientApplicationAsync( - MergedOptions mergedOptions) + MergedOptions mergedOptions, + bool isTokenBinding) { string key = GetApplicationKey(mergedOptions); @@ -918,7 +1025,7 @@ private bool IsInvalidClientCertificateOrSignedAssertionError(MsalServiceExcepti return app; // Build and store the application - var newApp = await BuildConfidentialClientApplicationAsync(mergedOptions); + var newApp = await BuildConfidentialClientApplicationAsync(mergedOptions, isTokenBinding); // Recompute the key as BuildConfidentialClientApplicationAsync can cause it to change. key = GetApplicationKey(mergedOptions); @@ -934,10 +1041,22 @@ private bool IsInvalidClientCertificateOrSignedAssertionError(MsalServiceExcepti /// /// Creates an MSAL confidential client application. /// - private async Task BuildConfidentialClientApplicationAsync(MergedOptions mergedOptions) + private async Task BuildConfidentialClientApplicationAsync( + MergedOptions mergedOptions, + bool isTokenBinding) { mergedOptions.PrepareAuthorityInstanceForMsal(); + // Validate that we have enough configuration to build an authority + // When PreserveAuthority is true, we use Authority directly, so PreparedInstance is not required + // When IsB2C is true, we still need PreparedInstance + if (!mergedOptions.PreserveAuthority && + string.IsNullOrEmpty(mergedOptions.PreparedInstance) && + string.IsNullOrEmpty(mergedOptions.Authority)) + { + throw new ArgumentException(IDWebErrorMessage.MissingIdentityConfiguration); + } + try { ConfidentialClientApplicationBuilder builder = ConfidentialClientApplicationBuilder @@ -976,7 +1095,41 @@ private async Task BuildConfidentialClientApplic } else if (mergedOptions.IsB2C) { - authority = $"{mergedOptions.PreparedInstance}{ClaimConstants.Tfp}/{mergedOptions.Domain}/{mergedOptions.DefaultUserFlow}"; + // B2C authority construction requires the tenant segment. If Domain was not configured + // (scenario: authority-only configuration providing Instance + SignUpSignInPolicyId), derive it. + string? domain = mergedOptions.Domain; + if (string.IsNullOrEmpty(domain)) + { + // Try tenantId first if provided + if (!string.IsNullOrEmpty(mergedOptions.TenantId)) + { + domain = mergedOptions.TenantId; + } + else if (!string.IsNullOrEmpty(mergedOptions.Instance)) + { + try + { + // Extract first label from host (e.g. fabrikamb2c from fabrikamb2c.b2clogin.com) + var host = new Uri(mergedOptions.Instance).Host; + var firstLabel = host.Split('.').FirstOrDefault(); + if (!string.IsNullOrEmpty(firstLabel)) + { + domain = firstLabel + ".onmicrosoft.com"; + } + } + catch + { + // Ignore derivation failures; will throw below if still null. + } + } + } + + if (string.IsNullOrEmpty(domain)) + { + throw new ArgumentException("B2C Domain could not be determined. Provide Domain or TenantId when using B2C authority-only configuration."); + } + + authority = $"{mergedOptions.PreparedInstance}{ClaimConstants.Tfp}/{domain}/{mergedOptions.DefaultUserFlow}"; builder.WithB2CAuthority(authority); } else @@ -991,7 +1144,8 @@ await builder.WithClientCredentialsAsync( mergedOptions.ClientCredentials!, _logger, _credentialsLoader, - new CredentialSourceLoaderParameters(mergedOptions.ClientId!, authority)); + new CredentialSourceLoaderParameters(mergedOptions.ClientId!, authority), + isTokenBinding); } catch (ArgumentException ex) when (ex.Message == IDWebErrorMessage.ClientCertificatesHaveExpiredOrCannotBeLoaded) { @@ -1072,12 +1226,29 @@ private void NotifyCertificateSelection( // In the case the token is a JWE (encrypted token), we use the decrypted token. string? tokenUsedToCallTheWebApi = GetActualToken(validatedToken); + string? originalTokenToCallWebApi = tokenUsedToCallTheWebApi; AcquireTokenOnBehalfOfParameterBuilder? builder = null; + TokenAcquisitionExtensionOptions? addInOptions = tokenAcquisitionExtensionOptionsMonitor?.CurrentValue; // Case of web APIs: we need to do an on-behalf-of flow, with the token used to call the API if (tokenUsedToCallTheWebApi != null) { + if (addInOptions != null && addInOptions.InvokeOnBeforeOnBehalfOfInitializedAsync != null) + { + var oboInitEventArgs = new OnBehalfOfEventArgs + { + UserAssertionToken = tokenUsedToCallTheWebApi, + User = userHint + }; + await addInOptions.InvokeOnBeforeOnBehalfOfInitializedAsync(oboInitEventArgs).ConfigureAwait(false); + + if (oboInitEventArgs.UserAssertionToken != null) + { + tokenUsedToCallTheWebApi = oboInitEventArgs.UserAssertionToken; + } + } + if (string.IsNullOrEmpty(tokenAcquisitionOptions?.LongRunningWebApiSessionKey)) { builder = application @@ -1114,12 +1285,12 @@ private void NotifyCertificateSelection( { builder.WithSendX5C(mergedOptions.SendX5C); - ClaimsPrincipal? user = _tokenAcquisitionHost.GetUserFromRequest(); + ClaimsPrincipal? userForCcsRouting = _tokenAcquisitionHost.GetUserFromRequest(); var userTenant = string.Empty; - if (user != null) + if (userForCcsRouting != null) { - userTenant = user.GetTenantId(); - builder.WithCcsRoutingHint(user.GetObjectId(), userTenant); + userTenant = userForCcsRouting.GetTenantId(); + builder.WithCcsRoutingHint(userForCcsRouting.GetObjectId(), userTenant); } if (!string.IsNullOrEmpty(tenantId)) { @@ -1134,6 +1305,17 @@ private void NotifyCertificateSelection( } if (tokenAcquisitionOptions != null) { + if (addInOptions != null && addInOptions.InvokeOnBeforeTokenAcquisitionForOnBehalfOfAsync != null) + { + var eventArgs = new OnBehalfOfEventArgs + { + User = userHint, + UserAssertionToken = originalTokenToCallWebApi + }; + + await addInOptions.InvokeOnBeforeTokenAcquisitionForOnBehalfOfAsync(builder, tokenAcquisitionOptions, eventArgs).ConfigureAwait(false); + } + AddFmiPathForSignedAssertionIfNeeded(tokenAcquisitionOptions, builder); var dict = MergeExtraQueryParameters(mergedOptions, tokenAcquisitionOptions); @@ -1164,6 +1346,7 @@ private void NotifyCertificateSelection( dict.Remove(assertionConstant); dict.Remove(subAssertionConstant); } + builder.WithExtraQueryParameters(dict); } if (tokenAcquisitionOptions.ExtraHeadersParameters != null) @@ -1176,6 +1359,11 @@ private void NotifyCertificateSelection( } builder.WithForceRefresh(tokenAcquisitionOptions.ForceRefresh); builder.WithClaims(tokenAcquisitionOptions.Claims); + var clientClaims = GetClientClaimsIfExist(tokenAcquisitionOptions); + if (clientClaims != null) + { + builder.WithExtraClientAssertionClaims(clientClaims); + } if (tokenAcquisitionOptions.PoPConfiguration != null) { builder.WithSignedHttpRequestProofOfPossession(tokenAcquisitionOptions.PoPConfiguration); @@ -1333,6 +1521,11 @@ private Task GetAuthenticationResultForWebAppWithAccountFr } builder.WithForceRefresh(tokenAcquisitionOptions.ForceRefresh); builder.WithClaims(tokenAcquisitionOptions.Claims); + var clientClaims = GetClientClaimsIfExist(tokenAcquisitionOptions); + if (clientClaims != null) + { + builder.WithExtraClientAssertionClaims(clientClaims); + } if (tokenAcquisitionOptions.PoPConfiguration != null) { builder.WithProofOfPossession(tokenAcquisitionOptions.PoPConfiguration); diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisitionExtensionOptions.cs b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisitionExtensionOptions.cs index b2ef5e167..434e0d4af 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisitionExtensionOptions.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisitionExtensionOptions.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; @@ -42,6 +43,74 @@ internal void InvokeOnBeforeTokenAcquisitionForApp(AcquireTokenForClientParamete /// public event BeforeTokenAcquisitionForTestUserAsync? OnBeforeTokenAcquisitionForTestUserAsync; + /// + /// Occurs before an asynchronous token acquisition operation for the On-Behalf-Of authentication flow is + /// initiated. + /// + public event BeforeTokenAcquisitionForOnBehalfOf? OnBeforeTokenAcquisitionForOnBehalfOf; + + /// + /// Occurs before an asynchronous token acquisition operation for the On-Behalf-Of authentication flow is + /// initiated. + /// + public event BeforeTokenAcquisitionForOnBehalfOfAsync? OnBeforeTokenAcquisitionForOnBehalfOfAsync; + + /// + /// Occurs before the On-Behalf-Of flow is initialized. + /// + public event BeforeOnBehalfOfInitialized? OnBeforeOnBehalfOfInitialized; + + /// + /// Occurs before the On-Behalf-Of flow is initialized. + /// + public event BeforeOnBehalfOfInitializedAsync? OnBeforeOnBehalfOfInitializedAsync; + + /// + /// Invoke the OnBeforeTokenAcquisitionForApp event. + /// + internal async Task InvokeOnBeforeTokenAcquisitionForOnBehalfOfAsync(AcquireTokenOnBehalfOfParameterBuilder builder, + AcquireTokenOptions? acquireTokenOptions, + OnBehalfOfEventArgs eventArgs) + { + // Run the async event if it is not null + if (OnBeforeTokenAcquisitionForOnBehalfOfAsync != null) + { + // (cannot directly await an async event because events are not tasks + // they are multicast delegates that invoke handlers, but don't return values to the publisher, + // nor do they support awaiting natively + var invocationList = OnBeforeTokenAcquisitionForOnBehalfOfAsync.GetInvocationList(); + var tasks = invocationList + .Cast() + .Select(handler => handler(builder, acquireTokenOptions, eventArgs)); + await Task.WhenAll(tasks); + } + + // Run the sync event if it is not null. + OnBeforeTokenAcquisitionForOnBehalfOf?.Invoke(builder, acquireTokenOptions, eventArgs); + } + + /// + /// Invoke the OnBeforeOnBehalfOfInitializedAsync event. + /// + internal async Task InvokeOnBeforeOnBehalfOfInitializedAsync(OnBehalfOfEventArgs eventArgs) + { + // Run the async event if it is not null + if (OnBeforeOnBehalfOfInitializedAsync != null) + { + // (cannot directly await an async event because events are not tasks + // they are multicast delegates that invoke handlers, but don't return values to the publisher, + // nor do they support awaiting natively + var invocationList = OnBeforeOnBehalfOfInitializedAsync.GetInvocationList(); + var tasks = invocationList + .Cast() + .Select(handler => handler(eventArgs)); + await Task.WhenAll(tasks); + } + + // Run the sync event if it is not null. + OnBeforeOnBehalfOfInitialized?.Invoke(eventArgs); + } + /// /// Invoke the BeforeTokenAcquisitionForTestUser event. /// diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisitionExtensions.cs b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisitionExtensions.cs index c2394f670..f70072a77 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisitionExtensions.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisitionExtensions.cs @@ -31,4 +31,31 @@ namespace Microsoft.Identity.Web /// User claims. public delegate Task BeforeTokenAcquisitionForTestUserAsync(AcquireTokenByUsernameAndPasswordConfidentialParameterBuilder builder, AcquireTokenOptions? acquireTokenOptions, ClaimsPrincipal user); + /// + /// Signature for token acquisition extensions that act on the request builder, for on-behalf-of flow. + /// + /// Builder + /// Token acquisition options for the request. Can be null. + /// Event arguments containing user claims and additional context information. + public delegate void BeforeTokenAcquisitionForOnBehalfOf(AcquireTokenOnBehalfOfParameterBuilder builder, AcquireTokenOptions? acquireTokenOptions, OnBehalfOfEventArgs eventArgs); + + /// + /// Signature for token acquisition extensions that act on the request builder, for on-behalf-of flow (Async version). + /// + /// Builder + /// Token acquisition options for the request. Can be null. + /// Event arguments containing user claims and additional context information. + public delegate Task BeforeTokenAcquisitionForOnBehalfOfAsync(AcquireTokenOnBehalfOfParameterBuilder builder, AcquireTokenOptions? acquireTokenOptions, OnBehalfOfEventArgs eventArgs); + + /// + /// Signature for a sync event that fires before the on-behalf-of flow is initialized. + /// + /// Event arguments containing the user assertion token. Handlers can modify to replace the assertion. + public delegate void BeforeOnBehalfOfInitialized(OnBehalfOfEventArgs eventArgs); + + /// + /// Signature for an async event that fires before the on-behalf-of flow is initialized. + /// + /// Event arguments containing the user assertion token. Handlers can modify to replace the assertion. + public delegate Task BeforeOnBehalfOfInitializedAsync(OnBehalfOfEventArgs eventArgs); } diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/WebApiBuilders.cs b/src/Microsoft.Identity.Web.TokenAcquisition/WebApiBuilders.cs index 152970ddc..eab099a5a 100644 --- a/src/Microsoft.Identity.Web.TokenAcquisition/WebApiBuilders.cs +++ b/src/Microsoft.Identity.Web.TokenAcquisition/WebApiBuilders.cs @@ -26,9 +26,8 @@ public static class WebApiBuilders /// The services being configured. /// IConfigurationSection. /// The authentication builder to chain. -#if NET6_0_OR_GREATER && !NET8_0_OR_GREATER - [RequiresUnreferencedCode("Calls Bind, Configure with Unspecified Configuration and ServiceCollection.")] -#endif + [RequiresUnreferencedCode("Calls Microsoft.Extensions.Configuration.ConfigurationBinder.Bind(IConfiguration, Object).")] + [RequiresDynamicCode("Calls Microsoft.Extensions.Configuration.ConfigurationBinder.Bind(IConfiguration, Object).")] public static MicrosoftIdentityAppCallsWebApiAuthenticationBuilder EnableTokenAcquisition( Action configureConfidentialClientApplicationOptions, string authenticationScheme, @@ -37,8 +36,6 @@ public static MicrosoftIdentityAppCallsWebApiAuthenticationBuilder EnableTokenAc { if (configuration != null) { - // TODO: This never was right. And the configureConfidentialClientApplicationOptions delegate is not used - // services.Configure(authenticationScheme, configuration); services.Configure(authenticationScheme, options => { configuration.Bind(options); }); diff --git a/src/Microsoft.Identity.Web.TokenCache/Microsoft.Identity.Web.TokenCache.csproj b/src/Microsoft.Identity.Web.TokenCache/Microsoft.Identity.Web.TokenCache.csproj index efb69b7de..20ede7cd4 100644 --- a/src/Microsoft.Identity.Web.TokenCache/Microsoft.Identity.Web.TokenCache.csproj +++ b/src/Microsoft.Identity.Web.TokenCache/Microsoft.Identity.Web.TokenCache.csproj @@ -7,6 +7,7 @@ {7885DFBB-0D20-4115-86E2-709C2E12253B} README.md $(NoWarn);NU1510 + true diff --git a/src/Microsoft.Identity.Web/AuthorizeForScopesAttribute.cs b/src/Microsoft.Identity.Web/AuthorizeForScopesAttribute.cs index 540768a73..e259536ee 100644 --- a/src/Microsoft.Identity.Web/AuthorizeForScopesAttribute.cs +++ b/src/Microsoft.Identity.Web/AuthorizeForScopesAttribute.cs @@ -53,7 +53,7 @@ public class AuthorizeForScopesAttribute : ExceptionFilterAttribute /// /// Handles the . /// - /// Context provided by ASP.NET Core. + /// context provided by ASP.NET Core. public override void OnException(ExceptionContext context) { if (context != null) diff --git a/src/Microsoft.Identity.Web/DownstreamWebApiSupport/DownstreamWebApi.cs b/src/Microsoft.Identity.Web/DownstreamWebApiSupport/DownstreamWebApi.cs index faae19acf..f1c68c25d 100644 --- a/src/Microsoft.Identity.Web/DownstreamWebApiSupport/DownstreamWebApi.cs +++ b/src/Microsoft.Identity.Web/DownstreamWebApiSupport/DownstreamWebApi.cs @@ -108,9 +108,8 @@ public async Task CallWebApiForUserAsync( } /// -#if NET6_0_OR_GREATER [RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Serialize(TValue, JsonSerializerOptions).")] -#endif + [RequiresDynamicCode("Calls System.Text.Json.JsonSerializer.Serialize(TValue, JsonSerializerOptions).")] public async Task CallWebApiForUserAsync( string serviceName, TInput input, diff --git a/src/Microsoft.Identity.Web/DownstreamWebApiSupport/DownstreamWebApiExtensions.cs b/src/Microsoft.Identity.Web/DownstreamWebApiSupport/DownstreamWebApiExtensions.cs index 3d21f4930..cc0244df0 100644 --- a/src/Microsoft.Identity.Web/DownstreamWebApiSupport/DownstreamWebApiExtensions.cs +++ b/src/Microsoft.Identity.Web/DownstreamWebApiSupport/DownstreamWebApiExtensions.cs @@ -25,9 +25,8 @@ public static class DownstreamWebApiExtensions [Obsolete("Use AddDownstreamApi in Microsoft.Identity.Abstractions, implemented in Microsoft.Identity.Web.DownstreamApi." + "See aka.ms/id-web-downstream-api-v2 for migration details.", false)] [EditorBrowsable(EditorBrowsableState.Never)] -#if NET6_0_OR_GREATER && !NET8_0_OR_GREATER [RequiresUnreferencedCode("Calls Microsoft.Extensions.DependencyInjection.OptionsConfigurationServiceCollectionExtensions.Configure(IServiceCollection, String, IConfiguration).")] -#endif + [RequiresDynamicCode("Calls Microsoft.Extensions.DependencyInjection.OptionsConfigurationServiceCollectionExtensions.Configure(IServiceCollection, String, IConfiguration).")] public static MicrosoftIdentityAppCallsWebApiAuthenticationBuilder AddDownstreamWebApi( this MicrosoftIdentityAppCallsWebApiAuthenticationBuilder builder, string serviceName, diff --git a/src/Microsoft.Identity.Web/DownstreamWebApiSupport/DownstreamWebApiGenericExtensions.cs b/src/Microsoft.Identity.Web/DownstreamWebApiSupport/DownstreamWebApiGenericExtensions.cs index 8f13285e9..2d263030e 100644 --- a/src/Microsoft.Identity.Web/DownstreamWebApiSupport/DownstreamWebApiGenericExtensions.cs +++ b/src/Microsoft.Identity.Web/DownstreamWebApiSupport/DownstreamWebApiGenericExtensions.cs @@ -38,9 +38,8 @@ public static class DownstreamWebApiGenericExtensions [Obsolete("Use IDownstreamApi.GetForUserAsync in Microsoft.Identity.Abstractions, implemented in Microsoft.Identity.Web.DownstreamApi." + "See aka.ms/id-web-downstream-api-v2 for migration details.", false)] [EditorBrowsable(EditorBrowsableState.Never)] -#if NET6_0_OR_GREATER [RequiresUnreferencedCode("Calls Microsoft.Identity.Web.DownstreamWebApiGenericExtensions.ConvertToOutput(TInput).")] -#endif + [RequiresDynamicCode("Calls Microsoft.Identity.Web.DownstreamWebApiGenericExtensions.ConvertToOutput(TInput).")] public static async Task GetForUserAsync( this IDownstreamWebApi downstreamWebApi, string serviceName, @@ -82,9 +81,8 @@ public static class DownstreamWebApiGenericExtensions [Obsolete("Use IDownstreamApi.GetForUserAsync in Microsoft.Identity.Abstractions, implemented in Microsoft.Identity.Web.DownstreamApi." + "See aka.ms/id-web-downstream-api-v2 for migration details.", false)] [EditorBrowsable(EditorBrowsableState.Never)] -#if NET6_0_OR_GREATER [RequiresUnreferencedCode("Calls Microsoft.Identity.Web.DownstreamWebApiGenericExtensions.ConvertFromInput(TInput).")] -#endif + [RequiresDynamicCode("Calls Microsoft.Identity.Web.DownstreamWebApiGenericExtensions.ConvertFromInput(TInput).")] public static async Task GetForUserAsync( this IDownstreamWebApi downstreamWebApi, string serviceName, @@ -129,9 +127,8 @@ await downstreamWebApi.CallWebApiForUserAsync( [Obsolete("Use IDownstreamApi.PostForUserAsync in Microsoft.Identity.Abstractions, implemented in Microsoft.Identity.Web.DownstreamApi." + "See aka.ms/id-web-downstream-api-v2 for migration details.", false)] [EditorBrowsable(EditorBrowsableState.Never)] -#if NET6_0_OR_GREATER [RequiresUnreferencedCode("Calls Microsoft.Identity.Web.DownstreamWebApiGenericExtensions.ConvertToOutput(TInput).")] -#endif + [RequiresDynamicCode("Calls Microsoft.Identity.Web.DownstreamWebApiGenericExtensions.ConvertToOutput(TInput).")] public static async Task PostForUserAsync( this IDownstreamWebApi downstreamWebApi, string serviceName, @@ -178,9 +175,8 @@ await downstreamWebApi.CallWebApiForUserAsync( [Obsolete("Use IDownstreamApi.PutForUserAsync in Microsoft.Identity.Abstractions, implemented in Microsoft.Identity.Web.DownstreamApi." + "See aka.ms/id-web-downstream-api-v2 for migration details.", false)] [EditorBrowsable(EditorBrowsableState.Never)] -#if NET6_0_OR_GREATER [RequiresUnreferencedCode("Calls Microsoft.Identity.Web.DownstreamWebApiGenericExtensions.ConvertFromInput(TInput).")] -#endif + [RequiresDynamicCode("Calls Microsoft.Identity.Web.DownstreamWebApiGenericExtensions.ConvertFromInput(TInput).")] public static async Task PutForUserAsync( this IDownstreamWebApi downstreamWebApi, string serviceName, @@ -226,9 +222,8 @@ await downstreamWebApi.CallWebApiForUserAsync( [Obsolete("Use IDownstreamApi.PutForUserAsync in Microsoft.Identity.Abstractions, implemented in Microsoft.Identity.Web.DownstreamApi." + "See aka.ms/id-web-downstream-api-v2 for migration details.", false)] [EditorBrowsable(EditorBrowsableState.Never)] -#if NET6_0_OR_GREATER [RequiresUnreferencedCode("Calls Microsoft.Identity.Web.DownstreamWebApiGenericExtensions.ConvertToOutput(TInput).")] -#endif + [RequiresDynamicCode("Calls Microsoft.Identity.Web.DownstreamWebApiGenericExtensions.ConvertToOutput(TInput).")] public static async Task PutForUserAsync( this IDownstreamWebApi downstreamWebApi, string serviceName, @@ -274,9 +269,8 @@ await downstreamWebApi.CallWebApiForUserAsync( [Obsolete("Use IDownstreamApi.CallWebApiForUserAsync in Microsoft.Identity.Abstractions, implemented in Microsoft.Identity.Web.DownstreamApi." + "See aka.ms/id-web-downstream-api-v2 for migration details.", false)] [EditorBrowsable(EditorBrowsableState.Never)] -#if NET6_0_OR_GREATER [RequiresUnreferencedCode("Calls Microsoft.Identity.Web.DownstreamWebApiGenericExtensions.ConvertToOutput(TInput).")] -#endif + [RequiresDynamicCode("Calls Microsoft.Identity.Web.DownstreamWebApiGenericExtensions.ConvertToOutput(TInput).")] public static async Task CallWebApiForUserAsync( this IDownstreamWebApi downstreamWebApi, string serviceName, @@ -296,17 +290,15 @@ await downstreamWebApi.CallWebApiForUserAsync( return await ConvertToOutputAsync(response).ConfigureAwait(false); } -#if NET6_0_OR_GREATER [RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Serialize(TValue, JsonSerializerOptions).")] -#endif + [RequiresDynamicCode("Calls System.Text.Json.JsonSerializer.Serialize(TValue, JsonSerializerOptions).")] private static StringContent ConvertFromInput(TInput input) { return new StringContent(JsonSerializer.Serialize(input), Encoding.UTF8, "application/json"); } -#if NET6_0_OR_GREATER [RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Deserialize(String, JsonSerializerOptions).")] -#endif + [RequiresDynamicCode("Calls System.Text.Json.JsonSerializer.Deserialize(String, JsonSerializerOptions).")] private static async Task ConvertToOutputAsync(HttpResponseMessage response) where TOutput : class { diff --git a/src/Microsoft.Identity.Web/DownstreamWebApiSupport/IDownstreamWebApi.cs b/src/Microsoft.Identity.Web/DownstreamWebApiSupport/IDownstreamWebApi.cs index abcbe8de8..96baf27c2 100644 --- a/src/Microsoft.Identity.Web/DownstreamWebApiSupport/IDownstreamWebApi.cs +++ b/src/Microsoft.Identity.Web/DownstreamWebApiSupport/IDownstreamWebApi.cs @@ -119,9 +119,8 @@ Task CallWebApiForUserAsync( /// } /// /// -#if NET6_0_OR_GREATER [RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Serialize(TValue, JsonSerializerOptions).")] -#endif + [RequiresDynamicCode("Calls System.Text.Json.JsonSerializer.Serialize(TValue, JsonSerializerOptions).")] public Task CallWebApiForUserAsync( string serviceName, TInput input, @@ -185,9 +184,8 @@ Task CallWebApiForUserAsync( /// } /// /// -#if NET6_0_OR_GREATER [RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Serialize(TValue, JsonSerializerOptions).")] -#endif + [RequiresDynamicCode("Calls System.Text.Json.JsonSerializer.Serialize(TValue, JsonSerializerOptions).")] Task CallWebApiForUserAsync( string serviceName, TInput input, diff --git a/src/Microsoft.Identity.Web/DownstreamWebApiSupport/MicrosoftIdentityAuthenticationMessageHandlerHttpClientBuilderExtensions.cs b/src/Microsoft.Identity.Web/DownstreamWebApiSupport/MicrosoftIdentityAuthenticationMessageHandlerHttpClientBuilderExtensions.cs index 94f3cb794..b821dccf0 100644 --- a/src/Microsoft.Identity.Web/DownstreamWebApiSupport/MicrosoftIdentityAuthenticationMessageHandlerHttpClientBuilderExtensions.cs +++ b/src/Microsoft.Identity.Web/DownstreamWebApiSupport/MicrosoftIdentityAuthenticationMessageHandlerHttpClientBuilderExtensions.cs @@ -22,9 +22,8 @@ public static class MicrosoftIdentityAuthenticationMessageHandlerHttpClientBuild /// Name of the configuration for the service. /// Configuration. /// The builder for chaining. -#if NET6_0_OR_GREATER && !NET8_0_OR_GREATER [RequiresUnreferencedCode("Calls Microsoft.Extensions.DependencyInjection.OptionsConfigurationServiceCollectionExtensions.Configure(IServiceCollection, String, IConfiguration).")] -#endif + [RequiresDynamicCode("Calls Microsoft.Extensions.DependencyInjection.OptionsConfigurationServiceCollectionExtensions.Configure(IServiceCollection, String, IConfiguration).")] public static IHttpClientBuilder AddMicrosoftIdentityUserAuthenticationHandler( this IHttpClientBuilder builder, string serviceName, @@ -65,9 +64,8 @@ public static IHttpClientBuilder AddMicrosoftIdentityUserAuthenticationHandler( /// Name of the configuration for the service. /// Configuration. /// The builder for chaining. -#if NET6_0_OR_GREATER && !NET8_0_OR_GREATER [RequiresUnreferencedCode("Calls Microsoft.Extensions.DependencyInjection.OptionsConfigurationServiceCollectionExtensions.Configure(IServiceCollection, String, IConfiguration).")] -#endif + [RequiresDynamicCode("Calls Microsoft.Extensions.DependencyInjection.OptionsConfigurationServiceCollectionExtensions.Configure(IServiceCollection, String, IConfiguration).")] public static IHttpClientBuilder AddMicrosoftIdentityAppAuthenticationHandler( this IHttpClientBuilder builder, string serviceName, diff --git a/src/Microsoft.Identity.Web/Internal/IdentityOptionsHelpers.cs b/src/Microsoft.Identity.Web/Internal/IdentityOptionsHelpers.cs new file mode 100644 index 000000000..6623fc91b --- /dev/null +++ b/src/Microsoft.Identity.Web/Internal/IdentityOptionsHelpers.cs @@ -0,0 +1,193 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Identity.Abstractions; +using Microsoft.IdentityModel.Tokens; + +#if !NETSTANDARD2_0 && !NET462 && !NET472 +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Identity.Web.Resource; +#endif + +namespace Microsoft.Identity.Web.Internal +{ + /// + /// Shared helper methods for identity options configuration and validation. + /// Used by both traditional (MergedOptions) and AOT-compatible paths. + /// + internal static class IdentityOptionsHelpers + { + /// + /// Builds the authority URL from the given application options. + /// Handles AAD, B2C, and CIAM scenarios. + /// + /// The application options containing instance, tenant, and domain information. + /// The constructed authority URL. + internal static string BuildAuthority(MicrosoftIdentityApplicationOptions options) + { + if (string.IsNullOrEmpty(options.Instance)) + { + throw new ArgumentNullException(nameof(options.Instance)); + } + +#if !NETSTANDARD2_0 && !NET462 && !NET472 + Uri baseUri = new Uri(options.Instance); + var domain = options.Domain; + var tenantId = options.TenantId; + + // B2C is detected by presence of SignUpSignInPolicyId + bool isB2C = !string.IsNullOrWhiteSpace(options.SignUpSignInPolicyId); + + if (isB2C) + { + var userFlow = options.SignUpSignInPolicyId; + return new Uri(baseUri, new PathString($"{baseUri.PathAndQuery}{domain}/{userFlow}/v2.0")).ToString(); + } + + return new Uri(baseUri, new PathString($"{baseUri.PathAndQuery}{tenantId}/v2.0")).ToString(); +#else + // For non-ASP.NET Core, use simple string concatenation + // options.Instance is guaranteed to be non-null because we check it at the start of the method + var instance = options.Instance!.TrimEnd('/'); + bool isB2C = !string.IsNullOrWhiteSpace(options.SignUpSignInPolicyId); + + if (isB2C) + { + return $"{instance}/{options.Domain}/{options.SignUpSignInPolicyId}/v2.0"; + } + + return $"{instance}/{options.TenantId}/v2.0"; +#endif + } + + +#if !NETSTANDARD2_0 && !NET462 && !NET472 + /// + /// Configures issuer validation on the JWT bearer options. + /// Sets up multi-tenant issuer validation logic that accepts both v1.0 and v2.0 tokens. + /// If the developer has already registered an IssuerValidator, it will not be overwritten. + /// + /// The JWT bearer options containing token validation parameters and authority. + /// The service provider to resolve the issuer validator factory. + internal static void ConfigureIssuerValidation( + JwtBearerOptions options, + IServiceProvider serviceProvider) + { + if (options.TokenValidationParameters.ValidateIssuer && + options.TokenValidationParameters.IssuerValidator == null) + { + var microsoftIdentityIssuerValidatorFactory = + serviceProvider.GetRequiredService(); + + options.TokenValidationParameters.IssuerValidator = + microsoftIdentityIssuerValidatorFactory.GetAadIssuerValidator(options.Authority!).Validate; + } + } + + /// + /// Ensures the JwtBearerOptions.Events object exists and wires up the + /// ConfigurationManager on the OnMessageReceived event. + /// + /// The JWT bearer options to configure. + internal static void InitializeJwtBearerEvents(JwtBearerOptions options) + { + if (options.Events == null) + { + options.Events = new JwtBearerEvents(); + } + + var existingOnMessageReceived = options.Events.OnMessageReceived; + options.Events.OnMessageReceived = async context => + { + context.Options.TokenValidationParameters.ConfigurationManager ??= + options.ConfigurationManager as BaseConfigurationManager; + + if (existingOnMessageReceived != null) + { + await existingOnMessageReceived(context).ConfigureAwait(false); + } + }; + } + + /// + /// Configures audience validation on the token validation parameters if not already configured. + /// Sets up custom validator for handling v1.0/v2.0 and B2C tokens correctly. + /// This is AOT-compatible as it directly sets up the validator without using reflection or MicrosoftIdentityOptions. + /// + /// The token validation parameters to configure. + /// The application (client) ID. + /// Whether the application targets Azure AD B2C. + internal static void ConfigureAudienceValidation( + TokenValidationParameters validationParameters, + string? clientId, + bool isB2C) + { + // Skip if audience validation is already configured by the caller + if (validationParameters.AudienceValidator != null || + validationParameters.ValidAudience != null || + validationParameters.ValidAudiences != null) + { + return; + } + + // Set up the audience validator directly without converting to MicrosoftIdentityOptions + validationParameters.AudienceValidator = (audiences, securityToken, validationParams) => + { + var claims = securityToken switch + { + System.IdentityModel.Tokens.Jwt.JwtSecurityToken jwtSecurityToken => jwtSecurityToken.Claims, + Microsoft.IdentityModel.JsonWebTokens.JsonWebToken jwtWebToken => jwtWebToken.Claims, + _ => throw new SecurityTokenValidationException(IDWebErrorMessage.TokenIsNotJwtToken), + }; + + validationParams.AudienceValidator = null; + + // Case of a default App ID URI (the developer did not provide explicit valid audience(s)) + if (string.IsNullOrEmpty(validationParams.ValidAudience) && + validationParams.ValidAudiences == null) + { + // handle v2.0 access token or Azure AD B2C tokens (even if v1.0) + if (isB2C || claims.Any(c => c.Type == Constants.Version && c.Value == Constants.V2)) + { + validationParams.ValidAudience = $"{clientId}"; + } + // handle v1.0 access token + else if (claims.Any(c => c.Type == Constants.Version && c.Value == Constants.V1)) + { + validationParams.ValidAudience = $"api://{clientId}"; + } + } + + Validators.ValidateAudience(audiences, securityToken, validationParams); + return true; + }; + } + + /// + /// Chains a handler onto the OnTokenValidated event to store the token for OBO scenarios. + /// + /// The existing OnTokenValidated handler, if any. + /// A new handler that stores the token and then calls the existing handler. + internal static Func ChainTokenStorageHandler(Func? existingHandler) + { + return async context => + { + // Only pass through a token if it is of an expected type + context.HttpContext.StoreTokenUsedToCallWebAPI( + context.SecurityToken is System.IdentityModel.Tokens.Jwt.JwtSecurityToken or + Microsoft.IdentityModel.JsonWebTokens.JsonWebToken ? context.SecurityToken : null); + + if (existingHandler != null) + { + await existingHandler(context).ConfigureAwait(false); + } + }; + } +#endif + } +} diff --git a/src/Microsoft.Identity.Web/Microsoft.Identity.Web.csproj b/src/Microsoft.Identity.Web/Microsoft.Identity.Web.csproj index 50fed045b..0777de23d 100644 --- a/src/Microsoft.Identity.Web/Microsoft.Identity.Web.csproj +++ b/src/Microsoft.Identity.Web/Microsoft.Identity.Web.csproj @@ -14,6 +14,10 @@ README.md + + true + + True @@ -55,7 +59,7 @@ - + diff --git a/src/Microsoft.Identity.Web/Policy/ScopeOrAppPermissionAuthorizationHandler.cs b/src/Microsoft.Identity.Web/Policy/ScopeOrAppPermissionAuthorizationHandler.cs index ae3dd9e64..0cbb1f275 100644 --- a/src/Microsoft.Identity.Web/Policy/ScopeOrAppPermissionAuthorizationHandler.cs +++ b/src/Microsoft.Identity.Web/Policy/ScopeOrAppPermissionAuthorizationHandler.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; @@ -75,7 +74,7 @@ protected override Task HandleRequirementAsync( if (appPermissionConfigurationKey != null) { - appPermissions = _configuration.GetValue(appPermissionConfigurationKey)?.Split(' '); + appPermissions = _configuration[appPermissionConfigurationKey]?.Split(' '); } if (appPermissions is null) @@ -102,7 +101,7 @@ protected override Task HandleRequirementAsync( { return Task.CompletedTask; } - + var hasScope = scopes != null && scopeClaims.SelectMany(s => s.Value.Split(' ')).Intersect(scopes).Any(); var hasAppPermission = appPermissions != null && appPermissionClaims.SelectMany(s => s.Value.Split(' ')).Intersect(appPermissions).Any(); diff --git a/src/Microsoft.Identity.Web/PostConfigureOptions/MicrosoftIdentityJwtBearerOptionsPostConfigurator.cs b/src/Microsoft.Identity.Web/PostConfigureOptions/MicrosoftIdentityJwtBearerOptionsPostConfigurator.cs new file mode 100644 index 000000000..470daa442 --- /dev/null +++ b/src/Microsoft.Identity.Web/PostConfigureOptions/MicrosoftIdentityJwtBearerOptionsPostConfigurator.cs @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#if NET10_0_OR_GREATER + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Web.Internal; +using Microsoft.Identity.Web.Resource; +using Microsoft.IdentityModel.Tokens; +using Microsoft.IdentityModel.Validators; + +namespace Microsoft.Identity.Web.PostConfigureOptions +{ + /// + /// Post-configures JwtBearerOptions for AOT-compatible path using MicrosoftIdentityApplicationOptions. + /// Performs validation, configuration, and OBO token storage setup. + /// + internal sealed class MicrosoftIdentityJwtBearerOptionsPostConfigurator : IPostConfigureOptions + { + private readonly IOptionsMonitor _appOptionsMonitor; + private readonly IServiceProvider _serviceProvider; + + public MicrosoftIdentityJwtBearerOptionsPostConfigurator( + IOptionsMonitor appOptionsMonitor, + IServiceProvider serviceProvider) + { + _appOptionsMonitor = appOptionsMonitor; + _serviceProvider = serviceProvider; + } + + public void PostConfigure(string? name, JwtBearerOptions options) + { + var appOptions = _appOptionsMonitor.Get(name ?? string.Empty); + + // Skip if not configured via our AOT path (no ClientId means not configured) + if (string.IsNullOrEmpty(appOptions.ClientId)) + { + return; + } + + // 1. VALIDATE (fail-fast with complete configuration) + ValidateRequiredOptions(appOptions); + + // 2. CONFIGURE (respect customer overrides) + + // Note: 'options.Authority' is set during the Configure phase in AddMicrosoftIdentityWebApiAot, + // before any PostConfigure runs to ensure ASP.NET's built-in JwtBearerPostConfigureOptions can + // create the ConfigurationManager from it. + + // Configure audience validation if not already set + IdentityOptionsHelpers.ConfigureAudienceValidation( + options.TokenValidationParameters, + appOptions.ClientId, + !string.IsNullOrWhiteSpace(appOptions.SignUpSignInPolicyId)); + + // Configure issuer validation + IdentityOptionsHelpers.ConfigureIssuerValidation(options, _serviceProvider); + + // Configure token decryption if credentials provided + if (appOptions.TokenDecryptionCredentials != null && appOptions.TokenDecryptionCredentials.Any()) + { + // Extract user assigned identity client ID from credentials if present + var managedIdentityCredential = appOptions.TokenDecryptionCredentials + .OfType() + .FirstOrDefault(c => !string.IsNullOrEmpty(c.ManagedIdentityClientId)); + + if (managedIdentityCredential != null) + { + DefaultCertificateLoader.UserAssignedManagedIdentityClientId = managedIdentityCredential.ManagedIdentityClientId; + } + + IEnumerable certificates = DefaultCertificateLoader.LoadAllCertificates( + appOptions.TokenDecryptionCredentials.OfType()); + IEnumerable keys = certificates.Select(c => new X509SecurityKey(c)); + options.TokenValidationParameters.TokenDecryptionKeys = keys; + } + + // Enable AAD signing key issuer validation + options.TokenValidationParameters.EnableAadSigningKeyIssuerValidation(); + + // Ensure events and wire up ConfigurationManager on message received + IdentityOptionsHelpers.InitializeJwtBearerEvents(options); + + // Add claims validation if not allowing ACL authorization + if (!appOptions.AllowWebApiToBeAuthorizedByACL) + { + MicrosoftIdentityWebApiAuthenticationBuilderExtensions.ChainOnTokenValidatedEventForClaimsValidation( + options.Events, name ?? JwtBearerDefaults.AuthenticationScheme); + } + + // ========================================================= + // 3. CHAIN OnTokenValidated (always - required for OBO) + // ========================================================= + options.Events.OnTokenValidated = IdentityOptionsHelpers.ChainTokenStorageHandler( + options.Events.OnTokenValidated); + } + + /// + /// Validates that required options are present based on the configuration scenario. + /// + /// The application options to validate. + /// Thrown when required options are missing. + internal static void ValidateRequiredOptions(MicrosoftIdentityApplicationOptions options) + { + if (string.IsNullOrEmpty(options.ClientId)) + { + throw new ArgumentNullException( + nameof(options.ClientId), + string.Format( + System.Globalization.CultureInfo.InvariantCulture, + IDWebErrorMessage.ConfigurationOptionRequired, + nameof(options.ClientId))); + } + + // B2C is detected by presence of SignUpSignInPolicyId + bool isB2C = !string.IsNullOrWhiteSpace(options.SignUpSignInPolicyId); + + if (string.IsNullOrEmpty(options.Authority)) + { + if (string.IsNullOrEmpty(options.Instance)) + { + throw new ArgumentNullException( + nameof(options.Instance), + string.Format( + System.Globalization.CultureInfo.InvariantCulture, + IDWebErrorMessage.ConfigurationOptionRequired, + nameof(options.Instance))); + } + + if (isB2C) + { + if (string.IsNullOrEmpty(options.Domain)) + { + throw new ArgumentNullException( + nameof(options.Domain), + string.Format( + System.Globalization.CultureInfo.InvariantCulture, + IDWebErrorMessage.ConfigurationOptionRequired, + nameof(options.Domain))); + } + } + else + { + if (string.IsNullOrEmpty(options.TenantId)) + { + throw new ArgumentNullException( + nameof(options.TenantId), + string.Format( + System.Globalization.CultureInfo.InvariantCulture, + IDWebErrorMessage.ConfigurationOptionRequired, + nameof(options.TenantId))); + } + } + } + } + } +} + +#endif diff --git a/src/Microsoft.Identity.Web/PublicAPI/net10.0/InternalAPI.Shipped.txt b/src/Microsoft.Identity.Web/PublicAPI/net10.0/InternalAPI.Shipped.txt index 4d4a6507f..23d39b924 100644 --- a/src/Microsoft.Identity.Web/PublicAPI/net10.0/InternalAPI.Shipped.txt +++ b/src/Microsoft.Identity.Web/PublicAPI/net10.0/InternalAPI.Shipped.txt @@ -34,6 +34,7 @@ Microsoft.Identity.Web.DefaultMicrosoftIdentityAuthenticationDelegatingHandlerFa Microsoft.Identity.Web.DownstreamWebApi.MergeOptions(string! optionsInstanceName, System.Action? calledApiOptionsOverride) -> Microsoft.Identity.Web.DownstreamWebApiOptions! Microsoft.Identity.Web.Extensions Microsoft.Identity.Web.IncrementalConsentAndConditionalAccessHelper +Microsoft.Identity.Web.Internal.IdentityOptionsHelpers Microsoft.Identity.Web.MergedOptionsValidation Microsoft.Identity.Web.MergedOptionsValidation.MergedOptionsValidation() -> void Microsoft.Identity.Web.MicrosoftIdentityConsentAndConditionalAccessHandler.NavigationManager.get -> Microsoft.AspNetCore.Components.NavigationManager! @@ -47,6 +48,9 @@ Microsoft.Identity.Web.MicrosoftIdentityWebApiAuthenticationBuilder.MicrosoftIde Microsoft.Identity.Web.MicrosoftIdentityWebApiAuthenticationBuilderWithConfiguration.MicrosoftIdentityWebApiAuthenticationBuilderWithConfiguration(Microsoft.Extensions.DependencyInjection.IServiceCollection! services, string! jwtBearerAuthenticationScheme, System.Action! configureJwtBearerOptions, System.Action! configureMicrosoftIdentityOptions, Microsoft.Extensions.Configuration.IConfigurationSection? configurationSection) -> void Microsoft.Identity.Web.MicrosoftIdentityWebAppAuthenticationBuilder.MicrosoftIdentityWebAppAuthenticationBuilder(Microsoft.Extensions.DependencyInjection.IServiceCollection! services, string! openIdConnectScheme, System.Action! configureMicrosoftIdentityOptions, Microsoft.Extensions.Configuration.IConfigurationSection? configurationSection) -> void Microsoft.Identity.Web.MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration.MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration(Microsoft.Extensions.DependencyInjection.IServiceCollection! services, string! openIdConnectScheme, System.Action! configureMicrosoftIdentityOptions, Microsoft.Extensions.Configuration.IConfigurationSection! configurationSection) -> void +Microsoft.Identity.Web.PostConfigureOptions.MicrosoftIdentityJwtBearerOptionsPostConfigurator +Microsoft.Identity.Web.PostConfigureOptions.MicrosoftIdentityJwtBearerOptionsPostConfigurator.MicrosoftIdentityJwtBearerOptionsPostConfigurator(Microsoft.Extensions.Options.IOptionsMonitor! appOptionsMonitor, System.IServiceProvider! serviceProvider) -> void +Microsoft.Identity.Web.PostConfigureOptions.MicrosoftIdentityJwtBearerOptionsPostConfigurator.PostConfigure(string? name, Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions! options) -> void Microsoft.Identity.Web.RequireScopeOptions Microsoft.Identity.Web.RequireScopeOptions.PostConfigure(string? name, Microsoft.AspNetCore.Authorization.AuthorizationOptions! options) -> void Microsoft.Identity.Web.RequireScopeOptions.RequireScopeOptions() -> void @@ -88,10 +92,16 @@ static Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException.Pro static Microsoft.Identity.Web.Extensions.ContainsAny(this string! searchFor, params string![]! stringCollection) -> bool static Microsoft.Identity.Web.IncrementalConsentAndConditionalAccessHelper.BuildAuthenticationProperties(string![]? scopes, Microsoft.Identity.Client.MsalUiRequiredException! ex, System.Security.Claims.ClaimsPrincipal! user, string? userflow = null) -> Microsoft.AspNetCore.Authentication.AuthenticationProperties! static Microsoft.Identity.Web.IncrementalConsentAndConditionalAccessHelper.CanBeSolvedByReSignInOfUser(Microsoft.Identity.Client.MsalUiRequiredException! ex) -> bool +static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.BuildAuthority(Microsoft.Identity.Abstractions.MicrosoftIdentityApplicationOptions! options) -> string! +static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.ChainTokenStorageHandler(System.Func? existingHandler) -> System.Func! +static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.ConfigureAudienceValidation(Microsoft.IdentityModel.Tokens.TokenValidationParameters! validationParameters, string? clientId, bool isB2C) -> void +static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.ConfigureIssuerValidation(Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions! options, System.IServiceProvider! serviceProvider) -> void +static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.InitializeJwtBearerEvents(Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions! options) -> void static Microsoft.Identity.Web.MergedOptionsValidation.Validate(Microsoft.Identity.Web.MergedOptions! options) -> void static Microsoft.Identity.Web.MicrosoftIdentityAuthenticationBaseMessageHandler.CreateProofOfPossessionConfiguration(Microsoft.Identity.Web.MicrosoftIdentityAuthenticationMessageHandlerOptions! options, System.Uri! apiUri, System.Net.Http.HttpMethod! method) -> void static Microsoft.Identity.Web.MicrosoftIdentityWebApiAuthenticationBuilder.CallsWebApiImplementation(Microsoft.Extensions.DependencyInjection.IServiceCollection! services, string! jwtBearerAuthenticationScheme, System.Action! configureConfidentialClientApplicationOptions, Microsoft.Extensions.Configuration.IConfigurationSection? configurationSection = null) -> void static Microsoft.Identity.Web.MicrosoftIdentityWebApiAuthenticationBuilderExtensions.ChainOnTokenValidatedEventForClaimsValidation(Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerEvents! events, string! jwtBearerScheme) -> void static Microsoft.Identity.Web.MicrosoftIdentityWebAppAuthenticationBuilder.WebAppCallsWebApiImplementation(Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Collections.Generic.IEnumerable? initialScopes, System.Action? configureMicrosoftIdentityOptions, string! openIdConnectScheme, System.Action? configureConfidentialClientApplicationOptions) -> void static Microsoft.Identity.Web.MicrosoftIdentityWebAppAuthenticationBuilderExtensions.PopulateOpenIdOptionsFromMergedOptions(Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions! options, Microsoft.Identity.Web.MergedOptions! mergedOptions) -> void +static Microsoft.Identity.Web.PostConfigureOptions.MicrosoftIdentityJwtBearerOptionsPostConfigurator.ValidateRequiredOptions(Microsoft.Identity.Abstractions.MicrosoftIdentityApplicationOptions! options) -> void static Microsoft.Identity.Web.TempDataLoginErrorAccessor.Create(Microsoft.AspNetCore.Mvc.ViewFeatures.ITempDataDictionaryFactory? factory, bool isDevelopment) -> Microsoft.Identity.Web.ILoginErrorAccessor! diff --git a/src/Microsoft.Identity.Web/PublicAPI/net10.0/PublicAPI.Shipped.txt b/src/Microsoft.Identity.Web/PublicAPI/net10.0/PublicAPI.Shipped.txt index 3f56dec7b..4b06b778f 100644 --- a/src/Microsoft.Identity.Web/PublicAPI/net10.0/PublicAPI.Shipped.txt +++ b/src/Microsoft.Identity.Web/PublicAPI/net10.0/PublicAPI.Shipped.txt @@ -197,8 +197,8 @@ override Microsoft.Identity.Web.TokenAcquisitionAppTokenCredential.GetToken(Azur override Microsoft.Identity.Web.TokenAcquisitionAppTokenCredential.GetTokenAsync(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask override Microsoft.Identity.Web.TokenAcquisitionTokenCredential.GetToken(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken) -> Azure.Core.AccessToken override Microsoft.Identity.Web.TokenAcquisitionTokenCredential.GetTokenAsync(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask -override Microsoft.Identity.Web.TokenCacheProviders.Session.MsalSessionTokenCacheProvider.ReadCacheBytesAsync(string! cacheKey) -> System.Threading.Tasks.Task! override Microsoft.Identity.Web.TokenCacheProviders.Session.MsalSessionTokenCacheProvider.ReadCacheBytesAsync(string! cacheKey, Microsoft.Identity.Web.TokenCacheProviders.CacheSerializerHints! cacheSerializerHints) -> System.Threading.Tasks.Task! +override Microsoft.Identity.Web.TokenCacheProviders.Session.MsalSessionTokenCacheProvider.ReadCacheBytesAsync(string! cacheKey) -> System.Threading.Tasks.Task! override Microsoft.Identity.Web.TokenCacheProviders.Session.MsalSessionTokenCacheProvider.RemoveKeyAsync(string! cacheKey) -> System.Threading.Tasks.Task! override Microsoft.Identity.Web.TokenCacheProviders.Session.MsalSessionTokenCacheProvider.WriteCacheBytesAsync(string! cacheKey, byte[]! bytes) -> System.Threading.Tasks.Task! static Microsoft.Identity.Web.AccountExtensions.ToClaimsPrincipal(this Microsoft.Identity.Client.IAccount! account) -> System.Security.Claims.ClaimsPrincipal! @@ -210,8 +210,8 @@ static Microsoft.Identity.Web.ClaimsPrincipalFactory.FromHomeTenantIdAndHomeObje static Microsoft.Identity.Web.ClaimsPrincipalFactory.FromTenantIdAndObjectId(string! tenantId, string! objectId) -> System.Security.Claims.ClaimsPrincipal! static Microsoft.Identity.Web.ClaimsPrincipalFactory.FromUsernamePassword(string! username, string! password) -> System.Security.Claims.ClaimsPrincipal! static Microsoft.Identity.Web.CookiePolicyOptionsExtensions.DisallowsSameSiteNone(string! userAgent) -> bool -static Microsoft.Identity.Web.CookiePolicyOptionsExtensions.HandleSameSiteCookieCompatibility(this Microsoft.AspNetCore.Builder.CookiePolicyOptions! options) -> Microsoft.AspNetCore.Builder.CookiePolicyOptions! static Microsoft.Identity.Web.CookiePolicyOptionsExtensions.HandleSameSiteCookieCompatibility(this Microsoft.AspNetCore.Builder.CookiePolicyOptions! options, System.Func! disallowsSameSiteNone) -> Microsoft.AspNetCore.Builder.CookiePolicyOptions! +static Microsoft.Identity.Web.CookiePolicyOptionsExtensions.HandleSameSiteCookieCompatibility(this Microsoft.AspNetCore.Builder.CookiePolicyOptions! options) -> Microsoft.AspNetCore.Builder.CookiePolicyOptions! static Microsoft.Identity.Web.DownstreamWebApiExtensions.AddDownstreamWebApi(this Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder! builder, string! serviceName, Microsoft.Extensions.Configuration.IConfiguration! configuration) -> Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder! static Microsoft.Identity.Web.DownstreamWebApiExtensions.AddDownstreamWebApi(this Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder! builder, string! serviceName, System.Action! configureOptions) -> Microsoft.Identity.Web.MicrosoftIdentityAppCallsWebApiAuthenticationBuilder! static Microsoft.Identity.Web.DownstreamWebApiGenericExtensions.CallWebApiForUserAsync(this Microsoft.Identity.Web.IDownstreamWebApi! downstreamWebApi, string! serviceName, System.Action? downstreamWebApiOptionsOverride = null, System.Security.Claims.ClaimsPrincipal? user = null, string? authenticationScheme = null) -> System.Threading.Tasks.Task! @@ -230,6 +230,7 @@ static Microsoft.Identity.Web.MicrosoftIdentityBlazorServiceCollectionExtensions static Microsoft.Identity.Web.MicrosoftIdentityWebApiAuthenticationBuilderExtensions.AddMicrosoftIdentityWebApi(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder! builder, Microsoft.Extensions.Configuration.IConfiguration! configuration, string! configSectionName = "AzureAd", string! jwtBearerScheme = "Bearer", bool subscribeToJwtBearerMiddlewareDiagnosticsEvents = false) -> Microsoft.Identity.Web.MicrosoftIdentityWebApiAuthenticationBuilderWithConfiguration! static Microsoft.Identity.Web.MicrosoftIdentityWebApiAuthenticationBuilderExtensions.AddMicrosoftIdentityWebApi(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder! builder, Microsoft.Extensions.Configuration.IConfigurationSection! configurationSection, string! jwtBearerScheme = "Bearer", bool subscribeToJwtBearerMiddlewareDiagnosticsEvents = false) -> Microsoft.Identity.Web.MicrosoftIdentityWebApiAuthenticationBuilderWithConfiguration! static Microsoft.Identity.Web.MicrosoftIdentityWebApiAuthenticationBuilderExtensions.AddMicrosoftIdentityWebApi(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder! builder, System.Action! configureJwtBearerOptions, System.Action! configureMicrosoftIdentityOptions, string! jwtBearerScheme = "Bearer", bool subscribeToJwtBearerMiddlewareDiagnosticsEvents = false) -> Microsoft.Identity.Web.MicrosoftIdentityWebApiAuthenticationBuilder! +static Microsoft.Identity.Web.MicrosoftIdentityWebApiAuthenticationBuilderExtensions.AddMicrosoftIdentityWebApiAot(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder! builder, System.Action! configureOptions, string! jwtBearerScheme, System.Action? configureJwtBearerOptions) -> Microsoft.AspNetCore.Authentication.AuthenticationBuilder! static Microsoft.Identity.Web.MicrosoftIdentityWebApiServiceCollectionExtensions.AddMicrosoftIdentityWebApiAuthentication(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfiguration! configuration, string! configSectionName = "AzureAd", string! jwtBearerScheme = "Bearer", bool subscribeToJwtBearerMiddlewareDiagnosticsEvents = false) -> Microsoft.Identity.Web.MicrosoftIdentityWebApiAuthenticationBuilderWithConfiguration! static Microsoft.Identity.Web.MicrosoftIdentityWebAppAuthenticationBuilderExtensions.AddMicrosoftIdentityWebApp(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder! builder, Microsoft.Extensions.Configuration.IConfiguration! configuration, string! configSectionName = "AzureAd", string! openIdConnectScheme = "OpenIdConnect", string? cookieScheme = "Cookies", bool subscribeToOpenIdConnectMiddlewareDiagnosticsEvents = false, string? displayName = null) -> Microsoft.Identity.Web.MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration! static Microsoft.Identity.Web.MicrosoftIdentityWebAppAuthenticationBuilderExtensions.AddMicrosoftIdentityWebApp(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder! builder, Microsoft.Extensions.Configuration.IConfigurationSection! configurationSection, string! openIdConnectScheme = "OpenIdConnect", string? cookieScheme = "Cookies", bool subscribeToOpenIdConnectMiddlewareDiagnosticsEvents = false, string? displayName = null) -> Microsoft.Identity.Web.MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration! diff --git a/src/Microsoft.Identity.Web/PublicAPI/net462/InternalAPI.Shipped.txt b/src/Microsoft.Identity.Web/PublicAPI/net462/InternalAPI.Shipped.txt index 7dc5c5811..d625c2b96 100644 --- a/src/Microsoft.Identity.Web/PublicAPI/net462/InternalAPI.Shipped.txt +++ b/src/Microsoft.Identity.Web/PublicAPI/net462/InternalAPI.Shipped.txt @@ -1 +1,7 @@ #nullable enable +Microsoft.Identity.Web.Internal.IdentityOptionsHelpers +Microsoft.Identity.Web.PostConfigureOptions.MicrosoftIdentityJwtBearerOptionsPostConfigurator +Microsoft.Identity.Web.PostConfigureOptions.MicrosoftIdentityJwtBearerOptionsPostConfigurator.MicrosoftIdentityJwtBearerOptionsPostConfigurator(Microsoft.Extensions.Options.IOptionsMonitor! appOptionsMonitor, System.IServiceProvider! serviceProvider) -> void +Microsoft.Identity.Web.PostConfigureOptions.MicrosoftIdentityJwtBearerOptionsPostConfigurator.PostConfigure(string? name, Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions! options) -> void +static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.BuildAuthority(Microsoft.Identity.Abstractions.MicrosoftIdentityApplicationOptions! options) -> string! +static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.ChainTokenStorageHandler(System.Func? existingHandler) -> System.Func! diff --git a/src/Microsoft.Identity.Web/PublicAPI/net472/InternalAPI.Shipped.txt b/src/Microsoft.Identity.Web/PublicAPI/net472/InternalAPI.Shipped.txt index 7dc5c5811..d625c2b96 100644 --- a/src/Microsoft.Identity.Web/PublicAPI/net472/InternalAPI.Shipped.txt +++ b/src/Microsoft.Identity.Web/PublicAPI/net472/InternalAPI.Shipped.txt @@ -1 +1,7 @@ #nullable enable +Microsoft.Identity.Web.Internal.IdentityOptionsHelpers +Microsoft.Identity.Web.PostConfigureOptions.MicrosoftIdentityJwtBearerOptionsPostConfigurator +Microsoft.Identity.Web.PostConfigureOptions.MicrosoftIdentityJwtBearerOptionsPostConfigurator.MicrosoftIdentityJwtBearerOptionsPostConfigurator(Microsoft.Extensions.Options.IOptionsMonitor! appOptionsMonitor, System.IServiceProvider! serviceProvider) -> void +Microsoft.Identity.Web.PostConfigureOptions.MicrosoftIdentityJwtBearerOptionsPostConfigurator.PostConfigure(string? name, Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions! options) -> void +static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.BuildAuthority(Microsoft.Identity.Abstractions.MicrosoftIdentityApplicationOptions! options) -> string! +static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.ChainTokenStorageHandler(System.Func? existingHandler) -> System.Func! diff --git a/src/Microsoft.Identity.Web/PublicAPI/net8.0/InternalAPI.Shipped.txt b/src/Microsoft.Identity.Web/PublicAPI/net8.0/InternalAPI.Shipped.txt index 4d4a6507f..2755559ec 100644 --- a/src/Microsoft.Identity.Web/PublicAPI/net8.0/InternalAPI.Shipped.txt +++ b/src/Microsoft.Identity.Web/PublicAPI/net8.0/InternalAPI.Shipped.txt @@ -34,6 +34,7 @@ Microsoft.Identity.Web.DefaultMicrosoftIdentityAuthenticationDelegatingHandlerFa Microsoft.Identity.Web.DownstreamWebApi.MergeOptions(string! optionsInstanceName, System.Action? calledApiOptionsOverride) -> Microsoft.Identity.Web.DownstreamWebApiOptions! Microsoft.Identity.Web.Extensions Microsoft.Identity.Web.IncrementalConsentAndConditionalAccessHelper +Microsoft.Identity.Web.Internal.IdentityOptionsHelpers Microsoft.Identity.Web.MergedOptionsValidation Microsoft.Identity.Web.MergedOptionsValidation.MergedOptionsValidation() -> void Microsoft.Identity.Web.MicrosoftIdentityConsentAndConditionalAccessHandler.NavigationManager.get -> Microsoft.AspNetCore.Components.NavigationManager! @@ -47,6 +48,9 @@ Microsoft.Identity.Web.MicrosoftIdentityWebApiAuthenticationBuilder.MicrosoftIde Microsoft.Identity.Web.MicrosoftIdentityWebApiAuthenticationBuilderWithConfiguration.MicrosoftIdentityWebApiAuthenticationBuilderWithConfiguration(Microsoft.Extensions.DependencyInjection.IServiceCollection! services, string! jwtBearerAuthenticationScheme, System.Action! configureJwtBearerOptions, System.Action! configureMicrosoftIdentityOptions, Microsoft.Extensions.Configuration.IConfigurationSection? configurationSection) -> void Microsoft.Identity.Web.MicrosoftIdentityWebAppAuthenticationBuilder.MicrosoftIdentityWebAppAuthenticationBuilder(Microsoft.Extensions.DependencyInjection.IServiceCollection! services, string! openIdConnectScheme, System.Action! configureMicrosoftIdentityOptions, Microsoft.Extensions.Configuration.IConfigurationSection? configurationSection) -> void Microsoft.Identity.Web.MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration.MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration(Microsoft.Extensions.DependencyInjection.IServiceCollection! services, string! openIdConnectScheme, System.Action! configureMicrosoftIdentityOptions, Microsoft.Extensions.Configuration.IConfigurationSection! configurationSection) -> void +Microsoft.Identity.Web.PostConfigureOptions.MicrosoftIdentityJwtBearerOptionsPostConfigurator +Microsoft.Identity.Web.PostConfigureOptions.MicrosoftIdentityJwtBearerOptionsPostConfigurator.MicrosoftIdentityJwtBearerOptionsPostConfigurator(Microsoft.Extensions.Options.IOptionsMonitor! appOptionsMonitor, System.IServiceProvider! serviceProvider) -> void +Microsoft.Identity.Web.PostConfigureOptions.MicrosoftIdentityJwtBearerOptionsPostConfigurator.PostConfigure(string? name, Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions! options) -> void Microsoft.Identity.Web.RequireScopeOptions Microsoft.Identity.Web.RequireScopeOptions.PostConfigure(string? name, Microsoft.AspNetCore.Authorization.AuthorizationOptions! options) -> void Microsoft.Identity.Web.RequireScopeOptions.RequireScopeOptions() -> void @@ -88,6 +92,11 @@ static Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException.Pro static Microsoft.Identity.Web.Extensions.ContainsAny(this string! searchFor, params string![]! stringCollection) -> bool static Microsoft.Identity.Web.IncrementalConsentAndConditionalAccessHelper.BuildAuthenticationProperties(string![]? scopes, Microsoft.Identity.Client.MsalUiRequiredException! ex, System.Security.Claims.ClaimsPrincipal! user, string? userflow = null) -> Microsoft.AspNetCore.Authentication.AuthenticationProperties! static Microsoft.Identity.Web.IncrementalConsentAndConditionalAccessHelper.CanBeSolvedByReSignInOfUser(Microsoft.Identity.Client.MsalUiRequiredException! ex) -> bool +static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.BuildAuthority(Microsoft.Identity.Abstractions.MicrosoftIdentityApplicationOptions! options) -> string! +static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.ChainTokenStorageHandler(System.Func? existingHandler) -> System.Func! +static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.ConfigureAudienceValidation(Microsoft.IdentityModel.Tokens.TokenValidationParameters! validationParameters, string? clientId, bool isB2C) -> void +static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.ConfigureIssuerValidation(Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions! options, System.IServiceProvider! serviceProvider) -> void +static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.InitializeJwtBearerEvents(Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions! options) -> void static Microsoft.Identity.Web.MergedOptionsValidation.Validate(Microsoft.Identity.Web.MergedOptions! options) -> void static Microsoft.Identity.Web.MicrosoftIdentityAuthenticationBaseMessageHandler.CreateProofOfPossessionConfiguration(Microsoft.Identity.Web.MicrosoftIdentityAuthenticationMessageHandlerOptions! options, System.Uri! apiUri, System.Net.Http.HttpMethod! method) -> void static Microsoft.Identity.Web.MicrosoftIdentityWebApiAuthenticationBuilder.CallsWebApiImplementation(Microsoft.Extensions.DependencyInjection.IServiceCollection! services, string! jwtBearerAuthenticationScheme, System.Action! configureConfidentialClientApplicationOptions, Microsoft.Extensions.Configuration.IConfigurationSection? configurationSection = null) -> void diff --git a/src/Microsoft.Identity.Web/PublicAPI/net9.0/InternalAPI.Shipped.txt b/src/Microsoft.Identity.Web/PublicAPI/net9.0/InternalAPI.Shipped.txt index 4d4a6507f..2755559ec 100644 --- a/src/Microsoft.Identity.Web/PublicAPI/net9.0/InternalAPI.Shipped.txt +++ b/src/Microsoft.Identity.Web/PublicAPI/net9.0/InternalAPI.Shipped.txt @@ -34,6 +34,7 @@ Microsoft.Identity.Web.DefaultMicrosoftIdentityAuthenticationDelegatingHandlerFa Microsoft.Identity.Web.DownstreamWebApi.MergeOptions(string! optionsInstanceName, System.Action? calledApiOptionsOverride) -> Microsoft.Identity.Web.DownstreamWebApiOptions! Microsoft.Identity.Web.Extensions Microsoft.Identity.Web.IncrementalConsentAndConditionalAccessHelper +Microsoft.Identity.Web.Internal.IdentityOptionsHelpers Microsoft.Identity.Web.MergedOptionsValidation Microsoft.Identity.Web.MergedOptionsValidation.MergedOptionsValidation() -> void Microsoft.Identity.Web.MicrosoftIdentityConsentAndConditionalAccessHandler.NavigationManager.get -> Microsoft.AspNetCore.Components.NavigationManager! @@ -47,6 +48,9 @@ Microsoft.Identity.Web.MicrosoftIdentityWebApiAuthenticationBuilder.MicrosoftIde Microsoft.Identity.Web.MicrosoftIdentityWebApiAuthenticationBuilderWithConfiguration.MicrosoftIdentityWebApiAuthenticationBuilderWithConfiguration(Microsoft.Extensions.DependencyInjection.IServiceCollection! services, string! jwtBearerAuthenticationScheme, System.Action! configureJwtBearerOptions, System.Action! configureMicrosoftIdentityOptions, Microsoft.Extensions.Configuration.IConfigurationSection? configurationSection) -> void Microsoft.Identity.Web.MicrosoftIdentityWebAppAuthenticationBuilder.MicrosoftIdentityWebAppAuthenticationBuilder(Microsoft.Extensions.DependencyInjection.IServiceCollection! services, string! openIdConnectScheme, System.Action! configureMicrosoftIdentityOptions, Microsoft.Extensions.Configuration.IConfigurationSection? configurationSection) -> void Microsoft.Identity.Web.MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration.MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration(Microsoft.Extensions.DependencyInjection.IServiceCollection! services, string! openIdConnectScheme, System.Action! configureMicrosoftIdentityOptions, Microsoft.Extensions.Configuration.IConfigurationSection! configurationSection) -> void +Microsoft.Identity.Web.PostConfigureOptions.MicrosoftIdentityJwtBearerOptionsPostConfigurator +Microsoft.Identity.Web.PostConfigureOptions.MicrosoftIdentityJwtBearerOptionsPostConfigurator.MicrosoftIdentityJwtBearerOptionsPostConfigurator(Microsoft.Extensions.Options.IOptionsMonitor! appOptionsMonitor, System.IServiceProvider! serviceProvider) -> void +Microsoft.Identity.Web.PostConfigureOptions.MicrosoftIdentityJwtBearerOptionsPostConfigurator.PostConfigure(string? name, Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions! options) -> void Microsoft.Identity.Web.RequireScopeOptions Microsoft.Identity.Web.RequireScopeOptions.PostConfigure(string? name, Microsoft.AspNetCore.Authorization.AuthorizationOptions! options) -> void Microsoft.Identity.Web.RequireScopeOptions.RequireScopeOptions() -> void @@ -88,6 +92,11 @@ static Microsoft.Identity.Web.CustomSignedAssertionProviderNotFoundException.Pro static Microsoft.Identity.Web.Extensions.ContainsAny(this string! searchFor, params string![]! stringCollection) -> bool static Microsoft.Identity.Web.IncrementalConsentAndConditionalAccessHelper.BuildAuthenticationProperties(string![]? scopes, Microsoft.Identity.Client.MsalUiRequiredException! ex, System.Security.Claims.ClaimsPrincipal! user, string? userflow = null) -> Microsoft.AspNetCore.Authentication.AuthenticationProperties! static Microsoft.Identity.Web.IncrementalConsentAndConditionalAccessHelper.CanBeSolvedByReSignInOfUser(Microsoft.Identity.Client.MsalUiRequiredException! ex) -> bool +static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.BuildAuthority(Microsoft.Identity.Abstractions.MicrosoftIdentityApplicationOptions! options) -> string! +static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.ChainTokenStorageHandler(System.Func? existingHandler) -> System.Func! +static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.ConfigureAudienceValidation(Microsoft.IdentityModel.Tokens.TokenValidationParameters! validationParameters, string? clientId, bool isB2C) -> void +static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.ConfigureIssuerValidation(Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions! options, System.IServiceProvider! serviceProvider) -> void +static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.InitializeJwtBearerEvents(Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions! options) -> void static Microsoft.Identity.Web.MergedOptionsValidation.Validate(Microsoft.Identity.Web.MergedOptions! options) -> void static Microsoft.Identity.Web.MicrosoftIdentityAuthenticationBaseMessageHandler.CreateProofOfPossessionConfiguration(Microsoft.Identity.Web.MicrosoftIdentityAuthenticationMessageHandlerOptions! options, System.Uri! apiUri, System.Net.Http.HttpMethod! method) -> void static Microsoft.Identity.Web.MicrosoftIdentityWebApiAuthenticationBuilder.CallsWebApiImplementation(Microsoft.Extensions.DependencyInjection.IServiceCollection! services, string! jwtBearerAuthenticationScheme, System.Action! configureConfidentialClientApplicationOptions, Microsoft.Extensions.Configuration.IConfigurationSection? configurationSection = null) -> void diff --git a/src/Microsoft.Identity.Web/PublicAPI/netstandard2.0/InternalAPI.Shipped.txt b/src/Microsoft.Identity.Web/PublicAPI/netstandard2.0/InternalAPI.Shipped.txt index 7dc5c5811..d625c2b96 100644 --- a/src/Microsoft.Identity.Web/PublicAPI/netstandard2.0/InternalAPI.Shipped.txt +++ b/src/Microsoft.Identity.Web/PublicAPI/netstandard2.0/InternalAPI.Shipped.txt @@ -1 +1,7 @@ #nullable enable +Microsoft.Identity.Web.Internal.IdentityOptionsHelpers +Microsoft.Identity.Web.PostConfigureOptions.MicrosoftIdentityJwtBearerOptionsPostConfigurator +Microsoft.Identity.Web.PostConfigureOptions.MicrosoftIdentityJwtBearerOptionsPostConfigurator.MicrosoftIdentityJwtBearerOptionsPostConfigurator(Microsoft.Extensions.Options.IOptionsMonitor! appOptionsMonitor, System.IServiceProvider! serviceProvider) -> void +Microsoft.Identity.Web.PostConfigureOptions.MicrosoftIdentityJwtBearerOptionsPostConfigurator.PostConfigure(string? name, Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions! options) -> void +static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.BuildAuthority(Microsoft.Identity.Abstractions.MicrosoftIdentityApplicationOptions! options) -> string! +static Microsoft.Identity.Web.Internal.IdentityOptionsHelpers.ChainTokenStorageHandler(System.Func? existingHandler) -> System.Func! diff --git a/src/Microsoft.Identity.Web/Resource/MicrosoftIdentityIssuerValidatorFactory.cs b/src/Microsoft.Identity.Web/Resource/MicrosoftIdentityIssuerValidatorFactory.cs index 25b1491dc..4e8d680e1 100644 --- a/src/Microsoft.Identity.Web/Resource/MicrosoftIdentityIssuerValidatorFactory.cs +++ b/src/Microsoft.Identity.Web/Resource/MicrosoftIdentityIssuerValidatorFactory.cs @@ -17,7 +17,7 @@ public class MicrosoftIdentityIssuerValidatorFactory /// Initializes a new instance of the class. /// /// Options passed-in to create the AadIssuerValidator object. - /// HttpClientFactory. + /// HttpClientFactoryForTests. public MicrosoftIdentityIssuerValidatorFactory( IOptions aadIssuerValidatorOptions, IHttpClientFactory httpClientFactory) diff --git a/src/Microsoft.Identity.Web/Resource/OpenIdConnectMiddlewareDiagnostics.cs b/src/Microsoft.Identity.Web/Resource/OpenIdConnectMiddlewareDiagnostics.cs index 5b1866849..bcbe461e8 100644 --- a/src/Microsoft.Identity.Web/Resource/OpenIdConnectMiddlewareDiagnostics.cs +++ b/src/Microsoft.Identity.Web/Resource/OpenIdConnectMiddlewareDiagnostics.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication.OpenIdConnect; @@ -137,7 +138,7 @@ private async Task OnRedirectToIdentityProviderAsync(RedirectContext context) private void DisplayProtocolMessage(OpenIdConnectMessage message) { - foreach (var property in message.GetType().GetProperties()) + foreach (var property in typeof(OpenIdConnectMessage).GetProperties()) { object? value = property.GetValue(message); if (value != null) diff --git a/src/Microsoft.Identity.Web/TokenCacheProviders/Session/SessionTokenCacheProviderExtension.cs b/src/Microsoft.Identity.Web/TokenCacheProviders/Session/SessionTokenCacheProviderExtension.cs index 2a2881255..645237b0c 100644 --- a/src/Microsoft.Identity.Web/TokenCacheProviders/Session/SessionTokenCacheProviderExtension.cs +++ b/src/Microsoft.Identity.Web/TokenCacheProviders/Session/SessionTokenCacheProviderExtension.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -36,6 +37,8 @@ public static class SessionTokenCacheProviderExtension /// /// The services collection to add to. /// The service collection. + [RequiresUnreferencedCode("Session State middleware does not currently support trimming or native AOT. https://aka.ms/aspnet/trimming")] + [RequiresDynamicCode("Session State middleware does not currently support trimming or native AOT. https://aka.ms/aspnet/trimming")] public static IServiceCollection AddSessionAppTokenCache(this IServiceCollection services) { return CreateSessionTokenCache(services); @@ -63,11 +66,15 @@ public static IServiceCollection AddSessionAppTokenCache(this IServiceCollection /// /// The services collection to add to. /// The service collection. + [RequiresUnreferencedCode("Session State middleware does not currently support trimming or native AOT. https://aka.ms/aspnet/trimming")] + [RequiresDynamicCode("Session State middleware does not currently support trimming or native AOT. https://aka.ms/aspnet/trimming")] public static IServiceCollection AddSessionPerUserTokenCache(this IServiceCollection services) { return CreateSessionTokenCache(services); } + [RequiresUnreferencedCode("Session State middleware does not currently support trimming or native AOT. https://aka.ms/aspnet/trimming")] + [RequiresDynamicCode("Session State middleware does not currently support trimming or native AOT. https://aka.ms/aspnet/trimming")] private static IServiceCollection CreateSessionTokenCache(IServiceCollection services) { _ = Throws.IfNull(services); diff --git a/src/Microsoft.Identity.Web/WebApiExtensions/MicrosoftIdentityWebApiAuthenticationBuilder.cs b/src/Microsoft.Identity.Web/WebApiExtensions/MicrosoftIdentityWebApiAuthenticationBuilder.cs index 37a2cd710..f93ee1cf5 100644 --- a/src/Microsoft.Identity.Web/WebApiExtensions/MicrosoftIdentityWebApiAuthenticationBuilder.cs +++ b/src/Microsoft.Identity.Web/WebApiExtensions/MicrosoftIdentityWebApiAuthenticationBuilder.cs @@ -28,9 +28,8 @@ public class MicrosoftIdentityWebApiAuthenticationBuilder : MicrosoftIdentityBas /// the Microsoft identity options. /// Configuration section from which to /// get parameters. -#if NET6_0_OR_GREATER && !NET8_0_OR_GREATER [RequiresUnreferencedCode("Calls Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder.MicrosoftIdentityBaseAuthenticationBuilder(IServiceCollection, IConfigurationSection).")] -#endif + [RequiresDynamicCode("Calls Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder.MicrosoftIdentityBaseAuthenticationBuilder(IServiceCollection, IConfigurationSection).")] internal MicrosoftIdentityWebApiAuthenticationBuilder( IServiceCollection services, string jwtBearerAuthenticationScheme, @@ -53,9 +52,8 @@ internal MicrosoftIdentityWebApiAuthenticationBuilder( /// /// The action to configure . /// The authentication builder to chain. -#if NET6_0_OR_GREATER && !NET8_0_OR_GREATER [RequiresUnreferencedCode("Calls Microsoft.Identity.Web.Internal.WebApiBuilders.EnableTokenAcquisition(IServiceCollection, string, Action, IConfigurationSection).")] -#endif + [RequiresDynamicCode("Calls Microsoft.Identity.Web.Internal.WebApiBuilders.EnableTokenAcquisition(IServiceCollection, string, Action, IConfigurationSection).")] public MicrosoftIdentityAppCallsWebApiAuthenticationBuilder EnableTokenAcquisitionToCallDownstreamApi( Action configureConfidentialClientApplicationOptions) { @@ -72,9 +70,8 @@ public MicrosoftIdentityAppCallsWebApiAuthenticationBuilder EnableTokenAcquisiti ConfigurationSection); } -#if NET6_0_OR_GREATER && !NET8_0_OR_GREATER [RequiresUnreferencedCode("Calls Microsoft.Identity.Web.Internal.WebApiBuilders.EnableTokenAcquisition(Action, String, IServiceCollection, IConfigurationSection).")] -#endif + [RequiresDynamicCode("Calls Microsoft.Identity.Web.Internal.WebApiBuilders.EnableTokenAcquisition(Action, String, IServiceCollection, IConfigurationSection).")] internal static void CallsWebApiImplementation( IServiceCollection services, string jwtBearerAuthenticationScheme, @@ -96,14 +93,8 @@ internal static void CallsWebApiImplementation( { options.Events ??= new JwtBearerEvents(); - var onTokenValidatedHandler = options.Events.OnTokenValidated; - - options.Events.OnTokenValidated = async context => - { - // Only pass through a token if it is of an expected type - context.HttpContext.StoreTokenUsedToCallWebAPI(context.SecurityToken is JwtSecurityToken or JsonWebToken ? context.SecurityToken : null); - await onTokenValidatedHandler(context).ConfigureAwait(false); - }; + // Chain token storage handler using shared helper + options.Events.OnTokenValidated = Internal.IdentityOptionsHelpers.ChainTokenStorageHandler(options.Events.OnTokenValidated); }); } } diff --git a/src/Microsoft.Identity.Web/WebApiExtensions/MicrosoftIdentityWebApiAuthenticationBuilderExtensions.Aot.cs b/src/Microsoft.Identity.Web/WebApiExtensions/MicrosoftIdentityWebApiAuthenticationBuilderExtensions.Aot.cs new file mode 100644 index 000000000..ed3092b17 --- /dev/null +++ b/src/Microsoft.Identity.Web/WebApiExtensions/MicrosoftIdentityWebApiAuthenticationBuilderExtensions.Aot.cs @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#if NET10_0_OR_GREATER + +using System; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Web.Internal; +using Microsoft.Identity.Web.PostConfigureOptions; +using Microsoft.Identity.Web.Resource; + +namespace Microsoft.Identity.Web +{ + /// + /// Extensions for for startup initialization of web APIs (AOT-compatible). + /// + public static partial class MicrosoftIdentityWebApiAuthenticationBuilderExtensions + { + /// + /// Protects the web API with Microsoft identity platform (AOT-compatible). + /// This is the AOT-safe alternative to + /// and does not rely on reflection-based configuration binding. + /// + /// The to which to add this configuration. + /// The action to configure . + /// The JWT bearer scheme name to be used. By default it uses "Bearer". + /// Optional action to configure . + /// The authentication builder to chain. + /// + /// + /// This method takes an delegate that + /// the caller uses to bind configuration values. + /// + /// + /// To get AOT-safe configuration binding, enable the configuration binding source generator + /// in your project file: + /// + /// + /// <EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator> + /// + /// + /// The source generator produces compile-time binding code for + /// + /// calls, eliminating the need for reflection at runtime. + /// + /// + /// + /// The following example shows how to protect a web API using AOT-compatible configuration binding: + /// + /// var azureAdSection = builder.Configuration.GetSection("AzureAd"); + /// + /// builder.Services + /// .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + /// .AddMicrosoftIdentityWebApiAot( + /// options => azureAdSection.Bind(options), + /// JwtBearerDefaults.AuthenticationScheme, + /// configureJwtBearerOptions: null); + /// + /// + public static AuthenticationBuilder AddMicrosoftIdentityWebApiAot( + this AuthenticationBuilder builder, + Action configureOptions, + string jwtBearerScheme, + Action? configureJwtBearerOptions) + { + _ = Throws.IfNull(builder); + _ = Throws.IfNull(configureOptions); + + // Register MicrosoftIdentityApplicationOptions + builder.Services.Configure(jwtBearerScheme, configureOptions); + + // Add JWT Bearer with optional custom configuration + if (configureJwtBearerOptions != null) + { + builder.AddJwtBearer(jwtBearerScheme, configureJwtBearerOptions); + } + else + { + builder.AddJwtBearer(jwtBearerScheme); + } + + // Set Authority during Configure phase so that ASP.NET's built-in JwtBearerPostConfigureOptions can create the ConfigurationManager from it. + builder.Services.AddOptions(jwtBearerScheme) + .Configure>((jwtOptions, appOptionsMonitor) => + { + if (!string.IsNullOrEmpty(jwtOptions.Authority)) + { + return; + } + + var appOptions = appOptionsMonitor.Get(jwtBearerScheme); + if (string.IsNullOrEmpty(appOptions.ClientId)) + { + // Skip if not configured via AOT path (no ClientId means not configured) + return; + } + + if (!string.IsNullOrEmpty(appOptions.Authority)) + { + var authority = AuthorityHelpers.BuildCiamAuthorityIfNeeded(appOptions.Authority, out _); + jwtOptions.Authority = AuthorityHelpers.EnsureAuthorityIsV2(authority ?? appOptions.Authority); + } + else + { + jwtOptions.Authority = AuthorityHelpers.EnsureAuthorityIsV2( + IdentityOptionsHelpers.BuildAuthority(appOptions)); + } + }); + + // Register core services + builder.Services.AddSingleton(); + builder.Services.AddHttpContextAccessor(); + builder.Services.AddHttpClient(); + builder.Services.TryAddSingleton(); + builder.Services.AddRequiredScopeAuthorization(); + builder.Services.AddRequiredScopeOrAppPermissionAuthorization(); + + builder.Services.AddOptions(); + + // Register the post-configurator for AOT path + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, MicrosoftIdentityJwtBearerOptionsPostConfigurator>()); + + // Register the merger to bridge MicrosoftIdentityApplicationOptions to MergedOptions + // This ensures TokenAcquisition works without modification + if (!HasImplementationType(builder.Services, typeof(MicrosoftIdentityApplicationOptionsMerger))) + { + builder.Services.TryAddSingleton, + MicrosoftIdentityApplicationOptionsMerger>(); + } + + return builder; + } + } +} + +#endif diff --git a/src/Microsoft.Identity.Web/WebApiExtensions/MicrosoftIdentityWebApiAuthenticationBuilderExtensions.cs b/src/Microsoft.Identity.Web/WebApiExtensions/MicrosoftIdentityWebApiAuthenticationBuilderExtensions.cs index 2d7b92c00..0e1d0ac3e 100644 --- a/src/Microsoft.Identity.Web/WebApiExtensions/MicrosoftIdentityWebApiAuthenticationBuilderExtensions.cs +++ b/src/Microsoft.Identity.Web/WebApiExtensions/MicrosoftIdentityWebApiAuthenticationBuilderExtensions.cs @@ -14,6 +14,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; +using Microsoft.Identity.Web.Internal; using Microsoft.Identity.Web.Resource; using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Validators; @@ -23,7 +24,7 @@ namespace Microsoft.Identity.Web /// /// Extensions for for startup initialization of web APIs. /// - public static class MicrosoftIdentityWebApiAuthenticationBuilderExtensions + public static partial class MicrosoftIdentityWebApiAuthenticationBuilderExtensions { /// /// Protects the web API with Microsoft identity platform (formerly Azure AD v2.0). @@ -37,9 +38,8 @@ public static class MicrosoftIdentityWebApiAuthenticationBuilderExtensions /// Set to true if you want to debug, or just understand the JWT bearer events. /// /// The authentication builder to chain. -#if NET6_0_OR_GREATER && !NET8_0_OR_GREATER [RequiresUnreferencedCode("Calls Microsoft.Identity.Web.MicrosoftIdentityWebApiAuthneticationBuilderExtensions.AddMicrosoftIdentityWebApi(AuthenticationBuilder, IConfigurationSection, string, bool).")] -#endif + [RequiresDynamicCode("Calls Microsoft.Identity.Web.MicrosoftIdentityWebApiAuthneticationBuilderExtensions.AddMicrosoftIdentityWebApi(AuthenticationBuilder, IConfigurationSection, string, bool).")] public static MicrosoftIdentityWebApiAuthenticationBuilderWithConfiguration AddMicrosoftIdentityWebApi( this AuthenticationBuilder builder, IConfiguration configuration, @@ -69,9 +69,8 @@ public static MicrosoftIdentityWebApiAuthenticationBuilderWithConfiguration AddM /// Set to true if you want to debug, or just understand the JWT bearer events. /// /// The authentication builder to chain. -#if NET6_0_OR_GREATER && !NET8_0_OR_GREATER [RequiresUnreferencedCode("Microsoft.Extensions.Configuration.ConfigurationBinder.Bind(IConfiguration, Object).")] -#endif + [RequiresDynamicCode("Microsoft.Extensions.Configuration.ConfigurationBinder.Bind(IConfiguration, Object).")] public static MicrosoftIdentityWebApiAuthenticationBuilderWithConfiguration AddMicrosoftIdentityWebApi( this AuthenticationBuilder builder, IConfigurationSection configurationSection, @@ -80,7 +79,7 @@ public static MicrosoftIdentityWebApiAuthenticationBuilderWithConfiguration AddM { _ = Throws.IfNull(configurationSection); _ = Throws.IfNull(builder); - + AddMicrosoftIdentityWebApiImplementation( builder, options => configurationSection.Bind(options), @@ -105,9 +104,8 @@ public static MicrosoftIdentityWebApiAuthenticationBuilderWithConfiguration AddM /// /// Set to true if you want to debug, or just understand the JWT bearer events. /// The authentication builder to chain. -#if NET6_0_OR_GREATER && !NET8_0_OR_GREATER [RequiresUnreferencedCode("Microsoft.Identity.Web.MicrosoftIdentityWebApiAuthenticationBuilder.MicrosoftIdentityWebApiAuthenticationBuilder(IServiceCollection, String, Action, Action, IConfigurationSection).")] -#endif + [RequiresDynamicCode("Microsoft.Identity.Web.MicrosoftIdentityWebApiAuthenticationBuilder.MicrosoftIdentityWebApiAuthenticationBuilder(IServiceCollection, String, Action, Action, IConfigurationSection).")] public static MicrosoftIdentityWebApiAuthenticationBuilder AddMicrosoftIdentityWebApi( this AuthenticationBuilder builder, Action configureJwtBearerOptions, @@ -133,6 +131,8 @@ public static MicrosoftIdentityWebApiAuthenticationBuilder AddMicrosoftIdentityW null); } + [RequiresUnreferencedCode("Calls Microsoft.Extensions.Configuration.ConfigurationBinder.GetValue")] + [RequiresDynamicCode("Calls Microsoft.Extensions.Configuration.ConfigurationBinder.GetValue")] private static void AddMicrosoftIdentityWebApiImplementation( AuthenticationBuilder builder, Action configureJwtBearerOptions, @@ -204,20 +204,10 @@ private static void AddMicrosoftIdentityWebApiImplementation( mergedOptions); } - // If the developer registered an IssuerValidator, do not overwrite it - if (options.TokenValidationParameters.ValidateIssuer && options.TokenValidationParameters.IssuerValidator == null) - { - // Instead of using the default validation (validating against a single tenant, as we do in line of business apps), - // we inject our own multi-tenant validation logic (which even accepts both v1.0 and v2.0 tokens) - MicrosoftIdentityIssuerValidatorFactory microsoftIdentityIssuerValidatorFactory = - serviceProvider.GetRequiredService(); - - options.TokenValidationParameters.IssuerValidator = - microsoftIdentityIssuerValidatorFactory.GetAadIssuerValidator(options.Authority).Validate; - } + // Configure issuer validation + IdentityOptionsHelpers.ConfigureIssuerValidation(options, serviceProvider); // If you provide a token decryption certificate, it will be used to decrypt the token - // TODO use the credential loader if (mergedOptions.TokenDecryptionCredentials != null) { DefaultCertificateLoader.UserAssignedManagedIdentityClientId = mergedOptions.UserAssignedManagedIdentityClientId; @@ -226,17 +216,8 @@ private static void AddMicrosoftIdentityWebApiImplementation( options.TokenValidationParameters.TokenDecryptionKeys = keys; } - if (options.Events == null) - { - options.Events = new JwtBearerEvents(); - } - options.TokenValidationParameters.EnableAadSigningKeyIssuerValidation(); - options.Events.OnMessageReceived += async context => - { - context.Options.TokenValidationParameters.ConfigurationManager ??= options.ConfigurationManager as BaseConfigurationManager; - await Task.CompletedTask.ConfigureAwait(false); - }; + IdentityOptionsHelpers.InitializeJwtBearerEvents(options); // When an access token for our own web API is validated, we add it to MSAL.NET's cache so that it can // be used from the controllers. diff --git a/src/Microsoft.Identity.Web/WebApiExtensions/MicrosoftIdentityWebApiAuthenticationBuilderWithConfiguration.cs b/src/Microsoft.Identity.Web/WebApiExtensions/MicrosoftIdentityWebApiAuthenticationBuilderWithConfiguration.cs index 1800bc844..8b7848e6c 100644 --- a/src/Microsoft.Identity.Web/WebApiExtensions/MicrosoftIdentityWebApiAuthenticationBuilderWithConfiguration.cs +++ b/src/Microsoft.Identity.Web/WebApiExtensions/MicrosoftIdentityWebApiAuthenticationBuilderWithConfiguration.cs @@ -12,9 +12,8 @@ namespace Microsoft.Identity.Web /// /// Builder for web API authentication with configuration. /// -#if NET6_0_OR_GREATER && !NET8_0_OR_GREATER [RequiresUnreferencedCode("Microsoft.Identity.Web.MicrosoftIdentityWebApiAuthenticationBuilder.MicrosoftIdentityWebApiAuthenticationBuilder(IServiceCollection, String, Action, Action, IConfigurationSection).")] -#endif + [RequiresDynamicCode("Microsoft.Identity.Web.MicrosoftIdentityWebApiAuthenticationBuilder.MicrosoftIdentityWebApiAuthenticationBuilder(IServiceCollection, String, Action, Action, IConfigurationSection).")] public class MicrosoftIdentityWebApiAuthenticationBuilderWithConfiguration : MicrosoftIdentityWebApiAuthenticationBuilder { internal MicrosoftIdentityWebApiAuthenticationBuilderWithConfiguration( @@ -33,9 +32,8 @@ internal MicrosoftIdentityWebApiAuthenticationBuilderWithConfiguration( /// This method expects the configuration file will have a section, named "AzureAd" as default, with the necessary settings to initialize authentication options. /// /// The authentication builder to chain. -#if NET6_0_OR_GREATER && !NET8_0_OR_GREATER [RequiresUnreferencedCode("Calls Microsoft.Extensions.Configuration.ConfigurationBinder.Bind(IConfiguration, Object).")] -#endif + [RequiresDynamicCode("Calls Microsoft.Extensions.Configuration.ConfigurationBinder.Bind(IConfiguration, Object).")] public MicrosoftIdentityAppCallsWebApiAuthenticationBuilder EnableTokenAcquisitionToCallDownstreamApi() { return EnableTokenAcquisitionToCallDownstreamApi(options => ConfigurationSection?.Bind(options)); diff --git a/src/Microsoft.Identity.Web/WebApiExtensions/MicrosoftIdentityWebApiServiceCollectionExtensions.cs b/src/Microsoft.Identity.Web/WebApiExtensions/MicrosoftIdentityWebApiServiceCollectionExtensions.cs index 02c59ba4f..e9b6d101b 100644 --- a/src/Microsoft.Identity.Web/WebApiExtensions/MicrosoftIdentityWebApiServiceCollectionExtensions.cs +++ b/src/Microsoft.Identity.Web/WebApiExtensions/MicrosoftIdentityWebApiServiceCollectionExtensions.cs @@ -25,9 +25,8 @@ public static partial class MicrosoftIdentityWebApiServiceCollectionExtensions /// /// Set to true if you want to debug, or just understand the JwtBearer events. /// The authentication builder to chain extension methods. -#if NET6_0_OR_GREATER && !NET8_0_OR_GREATER [RequiresUnreferencedCode("Microsoft.Identity.Web.MicrosoftIdentityWebApiAuthenticationBuilderExtensions.AddMicrosoftIdentityWebApi(AuthenticationBuilder, IConfiguration, String, String, Boolean).")] -#endif + [RequiresDynamicCode("Microsoft.Identity.Web.MicrosoftIdentityWebApiAuthenticationBuilderExtensions.AddMicrosoftIdentityWebApi(AuthenticationBuilder, IConfiguration, String, String, Boolean).")] public static MicrosoftIdentityWebApiAuthenticationBuilderWithConfiguration AddMicrosoftIdentityWebApiAuthentication( this IServiceCollection services, IConfiguration configuration, diff --git a/src/Microsoft.Identity.Web/WebAppExtensions/MicrosoftIdentityAppCallingWebApiAuthenticationBuilderExt.cs b/src/Microsoft.Identity.Web/WebAppExtensions/MicrosoftIdentityAppCallingWebApiAuthenticationBuilderExt.cs index 79ec15d34..84401118b 100644 --- a/src/Microsoft.Identity.Web/WebAppExtensions/MicrosoftIdentityAppCallingWebApiAuthenticationBuilderExt.cs +++ b/src/Microsoft.Identity.Web/WebAppExtensions/MicrosoftIdentityAppCallingWebApiAuthenticationBuilderExt.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; @@ -23,6 +24,8 @@ public static class MicrosoftIdentityAppCallsWebApiAuthenticationBuilderExtensio /// /// /// The service collection + [RequiresUnreferencedCode("Session State middleware does not currently support trimming or native AOT. https://aka.ms/aspnet/trimming")] + [RequiresDynamicCode("Session State middleware does not currently support trimming or native AOT. https://aka.ms/aspnet/trimming")] public static IServiceCollection AddSessionTokenCaches(this MicrosoftIdentityAppCallsWebApiAuthenticationBuilder builder) { _ = Throws.IfNull(builder); diff --git a/src/Microsoft.Identity.Web/WebAppExtensions/MicrosoftIdentityWebAppAuthenticationBuilder.cs b/src/Microsoft.Identity.Web/WebAppExtensions/MicrosoftIdentityWebAppAuthenticationBuilder.cs index dafd1d250..13844e7d5 100644 --- a/src/Microsoft.Identity.Web/WebAppExtensions/MicrosoftIdentityWebAppAuthenticationBuilder.cs +++ b/src/Microsoft.Identity.Web/WebAppExtensions/MicrosoftIdentityWebAppAuthenticationBuilder.cs @@ -32,9 +32,8 @@ public class MicrosoftIdentityWebAppAuthenticationBuilder : MicrosoftIdentityBas /// Action called to configure /// the Microsoft identity options. /// Optional configuration section. -#if NET6_0_OR_GREATER && !NET8_0_OR_GREATER [RequiresUnreferencedCode("Calls Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder.MicrosoftIdentityBaseAuthenticationBuilder(IServiceCollection, IConfigurationSection).")] -#endif + [RequiresDynamicCode("Calls Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder.MicrosoftIdentityBaseAuthenticationBuilder(IServiceCollection, IConfigurationSection).")] internal MicrosoftIdentityWebAppAuthenticationBuilder( IServiceCollection services, string openIdConnectScheme, @@ -58,9 +57,8 @@ internal MicrosoftIdentityWebAppAuthenticationBuilder( /// /// Initial scopes. /// The builder itself for chaining. -#if NET6_0_OR_GREATER && !NET8_0_OR_GREATER [RequiresUnreferencedCode("Calls Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder.MicrosoftIdentityBaseAuthenticationBuilder(IServiceCollection, IConfigurationSection).")] -#endif + [RequiresDynamicCode("Calls Microsoft.Identity.Web.MicrosoftIdentityBaseAuthenticationBuilder.MicrosoftIdentityBaseAuthenticationBuilder(IServiceCollection, IConfigurationSection).")] public MicrosoftIdentityAppCallsWebApiAuthenticationBuilder EnableTokenAcquisitionToCallDownstreamApi( IEnumerable? initialScopes = null) { @@ -75,9 +73,8 @@ public MicrosoftIdentityAppCallsWebApiAuthenticationBuilder EnableTokenAcquisiti /// MSAL.NET confidential client application options. /// Initial scopes. /// The builder itself for chaining. -#if NET6_0_OR_GREATER && !NET8_0_OR_GREATER [RequiresUnreferencedCode("Calls Microsoft.Identity.Web.MicrosoftIdentityWebAppAuthenticationBuilder.WebAppCallsWebApiImplementation(IServiceCollection, IEnumerable, Action, string, Action.")] -#endif + [RequiresDynamicCode("Calls Microsoft.Identity.Web.MicrosoftIdentityWebAppAuthenticationBuilder.WebAppCallsWebApiImplementation(IServiceCollection, IEnumerable, Action, string, Action.")] public MicrosoftIdentityAppCallsWebApiAuthenticationBuilder EnableTokenAcquisitionToCallDownstreamApi( Action? configureConfidentialClientApplicationOptions, IEnumerable? initialScopes = null) @@ -93,9 +90,8 @@ public MicrosoftIdentityAppCallsWebApiAuthenticationBuilder EnableTokenAcquisiti ConfigurationSection); } -#if NET6_0_OR_GREATER [RequiresUnreferencedCode("Calls Microsoft.Identity.Web.ClientInfo.CreateFromJson(string).")] -#endif + [RequiresDynamicCode("Calls Microsoft.Identity.Web.ClientInfo.CreateFromJson(string).")] internal static void WebAppCallsWebApiImplementation( IServiceCollection services, IEnumerable? initialScopes, diff --git a/src/Microsoft.Identity.Web/WebAppExtensions/MicrosoftIdentityWebAppAuthenticationBuilderExtensions.cs b/src/Microsoft.Identity.Web/WebAppExtensions/MicrosoftIdentityWebAppAuthenticationBuilderExtensions.cs index 1e731d0c0..3b7f64f01 100644 --- a/src/Microsoft.Identity.Web/WebAppExtensions/MicrosoftIdentityWebAppAuthenticationBuilderExtensions.cs +++ b/src/Microsoft.Identity.Web/WebAppExtensions/MicrosoftIdentityWebAppAuthenticationBuilderExtensions.cs @@ -37,9 +37,8 @@ public static class MicrosoftIdentityWebAppAuthenticationBuilderExtensions /// Set to true if you want to debug, or just understand the OpenID Connect events. /// A display name for the authentication handler. /// The builder for chaining. -#if NET6_0_OR_GREATER [RequiresUnreferencedCode("Calls a trim-incompatible AddMicrosoftIdentityWebApp.")] -#endif + [RequiresDynamicCode("Calls a trim-incompatible AddMicrosoftIdentityWebApp.")] public static MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration AddMicrosoftIdentityWebApp( this AuthenticationBuilder builder, IConfiguration configuration, @@ -80,9 +79,8 @@ public static MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration AddM /// Set to true if you want to debug, or just understand the OpenID Connect events. /// A display name for the authentication handler. /// The authentication builder for chaining. -#if NET6_0_OR_GREATER && !NET8_0_OR_GREATER [RequiresUnreferencedCode("Calls Microsoft.Extensions.Configuration.ConfigurationBinder.Bind(IConfiguration, Object).")] -#endif + [RequiresDynamicCode("Calls Microsoft.Extensions.Configuration.ConfigurationBinder.Bind(IConfiguration, Object).")] public static MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration AddMicrosoftIdentityWebApp( this AuthenticationBuilder builder, IConfigurationSection configurationSection, @@ -115,9 +113,8 @@ public static MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration AddM /// Set to true if you want to debug, or just understand the OpenID Connect events. /// A display name for the authentication handler. /// The authentication builder for chaining. -#if NET6_0_OR_GREATER && !NET8_0_OR_GREATER [RequiresUnreferencedCode("Microsoft.Identity.Web.MicrosoftIdentityWebAppAuthenticationBuilderExtensions.AddMicrosoftWebAppWithoutConfiguration(AuthenticationBuilder, Action, Action, String, String, Boolean, String).")] -#endif + [RequiresDynamicCode("Microsoft.Identity.Web.MicrosoftIdentityWebAppAuthenticationBuilderExtensions.AddMicrosoftWebAppWithoutConfiguration(AuthenticationBuilder, Action, Action, String, String, Boolean, String).")] public static MicrosoftIdentityWebAppAuthenticationBuilder AddMicrosoftIdentityWebApp( this AuthenticationBuilder builder, Action configureMicrosoftIdentityOptions, @@ -150,9 +147,8 @@ public static MicrosoftIdentityWebAppAuthenticationBuilder AddMicrosoftIdentityW /// A display name for the authentication handler. /// Configuration section. /// The authentication builder for chaining. -#if NET6_0_OR_GREATER && !NET8_0_OR_GREATER [RequiresUnreferencedCode("Calls Microsoft.Identity.Web.MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration.MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration(IServiceCollection, String, Action, IConfigurationSection)")] -#endif + [RequiresDynamicCode("Calls Microsoft.Identity.Web.MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration.MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration(IServiceCollection, String, Action, IConfigurationSection)")] private static MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration AddMicrosoftIdentityWebAppWithConfiguration( this AuthenticationBuilder builder, Action configureMicrosoftIdentityOptions, @@ -190,9 +186,8 @@ private static MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration Add /// Set to true if you want to debug, or just understand the OpenID Connect events. /// A display name for the authentication handler. /// The authentication builder for chaining. -#if NET6_0_OR_GREATER && !NET8_0_OR_GREATER [RequiresUnreferencedCode("Calls Microsoft.Identity.Web.MicrosoftIdentityWebAppAuthenticationBuilder.MicrosoftIdentityWebAppAuthenticationBuilder(IServiceCollection, String, Action, IConfigurationSection)")] -#endif + [RequiresDynamicCode("Calls Microsoft.Identity.Web.MicrosoftIdentityWebAppAuthenticationBuilder.MicrosoftIdentityWebAppAuthenticationBuilder(IServiceCollection, String, Action, IConfigurationSection)")] private static MicrosoftIdentityWebAppAuthenticationBuilder AddMicrosoftWebAppWithoutConfiguration( this AuthenticationBuilder builder, Action configureMicrosoftIdentityOptions, @@ -226,6 +221,8 @@ private static MicrosoftIdentityWebAppAuthenticationBuilder AddMicrosoftWebAppWi null); } + [RequiresUnreferencedCode("Calls Microsoft.Extensions.Configuration.ConfigurationBinder.GetValue")] + [RequiresDynamicCode("Calls Microsoft.Extensions.Configuration.ConfigurationBinder.GetValue")] private static void AddMicrosoftIdentityWebAppInternal( AuthenticationBuilder builder, Action configureMicrosoftIdentityOptions, diff --git a/src/Microsoft.Identity.Web/WebAppExtensions/MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration.cs b/src/Microsoft.Identity.Web/WebAppExtensions/MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration.cs index df58161bc..7d8fcc4af 100644 --- a/src/Microsoft.Identity.Web/WebAppExtensions/MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration.cs +++ b/src/Microsoft.Identity.Web/WebAppExtensions/MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration.cs @@ -23,9 +23,8 @@ public class MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration : Mic /// Action called to configure /// the Microsoft identity options. /// Optional configuration section. -#if NET6_0_OR_GREATER && !NET8_0_OR_GREATER [RequiresUnreferencedCode("Calls Microsoft.Identity.Web.MicrosoftIdentityWebAppAuthenticationBuilder.MicrosoftIdentityWebAppAuthenticationBuilder(IServiceCollection, String, Action, IConfigurationSection)")] -#endif + [RequiresDynamicCode("Calls Microsoft.Identity.Web.MicrosoftIdentityWebAppAuthenticationBuilder.MicrosoftIdentityWebAppAuthenticationBuilder(IServiceCollection, String, Action, IConfigurationSection)")] internal MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration( IServiceCollection services, string openIdConnectScheme, @@ -41,9 +40,8 @@ internal MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration( /// /// Optional initial scopes to request. /// The authentication builder for chaining. -#if NET6_0_OR_GREATER && !NET8_0_OR_GREATER [RequiresUnreferencedCode("Calls Microsoft.Extensions.Configuration.ConfigurationBinder.Bind(IConfiguration, Object).")] -#endif + [RequiresDynamicCode("Calls Microsoft.Extensions.Configuration.ConfigurationBinder.Bind(IConfiguration, Object).")] public new MicrosoftIdentityAppCallsWebApiAuthenticationBuilder EnableTokenAcquisitionToCallDownstreamApi( IEnumerable? initialScopes = null) { diff --git a/src/Microsoft.Identity.Web/WebAppExtensions/MicrosoftIdentityWebAppServiceCollectionExtensions.cs b/src/Microsoft.Identity.Web/WebAppExtensions/MicrosoftIdentityWebAppServiceCollectionExtensions.cs index a4d4f03da..e5eaedf02 100644 --- a/src/Microsoft.Identity.Web/WebAppExtensions/MicrosoftIdentityWebAppServiceCollectionExtensions.cs +++ b/src/Microsoft.Identity.Web/WebAppExtensions/MicrosoftIdentityWebAppServiceCollectionExtensions.cs @@ -32,10 +32,8 @@ public static partial class MicrosoftIdentityWebAppServiceCollectionExtensions /// Set to true if you want to debug, or just understand the OpenIdConnect events. /// A display name for the authentication handler. /// The authentication builder to chain extension methods. -#if NET6_0_OR_GREATER && !NET8_0_OR_GREATER [RequiresUnreferencedCode("Microsoft.Identity.Web.MicrosoftIdentityWebAppAuthenticationBuilderExtensions.AddMicrosoftIdentityWebApp(AuthenticationBuilder, IConfiguration, String, String, String, Boolean, String).")] -#endif - + [RequiresDynamicCode("Microsoft.Identity.Web.MicrosoftIdentityWebAppAuthenticationBuilderExtensions.AddMicrosoftIdentityWebApp(AuthenticationBuilder, IConfiguration, String, String, String, Boolean, String).")] public static MicrosoftIdentityWebAppAuthenticationBuilderWithConfiguration AddMicrosoftIdentityWebAppAuthentication( this IServiceCollection services, IConfiguration configuration, diff --git a/src/Shared/CodeAnalysisAttributes.cs b/src/Shared/CodeAnalysisAttributes.cs new file mode 100644 index 000000000..cf1b2475c --- /dev/null +++ b/src/Shared/CodeAnalysisAttributes.cs @@ -0,0 +1,175 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// This file provides polyfills for AOT-related attributes on older target frameworks +// that don't have them built-in. It is shared across multiple projects via MSBuild linking. + +// Suppress public API analyzer warnings for these internal polyfill types +#pragma warning disable RS0016 // Symbol is not part of the declared public API +#pragma warning disable RS0036 // Symbol is not part of the declared internal API +#pragma warning disable RS0051 // Symbol is not part of the declared API + +// These attributes are available in .NET 5.0+ but need polyfills for netstandard2.0 and .NET Framework +#if NETSTANDARD2_0 || NETFRAMEWORK + +namespace System.Diagnostics.CodeAnalysis +{ + /// + /// Indicates that the specified method requires dynamic access to code that is not referenced statically. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Constructor | AttributeTargets.Method, Inherited = false)] + internal sealed class RequiresDynamicCodeAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// A message that contains information about the usage of dynamic code. + public RequiresDynamicCodeAttribute(string message) + { + Message = message; + } + + /// + /// Gets a message that contains information about the usage of dynamic code. + /// + public string Message { get; } + + /// + /// Gets or sets an optional URL that contains more information about the method. + /// + public string? Url { get; set; } + } + + /// + /// Indicates that the specified method requires the ability to generate new code at runtime. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Constructor | AttributeTargets.Method, Inherited = false)] + internal sealed class RequiresUnreferencedCodeAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// A message that contains information about the usage of unreferenced code. + public RequiresUnreferencedCodeAttribute(string message) + { + Message = message; + } + + /// + /// Gets a message that contains information about the usage of unreferenced code. + /// + public string Message { get; } + + /// + /// Gets or sets an optional URL that contains more information about the method. + /// + public string? Url { get; set; } + } + + /// + /// Specifies the types of members that are dynamically accessed. + /// + [Flags] + internal enum DynamicallyAccessedMemberTypes + { + /// Specifies no members. + None = 0, + /// Specifies the default, parameterless public constructor. + PublicParameterlessConstructor = 1, + /// Specifies all public constructors. + PublicConstructors = 3, + /// Specifies all non-public constructors. + NonPublicConstructors = 4, + /// Specifies all public methods. + PublicMethods = 8, + /// Specifies all non-public methods. + NonPublicMethods = 16, + /// Specifies all public fields. + PublicFields = 32, + /// Specifies all non-public fields. + NonPublicFields = 64, + /// Specifies all public nested types. + PublicNestedTypes = 128, + /// Specifies all non-public nested types. + NonPublicNestedTypes = 256, + /// Specifies all public properties. + PublicProperties = 512, + /// Specifies all non-public properties. + NonPublicProperties = 1024, + /// Specifies all public events. + PublicEvents = 2048, + /// Specifies all non-public events. + NonPublicEvents = 4096, + /// Specifies all interfaces implemented by the type. + Interfaces = 8192, + /// Specifies all members. + All = -1 + } + + /// + /// Indicates that certain members on a specified Type are accessed dynamically. + /// + [AttributeUsage( + AttributeTargets.Field | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter | + AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.Method | + AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct, + Inherited = false)] + internal sealed class DynamicallyAccessedMembersAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// The types of members dynamically accessed. + public DynamicallyAccessedMembersAttribute(DynamicallyAccessedMemberTypes memberTypes) + { + MemberTypes = memberTypes; + } + + /// + /// Gets the which specifies the type of members dynamically accessed. + /// + public DynamicallyAccessedMemberTypes MemberTypes { get; } + } + + /// + /// Suppresses reporting of a specific rule violation, allowing multiple suppressions on a single code artifact. + /// + [AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)] + internal sealed class UnconditionalSuppressMessageAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// The category for the attribute. + /// The identifier of the analysis tool rule to be suppressed. + public UnconditionalSuppressMessageAttribute(string category, string checkId) + { + Category = category; + CheckId = checkId; + } + + /// Gets the category identifying the classification of the attribute. + public string Category { get; } + + /// Gets the identifier of the analysis tool rule to be suppressed. + public string CheckId { get; } + + /// Gets or sets the scope of the code that is relevant for the attribute. + public string? Scope { get; set; } + + /// Gets or sets a fully qualified path that represents the target of the attribute. + public string? Target { get; set; } + + /// Gets or sets an optional argument expanding on exclusion criteria. + public string? MessageId { get; set; } + + /// Gets or sets the justification for suppressing the code analysis message. + public string? Justification { get; set; } + } +} + +#pragma warning restore RS0016 +#pragma warning restore RS0036 +#pragma warning restore RS0051 + +#endif diff --git a/supportPolicy.md b/supportPolicy.md index efa6556a3..d386ee16e 100644 --- a/supportPolicy.md +++ b/supportPolicy.md @@ -1,19 +1,20 @@ # Microsoft.Identity.Web Support Policy -_Last updated May 12, 2025_ +_Last updated December 16, 2025_ ## Supported versions The following table lists IdentityWeb versions currently supported and receiving security fixes. | Major Version | Last Release | Patch Release Date | Support Phase|End of Support | | --------------|--------------|--------|------------|--------| -| 3.x | [![NuGet](https://img.shields.io/nuget/v/Microsoft.Identity.Web.svg?style=flat-square&label=nuget&colorB=00b200)](https://www.nuget.org/packages/Microsoft.Identity.Web/) |Monthly| Active | Not planned.
āœ…Supported versions: from 3.0.0 to [![NuGet](https://img.shields.io/nuget/v/Microsoft.Identity.Web.svg?style=flat-square&label=nuget&colorB=00b200)](https://www.nuget.org/packages/Microsoft.Identity.Web/)
āš ļøUnsupported versions `< 3.0.0`.| +| 4.x | [![NuGet](https://img.shields.io/nuget/v/Microsoft.Identity.Web.svg?style=flat-square&label=nuget&colorB=00b200)](https://www.nuget.org/packages/Microsoft.Identity.Web/) |Monthly| Active | Not planned.
āœ…Supported versions: from 4.0.0 to [![NuGet](https://img.shields.io/nuget/v/Microsoft.Identity.Web.svg?style=flat-square&label=nuget&colorB=00b200)](https://www.nuget.org/packages/Microsoft.Identity.Web/)
āš ļøUnsupported versions `< 4.0.0`.| ## Out of support versions The following table lists Microsoft.Identity.Web versions no longer supported and no longer receiving security fixes. | Major Version | Latest Patch Version| Patch Release Date | End of Support Date| | --------------|--------------|--------|--------| +| 3.x | 3.14.1 | August 27, 2025 | October 13, 2025 | | 2.x | 2.21.0 | July 18, 2024 | January 1, 2025 | | 1.x | 1.26.0 | February 5, 2023 | February 5, 2023| diff --git a/tests/DevApps/AjaxCallActionsWithDynamicConsent/appsettings.json b/tests/DevApps/AjaxCallActionsWithDynamicConsent/appsettings.json index aab0e7d84..42cc37050 100644 --- a/tests/DevApps/AjaxCallActionsWithDynamicConsent/appsettings.json +++ b/tests/DevApps/AjaxCallActionsWithDynamicConsent/appsettings.json @@ -1,9 +1,9 @@ { "AzureAd": { "Instance": "https://login.microsoftonline.com/", - "Domain": "msidlab4.onmicrosoft.com", - "TenantId": "f645ad92-e38d-4d1a-b510-d1b09a74a8ca", - "ClientId": "9a192b78-6580-4f8a-aace-f36ffea4f7be", + "Domain": "id4slab1.onmicrosoft.com", + "TenantId": "10c419d4-4a50-45b2-aa4e-919fb84df24f", + "ClientId": "a599ce88-0a5f-4a6e-beca-e67d3fc427f4", // To call an API "ClientSecret": "[secret-from-portal]", "CallbackPath": "/signin-oidc" diff --git a/tests/DevApps/ContosoWorker/appsettings.json b/tests/DevApps/ContosoWorker/appsettings.json index ba64c942d..cce19bf29 100644 --- a/tests/DevApps/ContosoWorker/appsettings.json +++ b/tests/DevApps/ContosoWorker/appsettings.json @@ -1,8 +1,8 @@ { "AzureAd": { "Instance": "https://login.microsoftonline.com/", - "TenantId": "msidlab4.onmicrosoft.com", - "ClientId": "f6b698c0-140c-448f-8155-4aa9bf77ceba", + "TenantId": "id4slab1.onmicrosoft.com", + "ClientId": "4ebc2cfc-14bf-4c88-9678-26543ec1c59d", "ClientCredentials": [ { "SourceType": "StoreWithDistinguishedName", diff --git a/tests/DevApps/MtlsPop/MtlsPopClient/MtlsPopClient.csproj b/tests/DevApps/MtlsPop/MtlsPopClient/MtlsPopClient.csproj new file mode 100644 index 000000000..4a7b2c7a0 --- /dev/null +++ b/tests/DevApps/MtlsPop/MtlsPopClient/MtlsPopClient.csproj @@ -0,0 +1,31 @@ + + + + Exe + net8.0 + enable + enable + true + + + + + + + + + + + + + + + + + + + Always + + + + diff --git a/tests/DevApps/MtlsPop/MtlsPopClient/Program.cs b/tests/DevApps/MtlsPop/MtlsPopClient/Program.cs new file mode 100644 index 000000000..7efb7844b --- /dev/null +++ b/tests/DevApps/MtlsPop/MtlsPopClient/Program.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Graph; +using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Web; + +namespace MtlsPopSample +{ + public class Program + { + public static async Task Main(string[] args) + { + var tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance(); + + tokenAcquirerFactory.Services.AddLogging(builder => + builder.AddConsole().SetMinimumLevel(LogLevel.Information)); + + tokenAcquirerFactory.Services.AddDownstreamApi("WebApi", + tokenAcquirerFactory.Configuration.GetSection("WebApi")); + + tokenAcquirerFactory.Services.AddMicrosoftGraph(); + + var sp = tokenAcquirerFactory.Build(); + + Console.WriteLine("Scenario 1: calling web API with mTLS PoP token..."); + var webApi = sp.GetRequiredService(); + var result = await webApi.GetForAppAsync>("WebApi").ConfigureAwait(false); + + Console.WriteLine("Web API result:"); + foreach (var forecast in result!) + { + Console.WriteLine($"{forecast.Date}: {forecast.Summary} - {forecast.TemperatureC}C/{forecast.TemperatureF}F"); + } + + Console.WriteLine(); + Console.WriteLine("Scenario 2: Calling Microsoft Graph with Bearer (non mTLS PoP) token..."); + var graphServiceClient = sp.GetRequiredService(); + var users = await graphServiceClient.Users + .Request() + .WithAppOnly() + .GetAsync(); + + Console.WriteLine("Microsoft Graph result:"); + Console.WriteLine($"{users.Count} users"); + } + } +} diff --git a/tests/DevApps/MtlsPop/MtlsPopClient/WeatherForecast.cs b/tests/DevApps/MtlsPop/MtlsPopClient/WeatherForecast.cs new file mode 100644 index 000000000..77f705c3e --- /dev/null +++ b/tests/DevApps/MtlsPop/MtlsPopClient/WeatherForecast.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace MtlsPopSample +{ + public class WeatherForecast + { + public DateOnly Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string? Summary { get; set; } + } +} diff --git a/tests/DevApps/MtlsPop/MtlsPopClient/appsettings.json b/tests/DevApps/MtlsPop/MtlsPopClient/appsettings.json new file mode 100644 index 000000000..b9df96e2e --- /dev/null +++ b/tests/DevApps/MtlsPop/MtlsPopClient/appsettings.json @@ -0,0 +1,23 @@ +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "bea21ebe-8b64-4d06-9f6d-6a889b120a7c", + "ClientId": "163ffef9-a313-45b4-ab2f-c7e2f5e0e23e", + "AzureRegion": "westus3", + "ClientCredentials": [ + { + "SourceType": "StoreWithDistinguishedName", + "CertificateStorePath": "CurrentUser/My", + "CertificateDistinguishedName": "CN=LabAuth.MSIDLab.com" + } + ], + "SendX5c": true + }, + "WebApi": { + "BaseUrl": "https://localhost:7060/", + "RelativePath": "WeatherForecast", + "ProtocolScheme": "MTLS_POP", + "RequestAppToken": true, + "Scopes": [ "https://graph.microsoft.com/.default" ] + } +} diff --git a/tests/DevApps/MtlsPop/MtlsPopWebApi/Controllers/WeatherForecastController.cs b/tests/DevApps/MtlsPop/MtlsPopWebApi/Controllers/WeatherForecastController.cs new file mode 100644 index 000000000..4203a83ce --- /dev/null +++ b/tests/DevApps/MtlsPop/MtlsPopWebApi/Controllers/WeatherForecastController.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Identity.Web.Resource; + +namespace MtlsPopSample.Controllers +{ + [ApiController] + [Route("[controller]")] + [Authorize(AuthenticationSchemes = MtlsPopAuthenticationHandler.ProtocolScheme)] + public class WeatherForecastController : ControllerBase + { + private static readonly string[] Summaries = new[] + { + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + }; + + private readonly ILogger _logger; + + public WeatherForecastController(ILogger logger) + { + _logger = logger; + } + + [HttpGet(Name = "GetWeatherForecast")] + public IEnumerable Get() + { + return Enumerable.Range(1, 5).Select(index => new WeatherForecast + { + Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = Summaries[Random.Shared.Next(Summaries.Length)] + }) + .ToArray(); + } + } +} diff --git a/tests/DevApps/MtlsPop/MtlsPopWebApi/MtlsPopAuthenticationHandler.cs b/tests/DevApps/MtlsPop/MtlsPopWebApi/MtlsPopAuthenticationHandler.cs new file mode 100644 index 000000000..7239ec317 --- /dev/null +++ b/tests/DevApps/MtlsPop/MtlsPopWebApi/MtlsPopAuthenticationHandler.cs @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Security.Claims; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text.Encodings.Web; +using System.Text.Json; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Tokens; + +namespace MtlsPopSample +{ + public class MtlsPopAuthenticationHandler : AuthenticationHandler + { + public const string ProtocolScheme = "MTLS_POP"; + + public MtlsPopAuthenticationHandler( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder) + : base(options, logger, encoder) + { + } + + protected override async Task HandleAuthenticateAsync() + { + Logger.LogInformation("MtlsPopAuthenticationHandler invoked"); + + var authHeader = Request.Headers.Authorization.FirstOrDefault(); + if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith($"{ProtocolScheme} ", StringComparison.OrdinalIgnoreCase)) + { + Logger.LogWarning("No MTLS_POP authorization header found"); + return AuthenticateResult.NoResult(); + } + + var authToken = authHeader.Substring($"{ProtocolScheme} ".Length).Trim(); + if (string.IsNullOrEmpty(authToken)) + { + Logger.LogWarning("MTLS_POP authorization header is empty"); + return AuthenticateResult.Fail("Empty MTLS_POP token"); + } + + try + { + var handler = new JsonWebTokenHandler(); + var token = handler.ReadJsonWebToken(authToken); + + var cnfClaim = token.Claims.FirstOrDefault(c => c.Type == "cnf"); + if (cnfClaim == null) + { + Logger.LogWarning("mTLS PoP token does not contain 'cnf' claim"); + return AuthenticateResult.Fail("Missing 'cnf' claim in MTLS_POP token"); + } + + Logger.LogInformation($"The 'cnf' claim value: {cnfClaim.Value}"); + + var cnfJson = JsonDocument.Parse(cnfClaim.Value); + if (!cnfJson.RootElement.TryGetProperty("x5t#S256", out var x5tS256Element)) + { + Logger.LogWarning("The 'cnf' claim does not contain 'x5t#S256' property"); + return AuthenticateResult.Fail("Missing 'x5t#S256' property in mTLS PoP 'cnf' claim"); + } + + var x5tS256 = x5tS256Element.GetString(); + if (string.IsNullOrEmpty(x5tS256)) + { + Logger.LogWarning("The 'cnf' claim contains an empty 'x5t#S256' property"); + return AuthenticateResult.Fail("Empty 'x5t#S256' property in mTLS PoP 'cnf' claim"); + } + + Logger.LogInformation($"Token bound to certificate with x5t#S256: {x5tS256}"); + + var clientCert = Context.Connection.ClientCertificate; + if (clientCert != null) + { + var certThumbprint = GetCertificateThumbprint(clientCert); + Logger.LogInformation($"Client cert thumbprint: {certThumbprint}"); + + if (!string.Equals(certThumbprint, x5tS256, StringComparison.OrdinalIgnoreCase)) + { + Logger.LogWarning($"Mismatch between cert thumbprint and 'x5t#S256' from mTLS PoP 'cnf' claim property: cert thumbprint - {certThumbprint}, x5t#S256 = {x5tS256}"); + return AuthenticateResult.Fail("Certificate thumbprint mismatch with mTLS PoP 'cnf' claim"); + } + + Logger.LogInformation("mTLS PoP token validation successful"); + } + else + { + Logger.LogInformation("No client certificate in request - skipping certificate binding verification"); + } + + // Create claims principal from the token + var claims = token.Claims.Select(c => new Claim(c.Type, c.Value)).ToList(); + var identity = new CaseSensitiveClaimsIdentity(claims, ProtocolScheme); + var principal = new ClaimsPrincipal(identity); + var ticket = new AuthenticationTicket(principal, ProtocolScheme); + + return AuthenticateResult.Success(ticket); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error validating mTLS PoP token"); + return AuthenticateResult.Fail($"mTLS PoP validation error: {ex.Message}"); + } + } + + private static string GetCertificateThumbprint(X509Certificate2 certificate) + { + using var sha256 = SHA256.Create(); + var hash = sha256.ComputeHash(certificate.RawData); + return Base64UrlEncoder.Encode(hash); + } + } +} diff --git a/tests/DevApps/MtlsPop/MtlsPopWebApi/MtlsPopWebApi.csproj b/tests/DevApps/MtlsPop/MtlsPopWebApi/MtlsPopWebApi.csproj new file mode 100644 index 000000000..8b593378f --- /dev/null +++ b/tests/DevApps/MtlsPop/MtlsPopWebApi/MtlsPopWebApi.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + + + + diff --git a/tests/DevApps/MtlsPop/MtlsPopWebApi/Program.cs b/tests/DevApps/MtlsPop/MtlsPopWebApi/Program.cs new file mode 100644 index 000000000..8cdefb988 --- /dev/null +++ b/tests/DevApps/MtlsPop/MtlsPopWebApi/Program.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Identity.Web; + +namespace MtlsPopSample +{ + public class Program + { + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + builder.Services.AddControllers(); + + // Learn more about configuring OpenAPI at https://learn.microsoft.com/aspnet/core/fundamentals/openapi/aspnetcore-openapi + builder.Services.AddEndpointsApiExplorer(); + + // Add standard JWT Bearer authentication + builder.Services.AddMicrosoftIdentityWebApiAuthentication(builder.Configuration); + + // Add custom MTLS_POP authentication handler + builder.Services.AddAuthentication() + .AddScheme( + MtlsPopAuthenticationHandler.ProtocolScheme, + options => {}); + + var app = builder.Build(); + + app.UseHttpsRedirection(); + + app.UseAuthentication(); + app.UseAuthorization(); + + app.MapControllers(); + + app.Run(); + } + } +} diff --git a/tests/DevApps/MtlsPop/MtlsPopWebApi/Properties/launchSettings.json b/tests/DevApps/MtlsPop/MtlsPopWebApi/Properties/launchSettings.json new file mode 100644 index 000000000..d0f30c895 --- /dev/null +++ b/tests/DevApps/MtlsPop/MtlsPopWebApi/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7060", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/tests/DevApps/MtlsPop/MtlsPopWebApi/WeatherForecast.cs b/tests/DevApps/MtlsPop/MtlsPopWebApi/WeatherForecast.cs new file mode 100644 index 000000000..77f705c3e --- /dev/null +++ b/tests/DevApps/MtlsPop/MtlsPopWebApi/WeatherForecast.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace MtlsPopSample +{ + public class WeatherForecast + { + public DateOnly Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string? Summary { get; set; } + } +} diff --git a/tests/DevApps/MtlsPop/MtlsPopWebApi/appsettings.json b/tests/DevApps/MtlsPop/MtlsPopWebApi/appsettings.json new file mode 100644 index 000000000..9f8f86015 --- /dev/null +++ b/tests/DevApps/MtlsPop/MtlsPopWebApi/appsettings.json @@ -0,0 +1,24 @@ +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "TenantId": "10c419d4-4a50-45b2-aa4e-919fb84df24f", + "ClientId": "a021aff4-57ad-453a-bae8-e4192e5860f3", + "Scopes": "https://graph.microsoft.com/.default" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Information" + } + }, + "AllowedHosts": "*", + "Kestrel": { + "Endpoints": { + "HttpsClientCert": { + "Url": "https://localhost:7060", + "ClientCertificateMode": "RequireCertificate", + "CheckCertificateRevocation": true + } + } + } +} diff --git a/tests/DevApps/MultipleAuthSchemes/appsettings.json b/tests/DevApps/MultipleAuthSchemes/appsettings.json index 2bc7c7a2a..600ca31ae 100644 --- a/tests/DevApps/MultipleAuthSchemes/appsettings.json +++ b/tests/DevApps/MultipleAuthSchemes/appsettings.json @@ -12,9 +12,9 @@ }, "AzureAd": { "Instance": "https://login.microsoftonline.com/", - "Domain": "msidlab4.onmicrosoft.com", - "TenantId": "f645ad92-e38d-4d1a-b510-d1b09a74a8ca", - "ClientId": "9a192b78-6580-4f8a-aace-f36ffea4f7be", + "Domain": "id4slab1.onmicrosoft.com", + "TenantId": "10c419d4-4a50-45b2-aa4e-919fb84df24f", + "ClientId": "a599ce88-0a5f-4a6e-beca-e67d3fc427f4", //"ClientSecret": "", "ClientCertificates": [ ], diff --git a/tests/DevApps/SidecarAdapter/python/README.md b/tests/DevApps/SidecarAdapter/python/README.md index 1ff45fd38..0f4d23cba 100644 --- a/tests/DevApps/SidecarAdapter/python/README.md +++ b/tests/DevApps/SidecarAdapter/python/README.md @@ -26,7 +26,7 @@ The examples depend on setting these variables ```sh $side_car_url = "" # Example values, use appropriate values for the token you want to request. -$token = uv run --with msal get_token.py --client-id "f79f9db9-c582-4b7b-9d4c-0e8fd40623f0" --authority "https://login.microsoftonline.com/f645ad92-e38d-4d1a-b510-d1b09a74a8ca" --scope "api://556d438d-2f4b-4add-9713-ede4e5f5d7da/access_as_user" +$token = uv run --with msal get_token.py --client-id "9808c2f0-4555-4dc2-beea-b4dc3212d39e" --authority "https://login.microsoftonline.com/10c419d4-4a50-45b2-aa4e-919fb84df24f" --scope "api://a021aff4-57ad-453a-bae8-e4192e5860f3/access_as_user" ``` Example: validate an authorization header returned by `get_token.py`: diff --git a/tests/DevApps/SidecarAdapter/typescript/package-lock.json b/tests/DevApps/SidecarAdapter/typescript/package-lock.json index 9dda437e3..632c433c5 100644 --- a/tests/DevApps/SidecarAdapter/typescript/package-lock.json +++ b/tests/DevApps/SidecarAdapter/typescript/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "express": "^5.1.0" + "express": "^5.2.0" }, "devDependencies": { "@azure/msal-node": "^3.8.0", @@ -745,9 +745,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.3.tgz", - "integrity": "sha512-h6cqHGZ6VdnwliFG1NXvMPTy/9PS3h8oLh7ImwR+kl+oYnQizgjxsONmmPSb2C66RksfkfIxEVtDSEcJiO0tqw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", "cpu": [ "arm" ], @@ -759,9 +759,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.3.tgz", - "integrity": "sha512-wd+u7SLT/u6knklV/ifG7gr5Qy4GUbH2hMWcDauPFJzmCZUAJ8L2bTkVXC2niOIxp8lk3iH/QX8kSrUxVZrOVw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", "cpu": [ "arm64" ], @@ -773,9 +773,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.3.tgz", - "integrity": "sha512-lj9ViATR1SsqycwFkJCtYfQTheBdvlWJqzqxwc9f2qrcVrQaF/gCuBRTiTolkRWS6KvNxSk4KHZWG7tDktLgjg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", "cpu": [ "arm64" ], @@ -787,9 +787,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.3.tgz", - "integrity": "sha512-+Dyo7O1KUmIsbzx1l+4V4tvEVnVQqMOIYtrxK7ncLSknl1xnMHLgn7gddJVrYPNZfEB8CIi3hK8gq8bDhb3h5A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", "cpu": [ "x64" ], @@ -801,9 +801,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.3.tgz", - "integrity": "sha512-u9Xg2FavYbD30g3DSfNhxgNrxhi6xVG4Y6i9Ur1C7xUuGDW3banRbXj+qgnIrwRN4KeJ396jchwy9bCIzbyBEQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", "cpu": [ "arm64" ], @@ -815,9 +815,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.3.tgz", - "integrity": "sha512-5M8kyi/OX96wtD5qJR89a/3x5x8x5inXBZO04JWhkQb2JWavOWfjgkdvUqibGJeNNaz1/Z1PPza5/tAPXICI6A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", "cpu": [ "x64" ], @@ -829,9 +829,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.3.tgz", - "integrity": "sha512-IoerZJ4l1wRMopEHRKOO16e04iXRDyZFZnNZKrWeNquh5d6bucjezgd+OxG03mOMTnS1x7hilzb3uURPkJ0OfA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", "cpu": [ "arm" ], @@ -843,9 +843,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.3.tgz", - "integrity": "sha512-ZYdtqgHTDfvrJHSh3W22TvjWxwOgc3ThK/XjgcNGP2DIwFIPeAPNsQxrJO5XqleSlgDux2VAoWQ5iJrtaC1TbA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", "cpu": [ "arm" ], @@ -857,9 +857,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.3.tgz", - "integrity": "sha512-NcViG7A0YtuFDA6xWSgmFb6iPFzHlf5vcqb2p0lGEbT+gjrEEz8nC/EeDHvx6mnGXnGCC1SeVV+8u+smj0CeGQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", "cpu": [ "arm64" ], @@ -871,9 +871,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.3.tgz", - "integrity": "sha512-d3pY7LWno6SYNXRm6Ebsq0DJGoiLXTb83AIPCXl9fmtIQs/rXoS8SJxxUNtFbJ5MiOvs+7y34np77+9l4nfFMw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", "cpu": [ "arm64" ], @@ -885,9 +885,23 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.3.tgz", - "integrity": "sha512-3y5GA0JkBuirLqmjwAKwB0keDlI6JfGYduMlJD/Rl7fvb4Ni8iKdQs1eiunMZJhwDWdCvrcqXRY++VEBbvk6Eg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", "cpu": [ "loong64" ], @@ -899,9 +913,23 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.3.tgz", - "integrity": "sha512-AUUH65a0p3Q0Yfm5oD2KVgzTKgwPyp9DSXc3UA7DtxhEb/WSPfbG4wqXeSN62OG5gSo18em4xv6dbfcUGXcagw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", "cpu": [ "ppc64" ], @@ -913,9 +941,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.3.tgz", - "integrity": "sha512-1makPhFFVBqZE+XFg3Dkq+IkQ7JvmUrwwqaYBL2CE+ZpxPaqkGaiWFEWVGyvTwZace6WLJHwjVh/+CXbKDGPmg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", "cpu": [ "riscv64" ], @@ -927,9 +955,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.3.tgz", - "integrity": "sha512-OOFJa28dxfl8kLOPMUOQBCO6z3X2SAfzIE276fwT52uXDWUS178KWq0pL7d6p1kz7pkzA0yQwtqL0dEPoVcRWg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", "cpu": [ "riscv64" ], @@ -941,9 +969,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.3.tgz", - "integrity": "sha512-jMdsML2VI5l+V7cKfZx3ak+SLlJ8fKvLJ0Eoa4b9/vCUrzXKgoKxvHqvJ/mkWhFiyp88nCkM5S2v6nIwRtPcgg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", "cpu": [ "s390x" ], @@ -955,9 +983,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.3.tgz", - "integrity": "sha512-tPgGd6bY2M2LJTA1uGq8fkSPK8ZLYjDjY+ZLK9WHncCnfIz29LIXIqUgzCR0hIefzy6Hpbe8Th5WOSwTM8E7LA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", "cpu": [ "x64" ], @@ -969,9 +997,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.3.tgz", - "integrity": "sha512-BCFkJjgk+WFzP+tcSMXq77ymAPIxsX9lFJWs+2JzuZTLtksJ2o5hvgTdIcZ5+oKzUDMwI0PfWzRBYAydAHF2Mw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", "cpu": [ "x64" ], @@ -982,10 +1010,24 @@ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.3.tgz", - "integrity": "sha512-KTD/EqjZF3yvRaWUJdD1cW+IQBk4fbQaHYJUmP8N4XoKFZilVL8cobFSTDnjTtxWJQ3JYaMgF4nObY/+nYkumA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", "cpu": [ "arm64" ], @@ -997,9 +1039,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.3.tgz", - "integrity": "sha512-+zteHZdoUYLkyYKObGHieibUFLbttX2r+58l27XZauq0tcWYYuKUwY2wjeCN9oK1Um2YgH2ibd6cnX/wFD7DuA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", "cpu": [ "arm64" ], @@ -1011,9 +1053,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.3.tgz", - "integrity": "sha512-of1iHkTQSo3kr6dTIRX6t81uj/c/b15HXVsPcEElN5sS859qHrOepM5p9G41Hah+CTqSh2r8Bm56dL2z9UQQ7g==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", "cpu": [ "ia32" ], @@ -1025,9 +1067,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.3.tgz", - "integrity": "sha512-s0hybmlHb56mWVZQj8ra9048/WZTPLILKxcvcq+8awSZmyiSUZjjem1AhU3Tf4ZKpYhK4mg36HtHDOe8QJS5PQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", "cpu": [ "x64" ], @@ -1039,9 +1081,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.3.tgz", - "integrity": "sha512-zGIbEVVXVtauFgl3MRwGWEN36P5ZGenHRMgNw88X5wEhEBpq0XrMEZwOn07+ICrwM17XO5xfMZqh0OldCH5VTA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", "cpu": [ "x64" ], @@ -1391,13 +1433,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -1657,23 +1699,27 @@ "license": "MIT" }, "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", - "debug": "^4.4.0", + "debug": "^4.4.3", "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", + "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/brace-expansion": { @@ -2314,18 +2360,19 @@ } }, "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.0.tgz", + "integrity": "sha512-XdpJDLxfztVY59X0zPI6sibRiGcxhTPXRD3IhJmjKf2jwMvkRGV1j7loB8U+heeamoU3XvihAaGRTR4aXXUN3A==", "license": "MIT", "dependencies": { "accepts": "^2.0.0", - "body-parser": "^2.2.0", + "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", + "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", @@ -2669,40 +2716,39 @@ } }, "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" - } - }, - "node_modules/http-errors/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/ignore": { @@ -2941,13 +2987,13 @@ } }, "node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", + "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", "dev": true, "license": "MIT", "dependencies": { - "jwa": "^1.4.1", + "jwa": "^1.4.2", "safe-buffer": "^5.0.1" } }, @@ -3140,9 +3186,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -3447,9 +3493,9 @@ } }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -3492,36 +3538,20 @@ } }, "node_modules/raw-body": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", - "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.7.0", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.10" } }, - "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3554,9 +3584,9 @@ } }, "node_modules/rollup": { - "version": "4.52.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.3.tgz", - "integrity": "sha512-RIDh866U8agLgiIcdpB+COKnlCreHJLfIhWC3LVflku5YHfpnsIKigRZeFfMfCc4dVcqNVfQQ5gO/afOck064A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, "license": "MIT", "dependencies": { @@ -3570,28 +3600,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.52.3", - "@rollup/rollup-android-arm64": "4.52.3", - "@rollup/rollup-darwin-arm64": "4.52.3", - "@rollup/rollup-darwin-x64": "4.52.3", - "@rollup/rollup-freebsd-arm64": "4.52.3", - "@rollup/rollup-freebsd-x64": "4.52.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.52.3", - "@rollup/rollup-linux-arm-musleabihf": "4.52.3", - "@rollup/rollup-linux-arm64-gnu": "4.52.3", - "@rollup/rollup-linux-arm64-musl": "4.52.3", - "@rollup/rollup-linux-loong64-gnu": "4.52.3", - "@rollup/rollup-linux-ppc64-gnu": "4.52.3", - "@rollup/rollup-linux-riscv64-gnu": "4.52.3", - "@rollup/rollup-linux-riscv64-musl": "4.52.3", - "@rollup/rollup-linux-s390x-gnu": "4.52.3", - "@rollup/rollup-linux-x64-gnu": "4.52.3", - "@rollup/rollup-linux-x64-musl": "4.52.3", - "@rollup/rollup-openharmony-arm64": "4.52.3", - "@rollup/rollup-win32-arm64-msvc": "4.52.3", - "@rollup/rollup-win32-ia32-msvc": "4.52.3", - "@rollup/rollup-win32-x64-gnu": "4.52.3", - "@rollup/rollup-win32-x64-msvc": "4.52.3", + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" } }, diff --git a/tests/DevApps/SidecarAdapter/typescript/package.json b/tests/DevApps/SidecarAdapter/typescript/package.json index b8ddde346..f7c8f7632 100644 --- a/tests/DevApps/SidecarAdapter/typescript/package.json +++ b/tests/DevApps/SidecarAdapter/typescript/package.json @@ -26,6 +26,6 @@ "vitest": "^3.2.4" }, "dependencies": { - "express": "^5.1.0" + "express": "^5.2.0" } } diff --git a/tests/DevApps/SidecarAdapter/typescript/test/sidecar.test.ts b/tests/DevApps/SidecarAdapter/typescript/test/sidecar.test.ts index f22b9a0fe..4264dd35c 100644 --- a/tests/DevApps/SidecarAdapter/typescript/test/sidecar.test.ts +++ b/tests/DevApps/SidecarAdapter/typescript/test/sidecar.test.ts @@ -18,7 +18,7 @@ interface LoginRequest { } const loginRequest: LoginRequest = { - scopes: ["api://556d438d-2f4b-4add-9713-ede4e5f5d7da/access_as_user"], + scopes: ["api://a021aff4-57ad-453a-bae8-e4192e5860f3/access_as_user"], openBrowser, successTemplate: "Successfully signed in! You can close this window now." }; @@ -26,8 +26,8 @@ const loginRequest: LoginRequest = { // Create msal application object const pca = new PublicClientApplication({ auth: { - clientId: "f79f9db9-c582-4b7b-9d4c-0e8fd40623f0", - authority: "https://login.microsoftonline.com/f645ad92-e38d-4d1a-b510-d1b09a74a8ca", + clientId: "9808c2f0-4555-4dc2-beea-b4dc3212d39e", + authority: "https://login.microsoftonline.com/10c419d4-4a50-45b2-aa4e-919fb84df24f", } }); diff --git a/tests/DevApps/WebAppCallsMicrosoftGraph/appsettings.json b/tests/DevApps/WebAppCallsMicrosoftGraph/appsettings.json index 13a1078fc..fa6289362 100644 --- a/tests/DevApps/WebAppCallsMicrosoftGraph/appsettings.json +++ b/tests/DevApps/WebAppCallsMicrosoftGraph/appsettings.json @@ -1,9 +1,9 @@ { "AzureAd": { "Instance": "https://login.microsoftonline.com/", - "Domain": "msidlab4.onmicrosoft.com", - "TenantId": "f645ad92-e38d-4d1a-b510-d1b09a74a8ca", - "ClientId": "9a192b78-6580-4f8a-aace-f36ffea4f7be", + "Domain": "id4slab1.onmicrosoft.com", + "TenantId": "10c419d4-4a50-45b2-aa4e-919fb84df24f", + "ClientId": "a599ce88-0a5f-4a6e-beca-e67d3fc427f4", // To call an API //"EnablePiiLogging": true, "CallbackPath": "/signin-oidc", diff --git a/tests/DevApps/WebAppCallsWebApiCallsGraph/Client/Controllers/HomeController.cs b/tests/DevApps/WebAppCallsWebApiCallsGraph/Client/Controllers/HomeController.cs index 7e4615463..5d927df4e 100644 --- a/tests/DevApps/WebAppCallsWebApiCallsGraph/Client/Controllers/HomeController.cs +++ b/tests/DevApps/WebAppCallsWebApiCallsGraph/Client/Controllers/HomeController.cs @@ -51,7 +51,7 @@ public async Task SayHello() var channel = GrpcChannel.ForAddress("https://localhost:5001"); var client = new Greeter.GreeterClient(channel); - string token = await _tokenAcquisition.GetAccessTokenForUserAsync(new string[] { "api://556d438d-2f4b-4add-9713-ede4e5f5d7da/access_as_user" }).ConfigureAwait(false); + string token = await _tokenAcquisition.GetAccessTokenForUserAsync(new string[] { "api://a021aff4-57ad-453a-bae8-e4192e5860f3/access_as_user" }).ConfigureAwait(false); var headers = new Metadata(); headers.Add("Authorization", $"Bearer {token}"); diff --git a/tests/DevApps/WebAppCallsWebApiCallsGraph/Client/appsettings.json b/tests/DevApps/WebAppCallsWebApiCallsGraph/Client/appsettings.json index 31eaeb544..912b7b49f 100644 --- a/tests/DevApps/WebAppCallsWebApiCallsGraph/Client/appsettings.json +++ b/tests/DevApps/WebAppCallsWebApiCallsGraph/Client/appsettings.json @@ -1,9 +1,9 @@ { "AzureAd": { "Instance": "https://login.microsoftonline.com/", - "Domain": "msidlab4.onmicrosoft.com", - "TenantId": "f645ad92-e38d-4d1a-b510-d1b09a74a8ca", - "ClientId": "9a192b78-6580-4f8a-aace-f36ffea4f7be", + "Domain": "id4slab1.onmicrosoft.com", + "TenantId": "10c419d4-4a50-45b2-aa4e-919fb84df24f", + "ClientId": "a599ce88-0a5f-4a6e-beca-e67d3fc427f4", "CallbackPath": "/signin-oidc", "SignedOutCallbackPath ": "/signout-callback-oidc", "EnablePiiLogging": true, @@ -21,23 +21,23 @@ "DownstreamApis": { "TodoList": { // TodoListScope is the scope of the Web API you want to call. - "Scopes": [ "api://556d438d-2f4b-4add-9713-ede4e5f5d7da/access_as_user" ], + "Scopes": [ "api://a021aff4-57ad-453a-bae8-e4192e5860f3/access_as_user" ], "BaseUrl": "http://localhost:44350" }, "SayHello": { // Scope for the web API set up w/gRPC - "Scopes": [ "api://556d438d-2f4b-4add-9713-ede4e5f5d7da/access_as_user" ], + "Scopes": [ "api://a021aff4-57ad-453a-bae8-e4192e5860f3/access_as_user" ], "BaseUrl": "https://localhost:5001" }, "AzureFunction": { // Scope for the web API set up Azure function - "Scopes": [ "api://556d438d-2f4b-4add-9713-ede4e5f5d7da/access_as_user" ], + "Scopes": [ "api://a021aff4-57ad-453a-bae8-e4192e5860f3/access_as_user" ], "BaseUrl": "http://localhost:7071/api/SampleFunc" }, "TodoListJwe": { // Scope for the web API used with the token decryption certificates. - "Scopes": [ "api://556d438d-2f4b-4add-9713-ede4e5f5d7da/access_as_user" ], + "Scopes": [ "api://a021aff4-57ad-453a-bae8-e4192e5860f3/access_as_user" ], "BaseUrl": "https://localhost:44350" } }, diff --git a/tests/DevApps/WebAppCallsWebApiCallsGraph/TodoListService/Controllers/TodoListController.cs b/tests/DevApps/WebAppCallsWebApiCallsGraph/TodoListService/Controllers/TodoListController.cs index ed6c0e3e2..19f945434 100644 --- a/tests/DevApps/WebAppCallsWebApiCallsGraph/TodoListService/Controllers/TodoListController.cs +++ b/tests/DevApps/WebAppCallsWebApiCallsGraph/TodoListService/Controllers/TodoListController.cs @@ -16,7 +16,7 @@ namespace TodoListService.Controllers { - /* equivalent + /* equivalent [Authorize(Policy = "RequiredScope(|AzureAd:Scope")] [Authorize(Policy = "RequiredScope(User.Read")] */ @@ -61,7 +61,7 @@ public async Task> GetAsync() await RegisterPeriodicCallbackForLongProcessing(null); - // string token1 = await _tokenAcquisition.GetAccessTokenForUserAsync(new string[] { "user.read" }, "f645ad92-e38d-4d1a-b510-d1b09a74a8ca").ConfigureAwait(false); + // string token1 = await _tokenAcquisition.GetAccessTokenForUserAsync(new string[] { "user.read" }, "10c419d4-4a50-45b2-aa4e-919fb84df24f").ConfigureAwait(false); // string token2 = await _tokenAcquisition.GetAccessTokenForUserAsync(new string[] { "user.read" }, "3ebb7dbb-24a5-4083-b60c-5a5977aabf3d").ConfigureAwait(false); await Task.FromResult(0); // fix CS1998 while the lines about the 2 tokens are commented out. @@ -92,7 +92,7 @@ private async Task RegisterPeriodicCallbackForLongProcessing(string keyHint) Timer timer = new Timer(async (state) => { HttpClient httpClient = new HttpClient(); - + var message = await httpClient.GetAsync(url); // CodeQL [SM03781] Requests are made to a sample controller on localhost }, null, 1000, 1000 * 60 * 1); // Callback every minute } diff --git a/tests/DevApps/WebAppCallsWebApiCallsGraph/TodoListService/appsettings.json b/tests/DevApps/WebAppCallsWebApiCallsGraph/TodoListService/appsettings.json index c0949471d..5c90d9a22 100644 --- a/tests/DevApps/WebAppCallsWebApiCallsGraph/TodoListService/appsettings.json +++ b/tests/DevApps/WebAppCallsWebApiCallsGraph/TodoListService/appsettings.json @@ -1,12 +1,12 @@ { "AzureAd": { "Instance": "https://login.microsoftonline.com/", - "Domain": "msidlab4.onmicrosoft.com", - "TenantId": "f645ad92-e38d-4d1a-b510-d1b09a74a8ca", - "ClientId": "556d438d-2f4b-4add-9713-ede4e5f5d7da", //"712ae8d7-548a-4306-95b6-ee9117ee86f0", JWE clientID + "Domain": "id4slab1.onmicrosoft.com", + "TenantId": "10c419d4-4a50-45b2-aa4e-919fb84df24f", + "ClientId": "a021aff4-57ad-453a-bae8-e4192e5860f3", //"712ae8d7-548a-4306-95b6-ee9117ee86f0", JWE clientID // Or instead of Instance + TenantId, you can use the Authority - // "Authority": "https://login.microsoftonline.com/f645ad92-e38d-4d1a-b510-d1b09a74a8ca/", + // "Authority": "https://login.microsoftonline.com/10c419d4-4a50-45b2-aa4e-919fb84df24f/", // To exercise the signing-key issuer: // - uncomment the following line (Authority) diff --git a/tests/DevApps/WebAppCallsWebApiCallsGraph/gRPC/appsettings.json b/tests/DevApps/WebAppCallsWebApiCallsGraph/gRPC/appsettings.json index 82b9de2a3..37d0c735a 100644 --- a/tests/DevApps/WebAppCallsWebApiCallsGraph/gRPC/appsettings.json +++ b/tests/DevApps/WebAppCallsWebApiCallsGraph/gRPC/appsettings.json @@ -1,9 +1,9 @@ { "AzureAd": { "Instance": "https://login.microsoftonline.com/", - "Domain": "msidlab4.onmicrosoft.com", - "TenantId": "f645ad92-e38d-4d1a-b510-d1b09a74a8ca", - "ClientId": "556d438d-2f4b-4add-9713-ede4e5f5d7da", + "Domain": "id4slab1.onmicrosoft.com", + "TenantId": "10c419d4-4a50-45b2-aa4e-919fb84df24f", + "ClientId": "a021aff4-57ad-453a-bae8-e4192e5860f3", "ClientCertificates": [ { "SourceType": "StoreWithDistinguishedName", diff --git a/tests/DevApps/aspnet-mvc/OwinWebApi/Web.config b/tests/DevApps/aspnet-mvc/OwinWebApi/Web.config index 0715cdcd8..73c31bc4b 100644 --- a/tests/DevApps/aspnet-mvc/OwinWebApi/Web.config +++ b/tests/DevApps/aspnet-mvc/OwinWebApi/Web.config @@ -20,6 +20,10 @@ + + + + @@ -78,7 +82,7 @@ - + @@ -94,7 +98,7 @@ - + @@ -102,19 +106,19 @@ - + - + - + - + diff --git a/tests/DevApps/aspnet-mvc/OwinWebApi/appsettings.json b/tests/DevApps/aspnet-mvc/OwinWebApi/appsettings.json index eff81f51a..a00405db1 100644 --- a/tests/DevApps/aspnet-mvc/OwinWebApi/appsettings.json +++ b/tests/DevApps/aspnet-mvc/OwinWebApi/appsettings.json @@ -1,9 +1,9 @@ { "AzureAd": { "Instance": "https://login.microsoftonline.com/", - "Domain": "msidlab4.onmicrosoft.com", - "TenantId": "f645ad92-e38d-4d1a-b510-d1b09a74a8ca", - "ClientId": "556d438d-2f4b-4add-9713-ede4e5f5d7da", //"712ae8d7-548a-4306-95b6-ee9117ee86f0", JWE clientID + "Domain": "id4slab1.onmicrosoft.com", + "TenantId": "10c419d4-4a50-45b2-aa4e-919fb84df24f", + "ClientId": "a021aff4-57ad-453a-bae8-e4192e5860f3", //"712ae8d7-548a-4306-95b6-ee9117ee86f0", JWE clientID // "ClientSecret": "", "Scopes": "access_as_user", "EnableCacheSynchronization": false, diff --git a/tests/DevApps/aspnet-mvc/OwinWebApp/Web.config b/tests/DevApps/aspnet-mvc/OwinWebApp/Web.config index 2904757e1..dea3fa959 100644 --- a/tests/DevApps/aspnet-mvc/OwinWebApp/Web.config +++ b/tests/DevApps/aspnet-mvc/OwinWebApp/Web.config @@ -9,10 +9,10 @@ - @@ -21,6 +21,10 @@ + + + + @@ -79,7 +83,7 @@ - + @@ -95,7 +99,7 @@ - + @@ -103,19 +107,19 @@ - + - + - + - + diff --git a/tests/DevApps/aspnet-mvc/OwinWebApp/appsettings.json b/tests/DevApps/aspnet-mvc/OwinWebApp/appsettings.json index 0b31d5031..1c3584687 100644 --- a/tests/DevApps/aspnet-mvc/OwinWebApp/appsettings.json +++ b/tests/DevApps/aspnet-mvc/OwinWebApp/appsettings.json @@ -1,9 +1,9 @@ { "AzureAd": { "Instance": "https://login.microsoftonline.com/", - "Domain": "msidlab4.onmicrosoft.com", - "TenantId": "f645ad92-e38d-4d1a-b510-d1b09a74a8ca", - "ClientId": "9a192b78-6580-4f8a-aace-f36ffea4f7be", + "Domain": "id4slab1.onmicrosoft.com", + "TenantId": "10c419d4-4a50-45b2-aa4e-919fb84df24f", + "ClientId": "a599ce88-0a5f-4a6e-beca-e67d3fc427f4", "RedirectUri": "https://localhost:44386/", // "ClientSecret": "", "EnableCacheSynchronization": false, diff --git a/tests/DevApps/blazor/BlazorApp/Components/Pages/Weather.razor b/tests/DevApps/blazor/BlazorApp/Components/Pages/Weather.razor index e568baada..ba5c3d336 100644 --- a/tests/DevApps/blazor/BlazorApp/Components/Pages/Weather.razor +++ b/tests/DevApps/blazor/BlazorApp/Components/Pages/Weather.razor @@ -88,7 +88,7 @@ else try { // Get the authorization header - var authorizationHeader = await authorizationHeaderProvider.CreateAuthorizationHeaderForUserAsync(["api://556d438d-2f4b-4add-9713-ede4e5f5d7da/.default"] + var authorizationHeader = await authorizationHeaderProvider.CreateAuthorizationHeaderForUserAsync(["api://a021aff4-57ad-453a-bae8-e4192e5860f3/.default"] //claimsPrincipal: authenticationSate.User, ); } diff --git a/tests/DevApps/blazor/BlazorApp/Program.cs b/tests/DevApps/blazor/BlazorApp/Program.cs index 57f32ac11..62bf41dfc 100644 --- a/tests/DevApps/blazor/BlazorApp/Program.cs +++ b/tests/DevApps/blazor/BlazorApp/Program.cs @@ -16,7 +16,7 @@ builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd")) - .EnableTokenAcquisitionToCallDownstreamApi(["api://556d438d-2f4b-4add-9713-ede4e5f5d7da/.default"]) + .EnableTokenAcquisitionToCallDownstreamApi(["api://a021aff4-57ad-453a-bae8-e4192e5860f3/.default"]) .AddInMemoryTokenCaches(); builder.Services.AddMicrosoftIdentityConsentHandler(); diff --git a/tests/DevApps/blazor/BlazorApp/appsettings.json b/tests/DevApps/blazor/BlazorApp/appsettings.json index cb076cced..b255be5b9 100644 --- a/tests/DevApps/blazor/BlazorApp/appsettings.json +++ b/tests/DevApps/blazor/BlazorApp/appsettings.json @@ -2,9 +2,9 @@ "$schema": "https://raw.githubusercontent.com/AzureAD/microsoft-identity-web/refs/heads/master/JsonSchemas/microsoft-identity-web.json", "AzureAd": { "Instance": "https://login.microsoftonline.com/", - "Domain": "msidlab4.onmicrosoft.com", - "TenantId": "f645ad92-e38d-4d1a-b510-d1b09a74a8ca", - "ClientId": "9a192b78-6580-4f8a-aace-f36ffea4f7be", + "Domain": "id4slab1.onmicrosoft.com", + "TenantId": "10c419d4-4a50-45b2-aa4e-919fb84df24f", + "ClientId": "a599ce88-0a5f-4a6e-beca-e67d3fc427f4", "CallbackPath": "/signin-oidc", "SignedOutCallbackPath ": "/signout-callback-oidc", "EnablePiiLogging": true, diff --git a/tests/DevApps/blazorserver-calls-api/Client/appsettings.json b/tests/DevApps/blazorserver-calls-api/Client/appsettings.json index 0fc3a3f41..f16737ce7 100644 --- a/tests/DevApps/blazorserver-calls-api/Client/appsettings.json +++ b/tests/DevApps/blazorserver-calls-api/Client/appsettings.json @@ -1,9 +1,9 @@ { "AzureAd": { "Instance": "https://login.microsoftonline.com/", - "Domain": "msidlab4.onmicrosoft.com", - "TenantId": "f645ad92-e38d-4d1a-b510-d1b09a74a8ca", - "ClientId": "9a192b78-6580-4f8a-aace-f36ffea4f7be", + "Domain": "id4slab1.onmicrosoft.com", + "TenantId": "10c419d4-4a50-45b2-aa4e-919fb84df24f", + "ClientId": "a599ce88-0a5f-4a6e-beca-e67d3fc427f4", "ClientCertificates": [ { "SourceType": "StoreWithDistinguishedName", @@ -20,7 +20,7 @@ - a scope corresponding to a V1 application (for instance /user_impersonation, where is the clientId of a V1 application, created in the https://portal.azure.com portal. */ - "Scopes": [ "api://556d438d-2f4b-4add-9713-ede4e5f5d7da/access_as_user" ], + "Scopes": [ "api://a021aff4-57ad-453a-bae8-e4192e5860f3/access_as_user" ], "BaseUrl": "https://localhost:44351", "RelativePath": "/api/todolist" diff --git a/tests/DevApps/blazorserver-calls-api/Service/appsettings.json b/tests/DevApps/blazorserver-calls-api/Service/appsettings.json index 40f9a3d1f..562f51dfc 100644 --- a/tests/DevApps/blazorserver-calls-api/Service/appsettings.json +++ b/tests/DevApps/blazorserver-calls-api/Service/appsettings.json @@ -1,9 +1,9 @@ { "AzureAd": { "Instance": "https://login.microsoftonline.com/", - "Domain": "msidlab4.onmicrosoft.com", - "TenantId": "f645ad92-e38d-4d1a-b510-d1b09a74a8ca", - "ClientId": "556d438d-2f4b-4add-9713-ede4e5f5d7da" + "Domain": "id4slab1.onmicrosoft.com", + "TenantId": "10c419d4-4a50-45b2-aa4e-919fb84df24f", + "ClientId": "a021aff4-57ad-453a-bae8-e4192e5860f3" }, "Kestrel": { "Endpoints": { diff --git a/tests/DevApps/daemon-app/Daemon-app/Program - SDK.cs b/tests/DevApps/daemon-app/Daemon-app/Program - SDK.cs index 00821828a..08bb737aa 100644 --- a/tests/DevApps/daemon-app/Daemon-app/Program - SDK.cs +++ b/tests/DevApps/daemon-app/Daemon-app/Program - SDK.cs @@ -27,8 +27,8 @@ static async Task Main(string[] args) /* var tokenAcquirer = tokenAcquirerFactory.GetTokenAcquirer(new MicrosoftIdentityApplicationOptions { - ClientId = "f6b698c0-140c-448f-8155-4aa9bf77ceba", - Authority = "https://login.microsoftonline.com/msidlab4.onmicrosoft.com", + ClientId = "4ebc2cfc-14bf-4c88-9678-26543ec1c59d", + Authority = "https://login.microsoftonline.com/id4slab1.onmicrosoft.com", ClientCredentials = new[] { new CredentialDescription() @@ -42,8 +42,8 @@ static async Task Main(string[] args) */ // Or var tokenAcquirer = tokenAcquirerFactory.GetTokenAcquirer( - authority: "https://login.microsoftonline.com/msidlab4.onmicrosoft.com", - clientId: "f6b698c0-140c-448f-8155-4aa9bf77ceba", + authority: "https://login.microsoftonline.com/id4slab1.onmicrosoft.com", + clientId: "4ebc2cfc-14bf-4c88-9678-26543ec1c59d", clientCredentials: new[] { new CredentialDescription() diff --git a/tests/DevApps/daemon-app/Daemon-app/Program-net60.cs b/tests/DevApps/daemon-app/Daemon-app/Program-net60.cs index 2c9212128..ab921a302 100644 --- a/tests/DevApps/daemon-app/Daemon-app/Program-net60.cs +++ b/tests/DevApps/daemon-app/Daemon-app/Program-net60.cs @@ -23,11 +23,11 @@ static async Task Main(string[] args) { var builder = WebApplication.CreateBuilder(args); var services = builder.Services; - + services.Configure(option => builder.Configuration.GetSection("AzureAd").Bind(option)); services.AddTokenAcquisition(); services.AddHttpClient(); - + //services.AddMicrosoftGraph(); // or services.AddTokenAcquisition() if you don't need graph // Add a cache @@ -47,7 +47,7 @@ static async Task Main(string[] args) // Get the token acquisition service ITokenAcquirerFactory tokenAcquirerFactory = app.Services.GetRequiredService(); var tokenAcquirer = tokenAcquirerFactory.GetTokenAcquirer(); - var result = await tokenAcquirer.GetTokenForAppAsync("api://556d438d-2f4b-4add-9713-ede4e5f5d7da/.default"); + var result = await tokenAcquirer.GetTokenForAppAsync("api://a021aff4-57ad-453a-bae8-e4192e5860f3/.default"); Console.WriteLine($"Token expires on {result.ExpiresOn}"); #endif diff --git a/tests/DevApps/daemon-app/Daemon-app/appsettings.json b/tests/DevApps/daemon-app/Daemon-app/appsettings.json index 59a8fb4c5..748b3b85c 100644 --- a/tests/DevApps/daemon-app/Daemon-app/appsettings.json +++ b/tests/DevApps/daemon-app/Daemon-app/appsettings.json @@ -1,8 +1,8 @@ { "AzureAd": { "Instance": "https://login.microsoftonline.com/", - "TenantId": "msidlab4.onmicrosoft.com", - "ClientId": "f6b698c0-140c-448f-8155-4aa9bf77ceba", + "TenantId": "id4slab1.onmicrosoft.com", + "ClientId": "4ebc2cfc-14bf-4c88-9678-26543ec1c59d", "ClientCredentials": [ { "SourceType": "StoreWithDistinguishedName", diff --git a/tests/DevApps/daemon-app/daemon-app-msi/appsettings.json b/tests/DevApps/daemon-app/daemon-app-msi/appsettings.json index 8b9a0c86a..76f6e4ece 100644 --- a/tests/DevApps/daemon-app/daemon-app-msi/appsettings.json +++ b/tests/DevApps/daemon-app/daemon-app-msi/appsettings.json @@ -8,7 +8,7 @@ // downstream API settings (per-resource) "AzureKeyVault": { "BaseUrl": "https://msidlabs.vault.azure.net/", - "RelativePath": "secrets/msidlab4?api-version=7.4", + "RelativePath": "secrets/id4slab1?api-version=7.4", "RequestAppToken": true, "Scopes": [ "https://vault.azure.net/.default" ], // per request settings diff --git a/tests/DevApps/daemon-app/daemon-console-calling-downstreamApi/appsettings.json b/tests/DevApps/daemon-app/daemon-console-calling-downstreamApi/appsettings.json index 703d87560..2b9212b73 100644 --- a/tests/DevApps/daemon-app/daemon-console-calling-downstreamApi/appsettings.json +++ b/tests/DevApps/daemon-app/daemon-console-calling-downstreamApi/appsettings.json @@ -1,8 +1,8 @@ { "AzureAd": { "Instance": "https://login.microsoftonline.com/", - "TenantId": "msidlab4.onmicrosoft.com", - "ClientId": "f6b698c0-140c-448f-8155-4aa9bf77ceba", + "TenantId": "id4slab1.onmicrosoft.com", + "ClientId": "4ebc2cfc-14bf-4c88-9678-26543ec1c59d", "ClientCredentials": [ { "SourceType": "StoreWithDistinguishedName", @@ -16,6 +16,6 @@ "BaseUrl": "https://localhost:7060/", "RelativePath": "/WeatherForecast", "RequestAppToken": true, - "Scopes": [ "api://556d438d-2f4b-4add-9713-ede4e5f5d7da/.default" ] + "Scopes": [ "api://a021aff4-57ad-453a-bae8-e4192e5860f3/.default" ] } } diff --git a/tests/DevApps/daemon-app/daemon-console-calling-msgraph/appsettings.json b/tests/DevApps/daemon-app/daemon-console-calling-msgraph/appsettings.json index e62cdc3f7..28710fc78 100644 --- a/tests/DevApps/daemon-app/daemon-console-calling-msgraph/appsettings.json +++ b/tests/DevApps/daemon-app/daemon-console-calling-msgraph/appsettings.json @@ -1,8 +1,8 @@ { "AzureAd": { "Instance": "https://login.microsoftonline.com/", - "TenantId": "msidlab4.onmicrosoft.com", - "ClientId": "f6b698c0-140c-448f-8155-4aa9bf77ceba", + "TenantId": "id4slab1.onmicrosoft.com", + "ClientId": "4ebc2cfc-14bf-4c88-9678-26543ec1c59d", "ClientCredentials": [ { "SourceType": "StoreWithDistinguishedName", diff --git a/tests/DevApps/daemon-app/minimal-web-api/appsettings.json b/tests/DevApps/daemon-app/minimal-web-api/appsettings.json index 839460d26..4f2a31263 100644 --- a/tests/DevApps/daemon-app/minimal-web-api/appsettings.json +++ b/tests/DevApps/daemon-app/minimal-web-api/appsettings.json @@ -1,8 +1,8 @@ { "AzureAd": { "Instance": "https://login.microsoftonline.com/", - "TenantId": "f645ad92-e38d-4d1a-b510-d1b09a74a8ca", - "ClientId": "556d438d-2f4b-4add-9713-ede4e5f5d7da", + "TenantId": "10c419d4-4a50-45b2-aa4e-919fb84df24f", + "ClientId": "a021aff4-57ad-453a-bae8-e4192e5860f3", "Scopes": "Weather.All", "TokenDecryptionCredentials": [ ] diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index 225d9c587..f211b0b95 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -34,7 +34,7 @@ 5.0.3 8.0.0 - 1.0.2 + 2.0.0 4.3.4 4.3.1 4.18.4 diff --git a/tests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs b/tests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs index 0c6c7f705..a28f84234 100644 --- a/tests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs +++ b/tests/E2E Tests/AgentApplications/AgentUserIdentityTestscs.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Graph; using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Client; using Microsoft.Identity.Web; using Microsoft.IdentityModel.Tokens; @@ -19,10 +20,10 @@ namespace AgentApplicationsTests public class AgentUserIdentityTests { string instance = "https://login.microsoftonline.com/"; - string tenantId = "31a58c3b-ae9c-4448-9e8f-e9e143e800df"; // Replace with your tenant ID - string agentApplication = "c4b2d4d9-9257-4c1a-a5c0-0a4907c83411"; // Replace with the actual agent application client ID - string agentIdentity = "44250d7d-2362-4fba-9ba0-49c19ae270e0"; // Replace with the actual agent identity - string userUpn = "aui3@msidlabtoint.onmicrosoft.com"; // Replace with the actual user upn. + string tenantId = "10c419d4-4a50-45b2-aa4e-919fb84df24f"; // Replace with your tenant ID + string agentApplication = "aab5089d-e764-47e3-9f28-cc11c2513821"; // Replace with the actual agent application client ID + string agentIdentity = "ab18ca07-d139-4840-8b3b-4be9610c6ed5"; // Replace with the actual agent identity + string userUpn = "agentuser1@id4slab1.onmicrosoft.com"; // Replace with the actual user upn. [Fact] public async Task AgentUserIdentityGetsTokenForGraphAsync() @@ -251,10 +252,83 @@ public async Task AgentUserIdentityGetsTokenForGraphWithCacheAsync() #endif } + [Fact] + public async Task AgentUserIdentityGetsTokenForGraphAsyncInvalidCertificate() + { + IServiceCollection services = new ServiceCollection(); + + // Configure the information about the agent application with an INVALID secret + services.Configure( + options => + { + options.Instance = instance; + options.TenantId = tenantId; + options.ClientId = agentApplication; + options.ClientCredentials = [ + new CredentialDescription + { + SourceType = CredentialSource.ClientSecret, + ClientSecret = "invalid-secret-that-will-fail-authentication" + } + ]; + }); + IServiceProvider serviceProvider = services.ConfigureServicesForAgentIdentitiesTests(); + + // Get an authorization header provider + IAuthorizationHeaderProvider authorizationHeaderProvider = serviceProvider.GetService()!; + AuthorizationHeaderProviderOptions options = new AuthorizationHeaderProviderOptions().WithAgentUserIdentity( + agentApplicationId: agentIdentity, + username: userUpn + ); + + // Track start time to verify the request doesn't retry infinitely + var startTime = DateTime.UtcNow; + + // Assert that authentication fails with invalid credentials + Exception? caughtException = null; + try + { + string authorizationHeaderWithUserToken = await authorizationHeaderProvider.CreateAuthorizationHeaderForUserAsync( + scopes: ["https://graph.microsoft.com/.default"], + options); + + // If we got here, no exception was thrown - this is unexpected + Assert.Fail($"Expected authentication to fail with invalid secret, but it generated token."); + } + catch (MsalServiceException msalEx) + { + caughtException = msalEx; + + // Calculate duration to ensure it doesn't hang indefinitely + var duration = DateTime.UtcNow - startTime; + + // Verify it failed within a reasonable timeframe (no infinite retry loop) + Assert.True(duration.TotalSeconds < 30, + $"Authentication failure took too long ({duration.TotalSeconds} seconds), indicating possible retry loop issue"); + + // Verify the exception indicates authentication failure + string message = msalEx.Message; + Assert.Contains("AADSTS7000215", message, StringComparison.Ordinal); + } + catch (Exception ex) + { + caughtException = ex; + + // Calculate duration + var duration = DateTime.UtcNow - startTime; + + // Verify it failed within a reasonable timeframe + Assert.True(duration.TotalSeconds < 30, + $"Authentication failure took too long ({duration.TotalSeconds} seconds), indicating possible retry loop issue"); + } + + + } + [Fact] public async Task AgentUserIdentityGetsTokenForGraphByUserIdAsync() { - string userOid = "04ea4dcd-f314-476f-be31-a13707cdd11e"; // Replace with the actual user OID. + string userOid = "a02b9a5b-ea57-40c9-bf00-8aa631b549ad"; // Replace with the actual user OID. IServiceCollection services = new ServiceCollection(); diff --git a/tests/E2E Tests/AgentApplications/AutonomousAgentTests.cs b/tests/E2E Tests/AgentApplications/AutonomousAgentTests.cs index 763f627d5..1211239e2 100644 --- a/tests/E2E Tests/AgentApplications/AutonomousAgentTests.cs +++ b/tests/E2E Tests/AgentApplications/AutonomousAgentTests.cs @@ -8,7 +8,6 @@ using Microsoft.Graph; using Microsoft.Identity.Abstractions; using Microsoft.Identity.Web; -using Microsoft.Identity.Web.TokenCacheProviders.Distributed; using Microsoft.Identity.Web.TokenCacheProviders.InMemory; using Microsoft.IdentityModel.Tokens; @@ -16,15 +15,18 @@ namespace AgentApplicationsTests { public class AutonomousAgentTests { - [Fact] - public async Task AutonomousAgentGetsAppTokenForAgentIdentityToCallGraphAsync() + const string overriddenTenantId = "10c419d4-4a50-45b2-aa4e-919fb84df24f"; + [Theory] + [InlineData("organizations")] + [InlineData("10c419d4-4a50-45b2-aa4e-919fb84df24f")] + public async Task AutonomousAgentGetsAppTokenForAgentIdentityToCallGraphAsync(string configuredTenantId) { IServiceCollection services = new ServiceCollection(); IConfiguration configuration = new ConfigurationBuilder().AddInMemoryCollection().Build(); configuration["AzureAd:Instance"] = "https://login.microsoftonline.com/"; - configuration["AzureAd:TenantId"] = "31a58c3b-ae9c-4448-9e8f-e9e143e800df"; - configuration["AzureAd:ClientId"] = "d15884b6-a447-4dd5-a5a5-a668c49f6300"; // Agent application. + configuration["AzureAd:TenantId"] = configuredTenantId; // Set to the GUID or organizations + configuration["AzureAd:ClientId"] = "aab5089d-e764-47e3-9f28-cc11c2513821"; // Agent application. configuration["AzureAd:ClientCredentials:0:SourceType"] = "StoreWithDistinguishedName"; configuration["AzureAd:ClientCredentials:0:CertificateStorePath"] = "LocalMachine/My"; configuration["AzureAd:ClientCredentials:0:CertificateDistinguishedName"] = "CN=LabAuth.MSIDLab.com"; @@ -39,11 +41,15 @@ public async Task AutonomousAgentGetsAppTokenForAgentIdentityToCallGraphAsync() services.AddMicrosoftGraph(); // If you want to call Microsoft Graph var serviceProvider = services.BuildServiceProvider(); - string agentIdentity = "d84da24a-2ea2-42b8-b5ab-8637ec208024"; // Replace with the actual agent identity + string agentIdentity = "ab18ca07-d139-4840-8b3b-4be9610c6ed5"; // Replace with the actual agent identity //// Get an authorization header and handle the call to the downstream API yoursel IAuthorizationHeaderProvider authorizationHeaderProvider = serviceProvider.GetService()!; AuthorizationHeaderProviderOptions options = new AuthorizationHeaderProviderOptions().WithAgentIdentity(agentIdentity); + if (configuredTenantId == "organizations") + { + options.AcquireTokenOptions.Tenant = overriddenTenantId; + } //// Request user tokens in autonomous agents. string authorizationHeaderWithAppToken = await authorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync("https://graph.microsoft.com/.default", options); @@ -56,7 +62,7 @@ public async Task AutonomousAgentGetsAppTokenForAgentIdentityToCallGraphAsync() // Verify the token does not represent an agent user identity using the extension method Assert.False(claimsIdentity.IsAgentUserIdentity()); - + // Verify we can retrieve the parent agent blueprint if present string? parentBlueprint = claimsIdentity.GetParentAgentBlueprint(); string agentApplication = configuration["AzureAd:ClientId"]!; @@ -65,10 +71,11 @@ public async Task AutonomousAgentGetsAppTokenForAgentIdentityToCallGraphAsync() //// If you want to call Microsoft Graph, just inject and use the Microsoft Graph SDK with the agent identity. GraphServiceClient graphServiceClient = serviceProvider.GetRequiredService(); var apps = await graphServiceClient.Applications.GetAsync(r => r.Options.WithAuthenticationOptions(options => - { - options.WithAgentIdentity(agentIdentity); - options.RequestAppToken = true; - })); + { + options.WithAgentIdentity(agentIdentity); + options.RequestAppToken = true; + options.AcquireTokenOptions.Tenant = configuredTenantId == "organizations" ? overriddenTenantId : null; + })); Assert.NotNull(apps); //// If you want to call downstream APIs letting IdWeb handle authentication. diff --git a/tests/E2E Tests/AgentApplications/GetFicAsyncTests.cs b/tests/E2E Tests/AgentApplications/GetFicAsyncTests.cs index 4bd10bdb6..1de0e3d34 100644 --- a/tests/E2E Tests/AgentApplications/GetFicAsyncTests.cs +++ b/tests/E2E Tests/AgentApplications/GetFicAsyncTests.cs @@ -17,9 +17,9 @@ public class GetFicAsyncTests public async Task GetFicTokensTestsAsync() { string instance = "https://login.microsoftonline.com/"; - string tenantId = "31a58c3b-ae9c-4448-9e8f-e9e143e800df"; // Replace with your tenant ID - string agentApplication = "d15884b6-a447-4dd5-a5a5-a668c49f6300"; // Replace with the actual agent application client ID - string agentIdentity = "d84da24a-2ea2-42b8-b5ab-8637ec208024"; // Replace with the actual agent identity + string tenantId = "10c419d4-4a50-45b2-aa4e-919fb84df24f"; // Replace with your tenant ID + string agentApplication = "aab5089d-e764-47e3-9f28-cc11c2513821"; // Replace with the actual agent application client ID + string agentIdentity = "ab18ca07-d139-4840-8b3b-4be9610c6ed5"; // Replace with the actual agent identity IServiceCollection services = new ServiceCollection(); diff --git a/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs b/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs index d63f95d8d..363c39659 100644 --- a/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs +++ b/tests/E2E Tests/CustomSignedAssertionProviderTests/CustomSignedAssertionProviderExtensibilityTests.cs @@ -27,12 +27,12 @@ public async Task UseSignedAssertionFromCustomSignedAssertionProvider() // this is how the authentication options can be configured in code rather than // in the appsettings file, though using the appsettings file is recommended - /* + /* tokenAcquirerFactory.Services.Configure(options => { options.Instance = "https://login.microsoftonline.com/"; - options.TenantId = "msidlab4.onmicrosoft.com"; - options.ClientId = "f6b698c0-140c-448f-8155-4aa9bf77ceba"; + options.TenantId = "id4slab1.onmicrosoft.com"; + options.ClientId = "4ebc2cfc-14bf-4c88-9678-26543ec1c59d"; options.ClientCredentials = [ new CredentialDescription() { SourceType = CredentialSource.CustomSignedAssertion, CustomSignedAssertionProviderName = "MyCustomExtension" diff --git a/tests/E2E Tests/CustomSignedAssertionProviderTests/appsettings.json b/tests/E2E Tests/CustomSignedAssertionProviderTests/appsettings.json index 991d7b190..f6461f857 100644 --- a/tests/E2E Tests/CustomSignedAssertionProviderTests/appsettings.json +++ b/tests/E2E Tests/CustomSignedAssertionProviderTests/appsettings.json @@ -1,8 +1,8 @@ { "AzureAd": { "Instance": "https://login.microsoftonline.com/", - "TenantId": "msidlab4.onmicrosoft.com", - "ClientId": "f6b698c0-140c-448f-8155-4aa9bf77ceba", + "TenantId": "id4slab1.onmicrosoft.com", + "ClientId": "4ebc2cfc-14bf-4c88-9678-26543ec1c59d", "ClientCredentials": [ { "SourceType": "CustomSignedAssertion", diff --git a/tests/E2E Tests/IntegrationTestService/Startup.cs b/tests/E2E Tests/IntegrationTestService/Startup.cs index eda1094b6..27fef74d2 100644 --- a/tests/E2E Tests/IntegrationTestService/Startup.cs +++ b/tests/E2E Tests/IntegrationTestService/Startup.cs @@ -8,7 +8,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Identity.Web; using Microsoft.Identity.Web.Test.Common; -using Microsoft.Identity.Lab.Api; +using Microsoft.Identity.Test.LabInfrastructure; using Microsoft.Identity.Web.TokenCacheProviders.InMemory; namespace IntegrationTestService @@ -25,9 +25,6 @@ public Startup(IConfiguration configuration) // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { - KeyVaultSecretsProvider keyVaultSecretsProvider = new(); - string secret = keyVaultSecretsProvider.GetSecretByName(TestConstants.OBOClientKeyVaultUri).Value; - services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddMicrosoftIdentityWebApi(Configuration, jwtBearerScheme: JwtBearerDefaults.AuthenticationScheme, subscribeToJwtBearerMiddlewareDiagnosticsEvents: true) .EnableTokenAcquisitionToCallDownstreamApi() @@ -47,16 +44,6 @@ public void ConfigureServices(IServiceCollection services) // Will be overriden by tests if needed services.AddInMemoryTokenCaches(); - services.Configure(JwtBearerDefaults.AuthenticationScheme, options => - { - options.ClientSecret = secret; - }); - - services.Configure(TestConstants.CustomJwtScheme2, options => - { - options.ClientSecret = secret; - }); - services.AddAuthorization(); services.AddRazorPages(); diff --git a/tests/E2E Tests/IntegrationTestService/appsettings.json b/tests/E2E Tests/IntegrationTestService/appsettings.json index a5ec75693..364f5d9d0 100644 --- a/tests/E2E Tests/IntegrationTestService/appsettings.json +++ b/tests/E2E Tests/IntegrationTestService/appsettings.json @@ -1,15 +1,29 @@ { "AzureAd": { "Instance": "https://login.microsoftonline.com/", - "Domain": "msidlab4.onmicrosoft.com", + "Domain": "id4slab1.onmicrosoft.com", "TenantId": "common", - "ClientId": "f4aa5217-e87c-42b2-82af-5624dd14ee72" + "ClientId": "8837cde9-4029-4bfc-9259-e9e70ce670f7", + "ClientCertificates": [ + { + "SourceType": "KeyVault", + "KeyVaultUrl": "https://msidlabs.vault.azure.net", + "KeyVaultCertificateName": "LabAuth" + } + ] }, "AzureAd2": { "Instance": "https://login.microsoftonline.com/", - "Domain": "msidlab4.onmicrosoft.com", + "Domain": "id4slab1.onmicrosoft.com", "TenantId": "common", - "ClientId": "f4aa5217-e87c-42b2-82af-5624dd14ee72" + "ClientId": "8837cde9-4029-4bfc-9259-e9e70ce670f7", + "ClientCertificates": [ + { + "SourceType": "KeyVault", + "KeyVaultUrl": "https://msidlabs.vault.azure.net", + "KeyVaultCertificateName": "LabAuth" + } + ] }, "CalledApi": { "Scopes": [ "user.read" ], diff --git a/tests/E2E Tests/OidcIdPSignedAssertionProviderTests/OidCIdPSignedAssertionProviderExtensibilityTests.cs b/tests/E2E Tests/OidcIdPSignedAssertionProviderTests/OidCIdPSignedAssertionProviderExtensibilityTests.cs index c149115b4..b643758e6 100644 --- a/tests/E2E Tests/OidcIdPSignedAssertionProviderTests/OidCIdPSignedAssertionProviderExtensibilityTests.cs +++ b/tests/E2E Tests/OidcIdPSignedAssertionProviderTests/OidCIdPSignedAssertionProviderExtensibilityTests.cs @@ -3,14 +3,17 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Net.Http; using System.Text.Json; using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Test.LabInfrastructure; using Microsoft.Identity.Web; using Microsoft.Identity.Web.Test.Common; using Microsoft.Identity.Web.Test.Common.Mocks; @@ -35,19 +38,22 @@ public class OidCIdPSignedAssertionProviderExtensibilityTests [OnlyOnAzureDevopsFact] public async Task CrossCloudFicIntegrationTest() { + // Arrange TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest(); TokenAcquirerFactory tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance(); tokenAcquirerFactory.Services.AddOidcFic(); + UpdateClientSecret(tokenAcquirerFactory); // for test only - get secret from KeyVault + // this is how the authentication options can be configured in code rather than // in the appsettings file, though using the appsettings file is recommended - /* + /* tokenAcquirerFactory.Services.Configure(options => { options.Instance = "https://login.microsoftonline.com/"; - options.TenantId = "msidlab4.onmicrosoft.com"; - options.ClientId = "5e71875b-ae52-4a3c-8b82-f6fdc8e1dbe1"; + options.TenantId = "id4slab1.onmicrosoft.com"; + options.ClientId = "af83f987-992d-4219-af18-d200268d3a89"; options.ClientCredentials = [ new CredentialDescription() { SourceType = CredentialSource.CustomSignedAssertion, CustomSignedAssertionProviderName = "MyCustomExtension" @@ -78,6 +84,42 @@ public async Task CrossCloudFicIntegrationTest() Assert.Contains("cp1", xmsCcValues); } + private static void UpdateClientSecret(TokenAcquirerFactory tokenAcquirerFactory) + { + KeyVaultSecretsProvider ksp = new KeyVaultSecretsProvider(); + var secret = ksp.GetSecretByName("ARLMSIDLAB1-IDLASBS-App-CC-Secret").Value; + + + var configuration = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddEnvironmentVariables() + .Build(); + + tokenAcquirerFactory.Services.AddSingleton(configuration); + + // Bind the AzureAd2 section into the named options. + tokenAcquirerFactory.Services.Configure( + "AzureAd2", + configuration.GetSection("AzureAd2")); + + // Apply any dynamic overrides after the JSON bind. + tokenAcquirerFactory.Services.PostConfigure( + "AzureAd2", + options => + { + options.ClientCredentials = new[] + { + new CredentialDescription + { + SourceType = CredentialSource.ClientSecret, + ClientSecret = secret + } + }; + + }); + } + //[Fact(Skip ="Does not run if run with the E2E test")] [Theory] [InlineData(false)] diff --git a/tests/E2E Tests/OidcIdPSignedAssertionProviderTests/appsettings.json b/tests/E2E Tests/OidcIdPSignedAssertionProviderTests/appsettings.json index 2d7bd7b7d..b8b94d5f3 100644 --- a/tests/E2E Tests/OidcIdPSignedAssertionProviderTests/appsettings.json +++ b/tests/E2E Tests/OidcIdPSignedAssertionProviderTests/appsettings.json @@ -2,9 +2,9 @@ "$schema": "https://raw.githubusercontent.com/AzureAD/microsoft-identity-web/refs/heads/master/JsonSchemas/microsoft-identity-web.json", "AzureAd": { "Instance": "https://login.microsoftonline.com/", - "TenantId": "msidlab4.onmicrosoft.com", + "TenantId": "id4slab1.onmicrosoft.com", "ExtraQueryParameters": { "dc": "ESTS-PUB-WEULR1-AZ1-FD000-TEST1" }, - "ClientId": "5e71875b-ae52-4a3c-8b82-f6fdc8e1dbe1", // this app is configured to trust credentials (tokens) from f6b698c0-140c-448f-8155-4aa9bf77ceba + "ClientId": "af83f987-992d-4219-af18-d200268d3a89", // this app is configured to trust credentials (tokens) from 4ebc2cfc-14bf-4c88-9678-26543ec1c59d "ClientCapabilities": [ "cp1" ], "ClientCredentials": [ { @@ -19,14 +19,13 @@ "AzureAd2": { "Instance": "https://login.microsoftonline.us/", "TenantId": "45ff0c17-f8b5-489b-b7fd-2fedebbec0c4", - "ClientId": "f13080ee-01fe-48c1-8e9f-f0dd6f69ac7b", + "ClientId": "c0555d2d-02f2-4838-802e-3463422e571d", "ExtraQueryParameters": { "dc": "ESTS-PUB-WEULR1-AZ1-FD000-TEST1" }, "SendX5C": true, "ClientCredentials": [ { - "SourceType": "StoreWithDistinguishedName", - "CertificateStorePath": "CurrentUser/My", - "CertificateDistinguishedName": "CN=LabAuth.MSIDLab.com" + "SourceType": "ClientSecret", + "ClientSecret": "placeholder" } ] } diff --git a/tests/E2E Tests/Sidecar.Tests/DownstreamApiOptionsMergeTests.cs b/tests/E2E Tests/Sidecar.Tests/DownstreamApiOptionsMergeTests.cs index ebbe22715..718947df1 100644 --- a/tests/E2E Tests/Sidecar.Tests/DownstreamApiOptionsMergeTests.cs +++ b/tests/E2E Tests/Sidecar.Tests/DownstreamApiOptionsMergeTests.cs @@ -3,7 +3,6 @@ using Microsoft.Identity.Abstractions; using Microsoft.Identity.Web.Sidecar; -using Microsoft.Identity.Web.Sidecar.Endpoints; using Xunit; namespace Sidecar.Tests; @@ -553,4 +552,208 @@ public void MergeDownstreamApiOptionsOverrides_WithRightExtraQueryParametersButL Assert.Single(result.ExtraQueryParameters); Assert.Equal("value1", result.ExtraQueryParameters["param1"]); } + + [Fact] + public void MergeDownstreamApiOptionsOverrides_WithRequestAppTokenTrue_OverridesRequestAppToken() + { + // Arrange + var left = new DownstreamApiOptions + { + RequestAppToken = false + }; + var right = new DownstreamApiOptions + { + RequestAppToken = true + }; + + // Act + var result = DownstreamApiOptionsMerger.MergeOptions(left, right); + + // Assert + Assert.True(result.RequestAppToken); + } + + [Fact] + public void MergeDownstreamApiOptionsOverrides_WithRequestAppTokenFalse_DoesNotOverride() + { + // Arrange + var left = new DownstreamApiOptions + { + RequestAppToken = true + }; + var right = new DownstreamApiOptions + { + RequestAppToken = false + }; + + // Act + var result = DownstreamApiOptionsMerger.MergeOptions(left, right); + + // Assert - left value preserved because right is false (default) + Assert.True(result.RequestAppToken); + } + + [Fact] + public void MergeDownstreamApiOptionsOverrides_WithBaseUrlOverride_OverridesBaseUrl() + { + // Arrange + var left = new DownstreamApiOptions + { + BaseUrl = "https://original.api.com/" + }; + var right = new DownstreamApiOptions + { + BaseUrl = "https://new.api.com/" + }; + + // Act + var result = DownstreamApiOptionsMerger.MergeOptions(left, right); + + // Assert + Assert.Equal("https://new.api.com/", result.BaseUrl); + } + + [Fact] + public void MergeDownstreamApiOptionsOverrides_WithHttpMethodOverride_OverridesHttpMethod() + { + // Arrange + var left = new DownstreamApiOptions + { + HttpMethod = "GET" + }; + var right = new DownstreamApiOptions + { + HttpMethod = "POST" + }; + + // Act + var result = DownstreamApiOptionsMerger.MergeOptions(left, right); + + // Assert + Assert.Equal("POST", result.HttpMethod); + } + + [Fact] + public void MergeDownstreamApiOptionsOverrides_WithManagedIdentityOverride_OverridesManagedIdentity() + { + // Arrange + var left = new DownstreamApiOptions + { + AcquireTokenOptions = new AcquireTokenOptions + { + ManagedIdentity = null + } + }; + var right = new DownstreamApiOptions + { + AcquireTokenOptions = new AcquireTokenOptions + { + ManagedIdentity = new ManagedIdentityOptions + { + UserAssignedClientId = "test-client-id" + } + } + }; + + // Act + var result = DownstreamApiOptionsMerger.MergeOptions(left, right); + + // Assert + Assert.NotNull(result.AcquireTokenOptions.ManagedIdentity); + Assert.Equal("test-client-id", result.AcquireTokenOptions.ManagedIdentity.UserAssignedClientId); + } + + /// + /// This test uses reflection to ensure all properties of DownstreamApiOptions are handled by the merger. + /// If a new property is added to DownstreamApiOptions and not handled, this test will fail. + /// + [Fact] + public void MergeDownstreamApiOptionsOverrides_AllPropertiesAreCopied() + { + // Arrange - Properties that are expected to be merged from DownstreamApiOptions + var handledProperties = new HashSet(StringComparer.OrdinalIgnoreCase) + { + // Direct properties + "Scopes", + "RequestAppToken", + "BaseUrl", + "RelativePath", + "HttpMethod", + "ContentType", + "AcceptHeader", + "ExtraHeaderParameters", + "ExtraQueryParameters", + // AcquireTokenOptions is handled specially (nested object) + "AcquireTokenOptions", + // Properties intentionally not merged (they use CustomizeHttpRequestMessage pattern or are clone-only) + "Clone", + "CustomizeHttpRequestMessage", + "Serializer", + "Deserializer", + "ProtocolScheme", + }; + + // Act - Get all public instance properties of DownstreamApiOptions + var allProperties = typeof(DownstreamApiOptions) + .GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance) + .Where(p => p.CanRead && p.CanWrite) + .Select(p => p.Name) + .ToList(); + + // Assert - Every property should be in our handled list + var unhandledProperties = allProperties + .Where(p => !handledProperties.Contains(p)) + .ToList(); + + Assert.True( + unhandledProperties.Count == 0, + $"The following properties of DownstreamApiOptions are not handled by the merger: {string.Join(", ", unhandledProperties)}. " + + "Please add handling for these properties in DownstreamApiOptionsMerger.MergeOptions() and add them to this test's handledProperties list."); + } + + /// + /// This test uses reflection to ensure all properties of AcquireTokenOptions are handled by the merger. + /// + [Fact] + public void MergeDownstreamApiOptionsOverrides_AllAcquireTokenOptionsPropertiesAreCopied() + { + // Arrange - Properties that are expected to be merged from AcquireTokenOptions + var handledProperties = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "Tenant", + "Claims", + "AuthenticationOptionsName", + "FmiPath", + "LongRunningWebApiSessionKey", + "PopPublicKey", + "CorrelationId", + "ManagedIdentity", + "ForceRefresh", + "ExtraQueryParameters", + "ExtraParameters", + "ExtraHeadersParameters", + "PopClaim", + // Properties intentionally not merged + "UserFlow", + "PopCryptoProvider", + "PoPConfiguration", + }; + + // Act - Get all public instance properties of AcquireTokenOptions + var allProperties = typeof(AcquireTokenOptions) + .GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance) + .Where(p => p.CanRead && p.CanWrite) + .Select(p => p.Name) + .ToList(); + + // Assert - Every property should be in our handled list + var unhandledProperties = allProperties + .Where(p => !handledProperties.Contains(p)) + .ToList(); + + Assert.True( + unhandledProperties.Count == 0, + $"The following properties of AcquireTokenOptions are not handled by the merger: {string.Join(", ", unhandledProperties)}. " + + "Please add handling for these properties in DownstreamApiOptionsMerger.MergeOptions() and add them to this test's handledProperties list."); + } } diff --git a/tests/E2E Tests/Sidecar.Tests/SidecarApiFactory.cs b/tests/E2E Tests/Sidecar.Tests/SidecarApiFactory.cs index dcf5632ce..cda4ec45f 100644 --- a/tests/E2E Tests/Sidecar.Tests/SidecarApiFactory.cs +++ b/tests/E2E Tests/Sidecar.Tests/SidecarApiFactory.cs @@ -24,9 +24,10 @@ internal SidecarApiFactory(Action? configureOptions) builder.AddInMemoryCollection(new Dictionary { { "AzureAd:Instance", "https://login.microsoftonline.com/" }, - { "AzureAd:TenantId", "31a58c3b-ae9c-4448-9e8f-e9e143e800df" }, - { "AzureAd:ClientId", "d15884b6-a447-4dd5-a5a5-a668c49f6300" }, - { "AzureAd:Audience", "d15884b6-a447-4dd5-a5a5-a668c49f6300" }, + { "AzureAd:TenantId", "10c419d4-4a50-45b2-aa4e-919fb84df24f" }, + { "AzureAd:ClientId", "aab5089d-e764-47e3-9f28-cc11c2513821" }, + { "AzureAd:Audience", "aab5089d-e764-47e3-9f28-cc11c2513821" }, + { "AzureAd:AllowWebApiToBeAuthorizedByACL", "true" }, { "AzureAd:ClientCredentials:0:SourceType", "StoreWithDistinguishedName" }, { "AzureAd:ClientCredentials:0:CertificateStorePath", "LocalMachine/My" }, { "AzureAd:ClientCredentials:0:CertificateDistinguishedName", "CN=LabAuth.MSIDLab.com" }, // Replace with the subject name of your certificate diff --git a/tests/E2E Tests/Sidecar.Tests/SidecarEndpointsE2ETests.cs b/tests/E2E Tests/Sidecar.Tests/SidecarEndpointsE2ETests.cs index a89b5b1e9..fc189d939 100644 --- a/tests/E2E Tests/Sidecar.Tests/SidecarEndpointsE2ETests.cs +++ b/tests/E2E Tests/Sidecar.Tests/SidecarEndpointsE2ETests.cs @@ -22,11 +22,17 @@ public class SidecarEndpointsE2ETests : IClassFixture public SidecarEndpointsE2ETests(SidecarApiFactory factory) => _factory = factory; - const string TenantId = "31a58c3b-ae9c-4448-9e8f-e9e143e800df"; // Replace with your tenant ID - const string AgentApplication = "d15884b6-a447-4dd5-a5a5-a668c49f6300"; // Replace with the actual agent application client ID - const string AgentIdentity = "d84da24a-2ea2-42b8-b5ab-8637ec208024"; // Replace with the actual agent identity - const string UserUpn = "aui1@msidlabtoint.onmicrosoft.com"; // Replace with the actual user upn. - string UserOid = "51c1aa1c-f6d0-4a92-936c-cadb27b717f2"; // Replace with the actual user OID. + const string TenantId = "10c419d4-4a50-45b2-aa4e-919fb84df24f"; // Replace with your tenant ID + const string AgentApplication = "aab5089d-e764-47e3-9f28-cc11c2513821"; // Replace with the actual agent application client ID + const string TestClientApplication = "825940df-c1fb-4604-8104-02965f55b1ee"; // Replace with the client application used for app-only calls + const string AgentIdentity = "ab18ca07-d139-4840-8b3b-4be9610c6ed5"; // Replace with the actual agent identity + const string UserUpn = "agentuser1@id4slab1.onmicrosoft.com"; // Replace with the actual user upn. + const string UserOid = "a02b9a5b-ea57-40c9-bf00-8aa631b549ad"; // Replace with the actual user OID. + const string Instance = "https://login.microsoftonline.com/"; // Replace with the Entra ID authority instance + const string UserReadScope = "user.read"; // Replace with the scope used for user calls + const string CertificateStorePath = "LocalMachine/My"; // Replace with the certificate store path + const string CertificateDistinguishedName = "CN=LabAuth.MSIDLab.com"; // Replace with the certificate subject name + static readonly string AgentApplicationScope = $"api://{AgentApplication}/.default"; // Replace with the API scope for the agent application [Fact] public async Task Validate_WhenBadTokenAsync() @@ -129,7 +135,7 @@ public async Task GetAuthorizationHeaderForAgentUserIdentityUnauthenticated_With var client = _factory.CreateClient(); var result = await client.GetAsync( - $"/AuthorizationHeaderUnauthenticated/AgentUserIdentityCallsGraph?AgentIdentity={AgentIdentity}&AgentUsername={UserUpn}&OptionsOverride.Tenant={TenantId}&OptionsOverride.Scopes=user.read"); + $"/AuthorizationHeaderUnauthenticated/AgentUserIdentityCallsGraph?AgentIdentity={AgentIdentity}&AgentUsername={UserUpn}&OptionsOverride.Tenant={TenantId}&OptionsOverride.Scopes={UserReadScope}"); Assert.True(result.IsSuccessStatusCode); @@ -163,7 +169,7 @@ public async Task TestAgentIdentityConfiguration_InvalidTenant() var client = _factory.CreateClient(); var result = await client.GetAsync( - $"/AuthorizationHeaderUnauthenticated/AgentUserIdentityCallsGraph?AgentIdentity={AgentIdentity}&AgentUsername={UserUpn}&OptionsOverride.AcquireTokenOptions.Tenant=invalid-tenant&OptionsOverride.Scopes=user.read"); + $"/AuthorizationHeaderUnauthenticated/AgentUserIdentityCallsGraph?AgentIdentity={AgentIdentity}&AgentUsername={UserUpn}&OptionsOverride.AcquireTokenOptions.Tenant=invalid-tenant&OptionsOverride.Scopes={UserReadScope}"); Assert.Equal(HttpStatusCode.Unauthorized, result.StatusCode); } @@ -174,20 +180,20 @@ private static async Task GetAuthorizationHeaderToCallTheSideCarAsync() ServiceCollection services = new(); IConfiguration configuration = new ConfigurationBuilder().AddInMemoryCollection().Build(); services.AddSingleton(configuration); - configuration["Instance"] = "https://login.microsoftonline.com/"; - configuration["TenantId"] = "31a58c3b-ae9c-4448-9e8f-e9e143e800df"; - configuration["ClientId"] = "5cbcd9ff-c994-49ac-87e7-08a93a9c0794"; + configuration["Instance"] = Instance; + configuration["TenantId"] = TenantId; + configuration["ClientId"] = TestClientApplication; configuration["SendX5C"] = "true"; configuration["ClientCredentials:0:SourceType"] = "StoreWithDistinguishedName"; - configuration["ClientCredentials:0:CertificateStorePath"] = "LocalMachine/My"; - configuration["ClientCredentials:0:CertificateDistinguishedName"] = "CN=LabAuth.MSIDLab.com"; + configuration["ClientCredentials:0:CertificateStorePath"] = CertificateStorePath; + configuration["ClientCredentials:0:CertificateDistinguishedName"] = CertificateDistinguishedName; services.AddTokenAcquisition().AddHttpClient().AddInMemoryTokenCaches(); services.Configure(configuration); IServiceProvider serviceProvider = services.BuildServiceProvider(); IAuthorizationHeaderProvider authorizationHeaderProvider = serviceProvider.GetRequiredService(); - string authorizationHeader = await authorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync("api://d15884b6-a447-4dd5-a5a5-a668c49f6300/.default", + string authorizationHeader = await authorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync(AgentApplicationScope, new AuthorizationHeaderProviderOptions() { AcquireTokenOptions = new AcquireTokenOptions() diff --git a/tests/E2E Tests/SimulateOidc/Properties/openid-configuration b/tests/E2E Tests/SimulateOidc/Properties/openid-configuration index 03e3b7528..e5a4d1d7e 100644 --- a/tests/E2E Tests/SimulateOidc/Properties/openid-configuration +++ b/tests/E2E Tests/SimulateOidc/Properties/openid-configuration @@ -1,5 +1,5 @@ { - "token_endpoint": "https://login.microsoftonline.com/f645ad92-e38d-4d1a-b510-d1b09a74a8ca/oauth2/v2.0/token", + "token_endpoint": "https://login.microsoftonline.com/10c419d4-4a50-45b2-aa4e-919fb84df24f/oauth2/v2.0/token", "token_endpoint_auth_methods_supported": [ "client_secret_post", "private_key_jwt", @@ -29,14 +29,14 @@ "email", "offline_access" ], - "issuer": "https://login.microsoftonline.com/f645ad92-e38d-4d1a-b510-d1b09a74a8ca/v2.0", + "issuer": "https://login.microsoftonline.com/10c419d4-4a50-45b2-aa4e-919fb84df24f/v2.0", "request_uri_parameter_supported": false, "userinfo_endpoint": "https://graph.microsoft.com/oidc/userinfo", - "authorization_endpoint": "https://login.microsoftonline.com/f645ad92-e38d-4d1a-b510-d1b09a74a8ca/oauth2/v2.0/authorize", - "device_authorization_endpoint": "https://login.microsoftonline.com/f645ad92-e38d-4d1a-b510-d1b09a74a8ca/oauth2/v2.0/devicecode", + "authorization_endpoint": "https://login.microsoftonline.com/10c419d4-4a50-45b2-aa4e-919fb84df24f/oauth2/v2.0/authorize", + "device_authorization_endpoint": "https://login.microsoftonline.com/10c419d4-4a50-45b2-aa4e-919fb84df24f/oauth2/v2.0/devicecode", "http_logout_supported": true, "frontchannel_logout_supported": true, - "end_session_endpoint": "https://login.microsoftonline.com/f645ad92-e38d-4d1a-b510-d1b09a74a8ca/oauth2/v2.0/logout", + "end_session_endpoint": "https://login.microsoftonline.com/10c419d4-4a50-45b2-aa4e-919fb84df24f/oauth2/v2.0/logout", "claims_supported": [ "sub", "iss", @@ -58,7 +58,7 @@ "c_hash", "email" ], - "kerberos_endpoint": "https://login.microsoftonline.com/f645ad92-e38d-4d1a-b510-d1b09a74a8ca/kerberos", + "kerberos_endpoint": "https://login.microsoftonline.com/10c419d4-4a50-45b2-aa4e-919fb84df24f/kerberos", "tenant_region_scope": "NA", "cloud_instance_name": "microsoftonline.com", "cloud_graph_host_name": "graph.windows.net", diff --git a/tests/E2E Tests/TokenAcquirerTests/TokenAcquirer.AuthorityMatrix.cs b/tests/E2E Tests/TokenAcquirerTests/TokenAcquirer.AuthorityMatrix.cs new file mode 100644 index 000000000..e8c442e3a --- /dev/null +++ b/tests/E2E Tests/TokenAcquirerTests/TokenAcquirer.AuthorityMatrix.cs @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Web; +using Microsoft.Identity.Web.Test.Common; +using Microsoft.Identity.Web.TestOnly; +using Microsoft.Identity.Web.TokenCacheProviders.InMemory; +using Xunit; + +namespace TokenAcquirerTests +{ + /// + /// E2E tests for authority matrix scenarios. + /// Issue #3610: E2E tests for complex authority scenarios. + /// These tests validate real token acquisition with various authority configurations. + /// +#if !FROM_GITHUB_ACTION + public partial class TokenAcquirer + { + [IgnoreOnAzureDevopsFact] + public async Task AcquireToken_AuthorityOnly_AAD_NoV2Suffix_Succeeds() + { + // Issue #3610: AAD authority without /v2.0 suffix should work + // Arrange + TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest(); + TokenAcquirerFactory tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance(); + IServiceCollection services = tokenAcquirerFactory.Services; + + services.Configure(s_optionName, option => + { + option.Authority = "https://login.microsoftonline.com/id4slab1.onmicrosoft.com"; // No /v2.0 suffix + option.ClientId = "4ebc2cfc-14bf-4c88-9678-26543ec1c59d"; + option.ClientCredentials = s_clientCredentials; + }); + + // Act & Assert + await CreateGraphClientAndAssertAsync(tokenAcquirerFactory, services); + } + + [IgnoreOnAzureDevopsFact] + public async Task AcquireToken_AuthorityOnly_AAD_WithV2Suffix_Succeeds() + { + // Issue #3610: AAD authority with /v2.0 suffix should work + // Arrange + TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest(); + TokenAcquirerFactory tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance(); + IServiceCollection services = tokenAcquirerFactory.Services; + + services.Configure(s_optionName, option => + { + option.Authority = "https://login.microsoftonline.com/id4slab1.onmicrosoft.com/v2.0"; + option.ClientId = "4ebc2cfc-14bf-4c88-9678-26543ec1c59d"; + option.ClientCredentials = s_clientCredentials; + }); + + // Act & Assert + await CreateGraphClientAndAssertAsync(tokenAcquirerFactory, services); + } + + + [IgnoreOnAzureDevopsFact] + public async Task AcquireToken_InstanceAndTenantId_AAD_Succeeds() + { + // Issue #3610: Instance + TenantId configuration should work + // Arrange + TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest(); + TokenAcquirerFactory tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance(); + IServiceCollection services = tokenAcquirerFactory.Services; + + services.Configure(s_optionName, option => + { + option.Instance = "https://login.microsoftonline.com/"; + option.TenantId = "id4slab1.onmicrosoft.com"; + option.ClientId = "4ebc2cfc-14bf-4c88-9678-26543ec1c59d"; + option.ClientCredentials = s_clientCredentials; + }); + + // Act & Assert + await CreateGraphClientAndAssertAsync(tokenAcquirerFactory, services); + } + + [IgnoreOnAzureDevopsFact] + public async Task AcquireToken_ConflictConfig_AAD_AuthorityIgnored_Succeeds() + { + // Issue #3610: When both Authority and Instance are set, Instance takes precedence + // Arrange + TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest(); + TokenAcquirerFactory tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance(); + IServiceCollection services = tokenAcquirerFactory.Services; + + services.Configure(s_optionName, option => + { + option.Authority = "https://login.microsoftonline.com/common/v2.0"; // Will be ignored + option.Instance = "https://login.microsoftonline.com/"; + option.TenantId = "id4slab1.onmicrosoft.com"; + option.ClientId = "4ebc2cfc-14bf-4c88-9678-26543ec1c59d"; + option.ClientCredentials = s_clientCredentials; + }); + + // Act & Assert - Should use Instance+TenantId, ignore Authority + await CreateGraphClientAndAssertAsync(tokenAcquirerFactory, services); + } + } +#else + public partial class TokenAcquirer + { + } +#endif +} diff --git a/tests/E2E Tests/TokenAcquirerTests/TokenAcquirer.cs b/tests/E2E Tests/TokenAcquirerTests/TokenAcquirer.cs index 84687c2a9..52be5ed4a 100644 --- a/tests/E2E Tests/TokenAcquirerTests/TokenAcquirer.cs +++ b/tests/E2E Tests/TokenAcquirerTests/TokenAcquirer.cs @@ -3,20 +3,24 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Microsoft.Graph; using Microsoft.Identity.Abstractions; -using Microsoft.Identity.Lab.Api; +using Microsoft.Identity.Test.LabInfrastructure; using Microsoft.Identity.Web; using Microsoft.Identity.Web.Test.Common; using Microsoft.Identity.Web.TestOnly; using Microsoft.Identity.Web.TokenCacheProviders.InMemory; +using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; using Xunit; using TaskStatus = System.Threading.Tasks.TaskStatus; @@ -25,7 +29,7 @@ namespace TokenAcquirerTests { [Collection(nameof(TokenAcquirerFactorySingletonProtection))] #if !FROM_GITHUB_ACTION - public class TokenAcquirer + public partial class TokenAcquirer { private static readonly string s_optionName = string.Empty; private static readonly CredentialDescription[] s_clientCredentials = new[] @@ -102,14 +106,14 @@ public async Task AcquireToken_ROPC_CCAasync() var tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance(); _ = tokenAcquirerFactory.Build(); - var labResponse = await LabUserHelper.GetSpecificUserAsync(TestConstants.OBOUser); + var userConfig = await LabResponseHelper.GetUserConfigAsync("MSAL-User-Default-JSON"); ITokenAcquirer tokenAcquirer = tokenAcquirerFactory.GetTokenAcquirer( - authority: "https://login.microsoftonline.com/organizations", - clientId: "9a192b78-6580-4f8a-aace-f36ffea4f7be", + authority: "https://login.microsoftonline.com/organizations", + clientId: "a599ce88-0a5f-4a6e-beca-e67d3fc427f4", clientCredentials: s_clientCredentials); - var user = ClaimsPrincipalFactory.FromUsernamePassword(labResponse.User.Upn, labResponse.User.GetOrFetchPassword()); + var user = ClaimsPrincipalFactory.FromUsernamePassword(userConfig.Upn, LabResponseHelper.FetchUserPassword(userConfig.LabName)); var result = await tokenAcquirer.GetTokenForUserAsync( scopes: new[] { "https://graph.microsoft.com/.default" }, user: user); @@ -132,14 +136,14 @@ public async Task AcquireToken_ROPC_CCA_WithForceRefresh_async() var tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance(); _ = tokenAcquirerFactory.Build(); - var labResponse = await LabUserHelper.GetSpecificUserAsync(TestConstants.OBOUser); + var userConfig = await LabResponseHelper.GetUserConfigAsync("MSAL-User-Default-JSON"); ITokenAcquirer tokenAcquirer = tokenAcquirerFactory.GetTokenAcquirer( - authority: "https://login.microsoftonline.com/organizations", - clientId: "9a192b78-6580-4f8a-aace-f36ffea4f7be", + authority: "https://login.microsoftonline.com/organizations", + clientId: "a599ce88-0a5f-4a6e-beca-e67d3fc427f4", clientCredentials: s_clientCredentials); - var user = ClaimsPrincipalFactory.FromUsernamePassword(labResponse.User.Upn, labResponse.User.GetOrFetchPassword()); + var user = ClaimsPrincipalFactory.FromUsernamePassword(userConfig.Upn, LabResponseHelper.FetchUserPassword(userConfig.LabName)); var result = await tokenAcquirer.GetTokenForUserAsync( scopes: new[] { "https://graph.microsoft.com/.default" }, user: user); @@ -209,8 +213,8 @@ public async Task AcquireToken_WithMicrosoftIdentityOptions_ClientCredentialsAsy services.Configure(s_optionName, option => { option.Instance = "https://login.microsoftonline.com/"; - option.TenantId = "msidlab4.onmicrosoft.com"; - option.ClientId = "f6b698c0-140c-448f-8155-4aa9bf77ceba"; + option.TenantId = "id4slab1.onmicrosoft.com"; + option.ClientId = "4ebc2cfc-14bf-4c88-9678-26543ec1c59d"; if (withClientCredentials) { option.ClientCertificates = s_clientCredentials.OfType(); @@ -235,8 +239,26 @@ public async Task AcquireToken_WithMicrosoftIdentityApplicationOptions_ClientCre services.Configure(s_optionName, option => { option.Instance = "https://login.microsoftonline.com/"; - option.TenantId = "msidlab4.onmicrosoft.com"; - option.ClientId = "f6b698c0-140c-448f-8155-4aa9bf77ceba"; + option.TenantId = "id4slab1.onmicrosoft.com"; + option.ClientId = "4ebc2cfc-14bf-4c88-9678-26543ec1c59d"; + option.ClientCredentials = s_clientCredentials; + }); + + await CreateGraphClientAndAssertAsync(tokenAcquirerFactory, services); + } + + [IgnoreOnAzureDevopsFact] + //[Fact] + public async Task AcquireToken_WithMicrosoftIdentityApplicationOptions_Authority_ClientCredentialsAsync() + { + TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest(); + TokenAcquirerFactory tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance(); + IServiceCollection services = tokenAcquirerFactory.Services; + + services.Configure(s_optionName, option => + { + option.Authority = "https://login.microsoftonline.com/id4slab1.onmicrosoft.com/v2.0"; + option.ClientId = "4ebc2cfc-14bf-4c88-9678-26543ec1c59d"; option.ClientCredentials = s_clientCredentials; }); @@ -273,8 +295,8 @@ public async Task AcquireToken_WithFactoryAndMicrosoftIdentityApplicationOptions // Get the token acquirer from the options. var tokenAcquirer = tokenAcquirerFactory.GetTokenAcquirer(new MicrosoftIdentityApplicationOptions { - ClientId = "f6b698c0-140c-448f-8155-4aa9bf77ceba", - Authority = "https://login.microsoftonline.com/msidlab4.onmicrosoft.com", + ClientId = "4ebc2cfc-14bf-4c88-9678-26543ec1c59d", + Authority = "https://login.microsoftonline.com/id4slab1.onmicrosoft.com", ClientCredentials = s_clientCredentials }); @@ -292,8 +314,8 @@ public async Task AcquireToken_WithFactoryAndAuthorityClientIdCert_ClientCredent tokenAcquirerFactory.Build(); var tokenAcquirer = tokenAcquirerFactory.GetTokenAcquirer( - authority: "https://login.microsoftonline.com/msidlab4.onmicrosoft.com", - clientId: "f6b698c0-140c-448f-8155-4aa9bf77ceba", + authority: "https://login.microsoftonline.com/id4slab1.onmicrosoft.com", + clientId: "4ebc2cfc-14bf-4c88-9678-26543ec1c59d", clientCredentials: s_clientCredentials); var result = await tokenAcquirer.GetTokenForAppAsync("https://graph.microsoft.com/.default"); @@ -311,8 +333,8 @@ public async Task LoadCredentialsIfNeededAsync_MultipleThreads_WaitsForSemaphore services.Configure(s_optionName, option => { option.Instance = "https://login.microsoftonline.com/"; - option.TenantId = "msidlab4.onmicrosoft.com"; - option.ClientId = "f6b698c0-140c-448f-8155-4aa9bf77ceba"; + option.TenantId = "id4slab1.onmicrosoft.com"; + option.ClientId = "4ebc2cfc-14bf-4c88-9678-26543ec1c59d"; option.ClientCredentials = s_clientCredentials; }); @@ -352,8 +374,8 @@ public async Task AcquireTokenWithPop_ClientCredentialsAsync() services.Configure(s_optionName, option => { option.Instance = "https://login.microsoftonline.com/"; - option.TenantId = "msidlab4.onmicrosoft.com"; - option.ClientId = "f6b698c0-140c-448f-8155-4aa9bf77ceba"; + option.TenantId = "id4slab1.onmicrosoft.com"; + option.ClientId = "4ebc2cfc-14bf-4c88-9678-26543ec1c59d"; option.ClientCredentials = s_clientCredentials; }); @@ -382,8 +404,8 @@ public async Task AcquireTokenWithMs10AtPop_ClientCredentialsAsync() services.Configure(s_optionName, option => { option.Instance = "https://login.microsoftonline.com/"; - option.TenantId = "msidlab4.onmicrosoft.com"; - option.ClientId = "f6b698c0-140c-448f-8155-4aa9bf77ceba"; + option.TenantId = "id4slab1.onmicrosoft.com"; + option.ClientId = "4ebc2cfc-14bf-4c88-9678-26543ec1c59d"; option.ClientCredentials = s_clientCredentials; }); @@ -406,6 +428,54 @@ public async Task AcquireTokenWithMs10AtPop_ClientCredentialsAsync() Assert.NotNull(result.AccessToken); } + [IgnoreOnAzureDevopsFact] + // [Fact] + public async Task AcquireTokenWithMtlsPop_WithBindingCertificate_ReturnsMtlsPopToken() + { + // Arrange + TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest(); + TokenAcquirerFactory tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance(); + IServiceCollection services = tokenAcquirerFactory.Services; + + services.Configure(s_optionName, option => + { + option.Instance = "https://login.microsoftonline.com/"; + option.TenantId = "bea21ebe-8b64-4d06-9f6d-6a889b120a7c"; // MSI Team tenant + option.ClientId = "163ffef9-a313-45b4-ab2f-c7e2f5e0e23e"; + option.AzureRegion = "westus3"; + option.ClientCredentials = s_clientCredentials; + }); + + services.AddInMemoryTokenCaches(); + + var serviceProvider = tokenAcquirerFactory.Build(); + ITokenAcquirer tokenAcquirer = tokenAcquirerFactory.GetTokenAcquirer(s_optionName); + + var tokenAcquisitionOptions = new TokenAcquisitionOptions + { + ExtraParameters = new Dictionary + { + { "IsTokenBinding", true } // mTLS PoP + } + }; + + // Act + var result = await tokenAcquirer.GetTokenForAppAsync("https://graph.microsoft.com/.default", tokenAcquisitionOptions); + + // Assert + Assert.NotNull(result.AccessToken); + Assert.StartsWith("eyJ0e", result.AccessToken, StringComparison.OrdinalIgnoreCase); + + var jsonWebToken = new JsonWebToken(result.AccessToken); + Assert.True(jsonWebToken.TryGetPayloadValue("cnf", out object? cnfClaim), "The mTLS PoP token should contain a 'cnf' claim"); + + var cnfJson = JsonSerializer.Deserialize(cnfClaim!.ToString()!); + Assert.True(cnfJson.TryGetProperty("x5t#S256", out var x5tS256), "The mTLS PoP 'cnf' claim should contain an 'x5t#S256' property"); + + var x5tS256Value = x5tS256.GetString(); + Assert.False(string.IsNullOrEmpty(x5tS256Value)); + } + private static string CreatePopClaim(RsaSecurityKey key, string algorithm) { var parameters = key.Rsa == null ? key.Parameters : key.Rsa.ExportParameters(false); diff --git a/tests/E2E Tests/WebAppUiTests/B2CWebAppCallsWebApiLocally.cs b/tests/E2E Tests/WebAppUiTests/B2CWebAppCallsWebApiLocally.cs index e7d917cc9..1b58a11d0 100644 --- a/tests/E2E Tests/WebAppUiTests/B2CWebAppCallsWebApiLocally.cs +++ b/tests/E2E Tests/WebAppUiTests/B2CWebAppCallsWebApiLocally.cs @@ -8,7 +8,7 @@ using System.Runtime.Versioning; using System.Threading.Tasks; using Azure.Identity; -using Microsoft.Identity.Lab.Api; +using Microsoft.Identity.Test.LabInfrastructure; using Microsoft.Playwright; using Xunit; using Xunit.Abstractions; @@ -102,7 +102,6 @@ public async Task Susi_B2C_LocalAccount_TodoAppFunctionsCorrectlyAsync() if (InitialConnectionRetryCount == 0) { throw ex; } } } - LabResponse labResponse = await LabUserHelper.GetB2CLocalAccountAsync(); // Initial sign in _output.WriteLine("Starting web app sign-in flow."); diff --git a/tests/E2E Tests/WebAppUiTests/TestingWebAppLocally.cs b/tests/E2E Tests/WebAppUiTests/TestingWebAppLocally.cs index 69fe8670e..7f69c8d62 100644 --- a/tests/E2E Tests/WebAppUiTests/TestingWebAppLocally.cs +++ b/tests/E2E Tests/WebAppUiTests/TestingWebAppLocally.cs @@ -7,7 +7,7 @@ using System.IO; using System.Runtime.Versioning; using System.Threading.Tasks; -using Microsoft.Identity.Lab.Api; +using Microsoft.Identity.Test.LabInfrastructure; using Microsoft.Playwright; using Xunit; using Xunit.Abstractions; @@ -29,7 +29,7 @@ public class TestingWebAppLocally : IClassFixture(); - await ExecuteWebAppCallsGraphFlowAsync(labResponse.User.Upn, labResponse.User.GetOrFetchPassword(), clientEnvVars, TraceFileClassName); + await ExecuteWebAppCallsGraphFlowAsync(userConfig.Upn, LabResponseHelper.FetchUserPassword(userConfig.LabName), clientEnvVars, TraceFileClassName); } [Theory(Skip = "https://github.com/AzureAD/microsoft-identity-web/issues/3288")] @@ -60,7 +60,7 @@ public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailWithCiamPassw {"AzureAd__Instance", "" } }; - await ExecuteWebAppCallsGraphFlowAsync("idlab@msidlabciam6.onmicrosoft.com", LabUserHelper.FetchUserPassword("msidlabciam6"), clientEnvVars, TraceFileClassNameCiam); + await ExecuteWebAppCallsGraphFlowAsync("idlab@msidlabciam6.onmicrosoft.com", LabResponseHelper.FetchUserPassword("msidlabciam6"), clientEnvVars, TraceFileClassNameCiam); } private async Task ExecuteWebAppCallsGraphFlowAsync(string upn, string credential, Dictionary? clientEnvVars, string traceFileClassName) diff --git a/tests/E2E Tests/WebAppUiTests/UiTestHelpers.cs b/tests/E2E Tests/WebAppUiTests/UiTestHelpers.cs index 5efbb4012..2bc56447e 100644 --- a/tests/E2E Tests/WebAppUiTests/UiTestHelpers.cs +++ b/tests/E2E Tests/WebAppUiTests/UiTestHelpers.cs @@ -71,14 +71,21 @@ public static async Task PerformSignOut_MicrosoftIdFlowAsync(IPage page, string } /// - /// In the Microsoft Identity flow, the user is at certain stages presented with a list of accounts known in + /// In the Microsoft Identity flow, the user is at certain stages presented with a list of accounts known in /// the current browsing session to choose from. This method selects the account using the user's email. /// /// page for the playwright browser /// user email address to select private static async Task SelectKnownAccountByEmail_MicrosoftIdFlowAsync(IPage page, string email) { - await page.Locator($"[data-test-id=\"{email}\"]").ClickAsync(); + ILocator accountLocator = page.Locator($"[data-test-id=\"{email}\"]"); + if (await accountLocator.CountAsync() == 0) + { + string normalizedEmail = email.ToLowerInvariant(); + accountLocator = page.Locator($"[data-test-id=\"{normalizedEmail}\"]"); + } + + await accountLocator.ClickAsync(); } /// @@ -91,7 +98,7 @@ private static async Task SelectKnownAccountByEmail_MicrosoftIdFlowAsync(IPage p public static async Task EnterPassword_MicrosoftIdFlow_ValidPasswordAsync(IPage page, string password, string staySignedInText, ITestOutputHelper? output = null) { // If using an account that has other non-password validation options, the below code should be uncommented - /* WriteLine(output, "Selecting \"Password\" as authentication method"); + /* WriteLine(output, "Selecting \"Password\" as authentication method"); await page.GetByRole(AriaRole.Button, new() { Name = TestConstants.PasswordText }).ClickAsync();*/ WriteLine(output, "Logging in ... entering and submitting password."); diff --git a/tests/E2E Tests/WebAppUiTests/WebAppCallsApiCallsGraphLocally.cs b/tests/E2E Tests/WebAppUiTests/WebAppCallsApiCallsGraphLocally.cs index a3c867dac..b486a8873 100644 --- a/tests/E2E Tests/WebAppUiTests/WebAppCallsApiCallsGraphLocally.cs +++ b/tests/E2E Tests/WebAppUiTests/WebAppCallsApiCallsGraphLocally.cs @@ -7,7 +7,7 @@ using System.Runtime.Versioning; using System.Text; using System.Threading.Tasks; -using Microsoft.Identity.Lab.Api; +using Microsoft.Identity.Test.LabInfrastructure; using Microsoft.Playwright; using Xunit; using Xunit.Abstractions; @@ -96,12 +96,12 @@ public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds } page = await NavigateToWebAppAsync(context, TodoListClientPort); - LabResponse labResponse = await LabUserHelper.GetDefaultUserAsync(); + var userConfig = await LabResponseHelper.GetUserConfigAsync("MSAL-User-Default-JSON"); // Initial sign in _output.WriteLine("Starting web app sign-in flow."); - string email = labResponse.User.Upn; - await UiTestHelpers.FirstLogin_MicrosoftIdFlow_ValidEmailPasswordAsync(page, email, labResponse.User.GetOrFetchPassword(), _output); + string email = userConfig.Upn; + await UiTestHelpers.FirstLogin_MicrosoftIdFlow_ValidEmailPasswordAsync(page, email, LabResponseHelper.FetchUserPassword(userConfig.LabName), _output); await Assertions.Expect(page.GetByText("TodoList")).ToBeVisibleAsync(_assertVisibleOptions); await Assertions.Expect(page.GetByText(email)).ToBeVisibleAsync(_assertVisibleOptions); _output.WriteLine("Web app sign-in flow successful."); @@ -115,7 +115,12 @@ public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds // Sign in again using Todo List button _output.WriteLine("Starting web app sign-in flow using Todo List button after sign out."); await page.GetByRole(AriaRole.Link, new() { Name = "TodoList" }).ClickAsync(); - await UiTestHelpers.SuccessiveLogin_MicrosoftIdFlow_ValidEmailPasswordAsync(page, email, labResponse.User.GetOrFetchPassword(), _output); + await UiTestHelpers.SuccessiveLogin_MicrosoftIdFlow_ValidEmailPasswordAsync(page, email, LabResponseHelper.FetchUserPassword(userConfig.LabName), _output); + await page.WaitForLoadStateAsync(LoadState.DOMContentLoaded); + if (!page.Url.Contains("/TodoList", StringComparison.OrdinalIgnoreCase)) + { + await page.GetByRole(AriaRole.Link, new() { Name = "TodoList" }).ClickAsync(); + } var todoLink = page.GetByRole(AriaRole.Link, new() { Name = "Create New" }); await Assertions.Expect(todoLink).ToBeVisibleAsync(_assertVisibleOptions); _output.WriteLine("Web app sign-in flow successful using Todo List button after sign out."); @@ -237,7 +242,7 @@ public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds // Initial sign in _output.WriteLine("Starting web app sign-in flow."); string email = "idlab@msidlabciam6.onmicrosoft.com"; - await UiTestHelpers.FirstLogin_MicrosoftIdFlow_ValidEmailPasswordAsync(page, email, LabUserHelper.FetchUserPassword("msidlabciam6"), _output); + await UiTestHelpers.FirstLogin_MicrosoftIdFlow_ValidEmailPasswordAsync(page, email, LabResponseHelper.FetchUserPassword("msidlabciam6"), _output); await Assertions.Expect(page.GetByText("Welcome")).ToBeVisibleAsync(_assertVisibleOptions); await Assertions.Expect(page.GetByText(email)).ToBeVisibleAsync(_assertVisibleOptions); _output.WriteLine("Web app sign-in flow successful."); @@ -251,7 +256,7 @@ public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds // Sign in again using Todo List button _output.WriteLine("Starting web app sign-in flow using sign in button after sign out."); await page.GetByRole(AriaRole.Link, new() { Name = "Sign in" }).ClickAsync(); - await UiTestHelpers.FirstLogin_MicrosoftIdFlow_ValidEmailPasswordAsync(page, email, LabUserHelper.FetchUserPassword("msidlabciam6"), _output); + await UiTestHelpers.FirstLogin_MicrosoftIdFlow_ValidEmailPasswordAsync(page, email, LabResponseHelper.FetchUserPassword("msidlabciam6"), _output); await Assertions.Expect(page.GetByText("Welcome")).ToBeVisibleAsync(_assertVisibleOptions); await Assertions.Expect(page.GetByText(email)).ToBeVisibleAsync(_assertVisibleOptions); _output.WriteLine("Web app sign-in flow successful using Sign in button after sign out."); diff --git a/tests/E2E Tests/WebAppUiTests/WebAppIntegrationTests.cs b/tests/E2E Tests/WebAppUiTests/WebAppIntegrationTests.cs index 93e0b6521..9a66752e1 100644 --- a/tests/E2E Tests/WebAppUiTests/WebAppIntegrationTests.cs +++ b/tests/E2E Tests/WebAppUiTests/WebAppIntegrationTests.cs @@ -5,7 +5,7 @@ using System.Diagnostics; using System.Runtime.InteropServices; using System.Threading.Tasks; -using Microsoft.Identity.Lab.Api; +using Microsoft.Identity.Test.LabInfrastructure; using Microsoft.Playwright; using Xunit; @@ -29,14 +29,14 @@ public async Task ChallengeUser_MicrosoftIdentityFlow_RemoteApp_ValidEmailPasswo var context = await browser.NewContextAsync(new BrowserNewContextOptions { IgnoreHTTPSErrors = true }); var page = await context.NewPageAsync(); await page.GotoAsync(UrlString); - LabResponse labResponse = await LabUserHelper.GetDefaultUserAsync(); + var userConfig = await LabResponseHelper.GetUserConfigAsync("MSAL-User-Default-JSON"); try { // Act Trace.WriteLine("Starting Playwright automation: web app sign-in & call Graph"); - string email = labResponse.User.Upn; - await UiTestHelpers.FirstLogin_MicrosoftIdFlow_ValidEmailPasswordAsync(page, email, labResponse.User.GetOrFetchPassword()); + string email = userConfig.Upn; + await UiTestHelpers.FirstLogin_MicrosoftIdFlow_ValidEmailPasswordAsync(page, email, LabResponseHelper.FetchUserPassword(userConfig.LabName)); // Assert await Assertions.Expect(page.GetByText("Welcome")).ToBeVisibleAsync(); diff --git a/tests/Microsoft.Identity.Web.AotCompatibility.TestApp/Microsoft.Identity.Web.AotCompatibility.TestApp.csproj b/tests/Microsoft.Identity.Web.AotCompatibility.TestApp/Microsoft.Identity.Web.AotCompatibility.TestApp.csproj index 8c5d74a53..202d22346 100644 --- a/tests/Microsoft.Identity.Web.AotCompatibility.TestApp/Microsoft.Identity.Web.AotCompatibility.TestApp.csproj +++ b/tests/Microsoft.Identity.Web.AotCompatibility.TestApp/Microsoft.Identity.Web.AotCompatibility.TestApp.csproj @@ -1,7 +1,7 @@ - - + + - net9.0 + net10.0 $(TargetFrameworks); net10.0 Exe true @@ -9,6 +9,13 @@ false false false + true + + + + + true + true @@ -23,14 +30,16 @@ - - + + + + diff --git a/tests/Microsoft.Identity.Web.AotCompatibility.TestApp/Program.cs b/tests/Microsoft.Identity.Web.AotCompatibility.TestApp/Program.cs index eadd92b4e..1fe2d2b60 100644 --- a/tests/Microsoft.Identity.Web.AotCompatibility.TestApp/Program.cs +++ b/tests/Microsoft.Identity.Web.AotCompatibility.TestApp/Program.cs @@ -1,11 +1,31 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Identity.Web; +using Microsoft.Identity.Web.TokenCacheProviders.InMemory; + internal sealed class Program { // The code in this program is expected to be trim and AOT compatible private static int Main() { - return 100; + var builder = WebApplication.CreateSlimBuilder(); + + var azureAdSection = builder.Configuration.GetSection("AzureAd"); + builder.Services + .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApiAot( + options => azureAdSection.Bind(options), + JwtBearerDefaults.AuthenticationScheme, + null); + + builder.Services.AddTokenAcquisition() + .AddInMemoryTokenCaches(); + + return 0; } } diff --git a/tests/Microsoft.Identity.Web.Test.Common/Mocks/MockMtlsHttpClientFactory.cs b/tests/Microsoft.Identity.Web.Test.Common/Mocks/MockMtlsHttpClientFactory.cs new file mode 100644 index 000000000..a3e65152d --- /dev/null +++ b/tests/Microsoft.Identity.Web.Test.Common/Mocks/MockMtlsHttpClientFactory.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Runtime.InteropServices; +using System.Security.Cryptography.X509Certificates; +using Microsoft.Identity.Client; +using Xunit; + +namespace Microsoft.Identity.Web.Test.Common.Mocks +{ + /// + /// HttpClient factory that serves Http responses for testing purposes and supports mTLS certificate binding. + /// + /// + /// This implements both IHttpClientFactory and IMsalMtlsHttpClientFactory for testing mTLS scenarios. + /// + public class MockMtlsHttpClientFactory : IMsalMtlsHttpClientFactory, IHttpClientFactory, IDisposable + { + private LinkedList _httpMessageHandlerQueue = new(); + + private volatile bool _addInstanceDiscovery = true; + + public MockHttpMessageHandler AddMockHandler(MockHttpMessageHandler handler) + { + if (_httpMessageHandlerQueue.Count == 0 && _addInstanceDiscovery) + { + _addInstanceDiscovery = false; + handler.ReplaceMockHttpMessageHandler = (h) => + { + return _httpMessageHandlerQueue.AddFirst(h).Value; + }; + } + + // add a message to the front of the queue + _httpMessageHandlerQueue.AddLast(handler); + return handler; + } + + public HttpClient GetHttpClient() + { + HttpMessageHandler? messageHandler = _httpMessageHandlerQueue.First?.Value; + if (messageHandler == null) + { + throw new InvalidOperationException("The mock HTTP message handler queue is empty."); + } + _httpMessageHandlerQueue.RemoveFirst(); + + var httpClient = new HttpClient(messageHandler); + + httpClient.DefaultRequestHeaders.Accept.Clear(); + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + return httpClient; + } + + public HttpClient CreateClient(string name) + { + return GetHttpClient(); + } + + /// + /// Gets an HttpClient configured with the specified certificate for mTLS. + /// + /// The certificate to use for mTLS. + /// An HttpClient configured for mTLS. + public HttpClient GetHttpClient(X509Certificate2 certificate) + { + // For testing purposes, return the same mocked HttpClient regardless of certificate + // In a real implementation, this would configure the HttpClient with the certificate + return GetHttpClient(); + } + + /// + public void Dispose() + { + // This ensures we only check the mock queue on dispose when we're not in the middle of an + // exception flow. Otherwise, any early assertion will cause this to likely fail + // even though it's not the root cause. +#pragma warning disable CS0618 // Type or member is obsolete - this is non-production code so it's fine + if (Marshal.GetExceptionCode() == 0) +#pragma warning restore CS0618 // Type or member is obsolete + { + string remainingMocks = string.Join( + " ", + _httpMessageHandlerQueue.Select( + h => (h as MockHttpMessageHandler)?.ExpectedUrl ?? string.Empty)); + + Assert.Empty(_httpMessageHandlerQueue); + } + } + } +} diff --git a/tests/Microsoft.Identity.Web.Test.Common/TestConstants.cs b/tests/Microsoft.Identity.Web.Test.Common/TestConstants.cs index eb6342a38..95266f4c7 100644 --- a/tests/Microsoft.Identity.Web.Test.Common/TestConstants.cs +++ b/tests/Microsoft.Identity.Web.Test.Common/TestConstants.cs @@ -9,7 +9,7 @@ namespace Microsoft.Identity.Web.Test.Common { - + public static class TestConstants { public const string ProductionPrefNetworkEnvironment = "login.microsoftonline.com"; @@ -27,7 +27,7 @@ public static class TestConstants public const string ClientId = "87f0ee88-8251-48b3-8825-e0c9563f5234"; public const string GuestTenantId = "guest-tenant-id"; public const string HomeTenantId = "home-tenant-id"; - public const string TenantIdAsGuid = "f645ad92-e38d-4d1a-b510-d1b09a74a8ca"; + public const string TenantIdAsGuid = "10c419d4-4a50-45b2-aa4e-919fb84df24f"; public const string ObjectIdAsGuid = "6364bb70-9521-3fa8-989d-c2c19ff90223"; public const string Domain = "contoso.onmicrosoft.com"; public const string Uid = "my-home-object-id"; @@ -53,7 +53,7 @@ public static class TestConstants public const string AadIssuer = AadInstance + "/" + TenantIdAsGuid + "/v2.0"; public const string UsGovIssuer = "https://login.microsoftonline.us/" + UsGovTenantId + "/v2.0"; public const string UsGovTenantId = "72f988bf-86f1-41af-91ab-2d7cd011db47"; - public const string V1Issuer = "https://sts.windows.net/f645ad92-e38d-4d1a-b510-d1b09a74a8ca/"; + public const string V1Issuer = "https://sts.windows.net/10c419d4-4a50-45b2-aa4e-919fb84df24f/"; public const string GraphBaseUrlBeta = "https://graph.microsoft.com/beta"; public const string GraphBaseUrl = "https://graph.microsoft.com/v1.0"; @@ -106,16 +106,17 @@ public static class TestConstants public const string GraphScopes = "user.write user.read.all"; // Constants for the lab - public const string OBOClientKeyVaultUri = "TodoListServiceV2-OBO"; public const string ConfidentialClientKeyVaultUri = "https://msidlabs.vault.azure.net/secrets/LabVaultAccessCert/"; - public const string ConfidentialClientId = "88f91eac-c606-4c67-a0e2-a5e8a186854f"; - public const string ConfidentialClientLabTenant = "f645ad92-e38d-4d1a-b510-d1b09a74a8ca"; - public const string OBOUser = "idlab1@msidlab4.onmicrosoft.com"; - public const string OBOClientSideClientId = "c0485386-1e9a-4663-bc96-7ab30656de7f"; - public static string[] s_oBOApiScope = new string[] { "api://f4aa5217-e87c-42b2-82af-5624dd14ee72/.default" }; + public const string ConfidentialClientId = "3bf56293-fbb5-42bd-a407-248ba7431a8c"; + public const string ConfidentialClientLabTenant = "10c419d4-4a50-45b2-aa4e-919fb84df24f"; + public const string OBOUser = "MSAL-User-Default@id4slab1.onmicrosoft.com"; + public const string OBOClientSideClientId = "9c0e534b-879c-4dce-b0e2-0e1be873ba14"; + public static string[] s_oBOApiScope = new string[] { "api://8837cde9-4029-4bfc-9259-e9e70ce670f7/.default" }; public const string LabClientId = "f62c5ae3-bf3a-4af5-afa8-a68b800396e9"; public const string MSIDLabLabKeyVaultName = "https://msidlabs.vault.azure.net"; - public const string AzureADIdentityDivisionTestAgentSecret = "MSIDLAB4-IDLABS-APP-AzureADMyOrg-CC"; + public const string ID4sKeyVaultName = "id4skeyvault"; + public const string ID4sKeyVaultUri = "https://id4skeyvault.vault.azure.net/"; + public const string AzureADIdentityDivisionTestAgentSecret = "MISE-App-FMICLIENT"; public const string BuildAutomationKeyVaultName = "https://buildautomation.vault.azure.net/"; // This value is only for testing purposes. It is for a certificate that is not used for anything other than running tests @@ -217,7 +218,7 @@ public static class TestConstants ""preferred_network"":""login.microsoftonline.com"", ""preferred_cache"":""login.windows.net"", ""aliases"":[ - ""login.microsoftonline.com"", + ""login.microsoftonline.com"", ""login.windows.net"", ""login.microsoft.com"", ""sts.windows.net""]}, diff --git a/tests/Microsoft.Identity.Web.Test.Integration/AcquireTokenForAppIntegrationTests.cs b/tests/Microsoft.Identity.Web.Test.Integration/AcquireTokenForAppIntegrationTests.cs index 0b18f0a0a..d3ec32825 100644 --- a/tests/Microsoft.Identity.Web.Test.Integration/AcquireTokenForAppIntegrationTests.cs +++ b/tests/Microsoft.Identity.Web.Test.Integration/AcquireTokenForAppIntegrationTests.cs @@ -18,7 +18,7 @@ using Microsoft.Identity.Web.Test.Common; using Microsoft.Identity.Web.Test.Common.Mocks; using Microsoft.Identity.Web.Test.Common.TestHelpers; -using Microsoft.Identity.Lab.Api; +using Microsoft.Identity.Test.LabInfrastructure; using Microsoft.Identity.Web.TokenCacheProviders.InMemory; using Xunit; using Xunit.Abstractions; @@ -49,7 +49,7 @@ public AcquireTokenForAppIntegrationTests(ITestOutputHelper output) // test set- { _output = output; - KeyVaultSecretsProvider keyVaultSecretsProvider = new KeyVaultSecretsProvider(TestConstants.MSIDLabLabKeyVaultName); + KeyVaultSecretsProvider keyVaultSecretsProvider = new KeyVaultSecretsProvider(TestConstants.ID4sKeyVaultUri); _ccaSecret = keyVaultSecretsProvider.GetSecretByName(TestConstants.AzureADIdentityDivisionTestAgentSecret).Value; // Need the secret before building the services @@ -63,7 +63,7 @@ public AcquireTokenForAppIntegrationTests(ITestOutputHelper output) // test set- throw new ArgumentNullException(message: "No secret returned from Key Vault. ", null); } } - + [Theory] [InlineData(true, Constants.Bearer)] [InlineData(true, "PoP")] diff --git a/tests/Microsoft.Identity.Web.Test.Integration/AcquireTokenForUserIntegrationTests.cs b/tests/Microsoft.Identity.Web.Test.Integration/AcquireTokenForUserIntegrationTests.cs index 6ed272ff5..3a1ead24a 100644 --- a/tests/Microsoft.Identity.Web.Test.Integration/AcquireTokenForUserIntegrationTests.cs +++ b/tests/Microsoft.Identity.Web.Test.Integration/AcquireTokenForUserIntegrationTests.cs @@ -14,7 +14,7 @@ using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Identity.Client; -using Microsoft.Identity.Lab.Api; +using Microsoft.Identity.Test.LabInfrastructure; using Microsoft.Identity.Web.Test.Common; using Microsoft.Identity.Web.Test.Common.TestHelpers; using Microsoft.Identity.Web.TokenCacheProviders.Distributed; @@ -95,7 +95,7 @@ public async Task TestSigningKeyIssuerAsync() string authority = "http://localhost:1234"; Process? p = ExternalApp.Start( typeof(AcquireTokenForUserIntegrationTests), - @"tests\E2E Tests\SimulateOidc\", + @"tests\E2E Tests\SimulateOidc\", "SimulateOidc.exe", $"--urls={authority}"); if (p != null && !p.HasExited) @@ -193,18 +193,18 @@ private HttpClient CreateHttpClient( private static async Task AcquireTokenForLabUserAsync() { - var labResponse = await LabUserHelper.GetSpecificUserAsync(TestConstants.OBOUser); + var userConfig = await LabResponseHelper.GetUserConfigAsync("MSAL-User-Default-JSON"); var msalPublicClient = PublicClientApplicationBuilder .Create(TestConstants.OBOClientSideClientId) - .WithAuthority(labResponse.Lab.Authority, TestConstants.Organizations) + .WithAuthority($"https://login.microsoftonline.com/{TestConstants.TenantIdAsGuid}", TestConstants.Organizations) .Build(); #pragma warning disable CS0618 // Obsolete AuthenticationResult authResult = await msalPublicClient .AcquireTokenByUsernamePassword( TestConstants.s_oBOApiScope, - TestConstants.OBOUser, - labResponse.User.GetOrFetchPassword()) + userConfig.Upn, + LabResponseHelper.FetchUserPassword(userConfig.LabName)) .ExecuteAsync(CancellationToken.None) ; #pragma warning restore CS0618 // Obsolete diff --git a/tests/Microsoft.Identity.Web.Test/AgentIdentitiesExtensionTests.cs b/tests/Microsoft.Identity.Web.Test/AgentIdentitiesExtensionTests.cs new file mode 100644 index 000000000..8987575e2 --- /dev/null +++ b/tests/Microsoft.Identity.Web.Test/AgentIdentitiesExtensionTests.cs @@ -0,0 +1,145 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using Microsoft.Identity.Abstractions; +using Xunit; + +namespace Microsoft.Identity.Web.Test +{ + /// + /// Tests for the AgentIdentityExtension class (ForAgentIdentity and WithAgentIdentity methods). + /// + public class AgentIdentitiesExtensionTests + { + private const string TestAgentApplicationId = "test-agent-app-id"; + + [Fact] + public void WithAgentIdentity_WithDefaultAuthenticationOptionsName_UsesAzureAdConfigurationSection() + { + // Arrange + var options = new AuthorizationHeaderProviderOptions(); + + // Act + options.WithAgentIdentity(TestAgentApplicationId); + + // Assert + Assert.NotNull(options.AcquireTokenOptions); + Assert.NotNull(options.AcquireTokenOptions.ExtraParameters); + Assert.True(options.AcquireTokenOptions.ExtraParameters.ContainsKey(Constants.MicrosoftIdentityOptionsParameter)); + + var microsoftIdentityOptions = options.AcquireTokenOptions.ExtraParameters[Constants.MicrosoftIdentityOptionsParameter] as MicrosoftEntraApplicationOptions; + Assert.NotNull(microsoftIdentityOptions); + Assert.Equal(TestAgentApplicationId, microsoftIdentityOptions.ClientId); + + // Verify the ConfigurationSection is set to "AzureAd" when AuthenticationOptionsName is not set + var clientCredential = Assert.Single(microsoftIdentityOptions.ClientCredentials!); + Assert.Equal(CredentialSource.CustomSignedAssertion, clientCredential.SourceType); + Assert.Equal("OidcIdpSignedAssertion", clientCredential.CustomSignedAssertionProviderName); + Assert.NotNull(clientCredential.CustomSignedAssertionProviderData); + Assert.True(clientCredential.CustomSignedAssertionProviderData.TryGetValue("ConfigurationSection", out var configSection)); + Assert.Equal("AzureAd", configSection); + } + + [Fact] + public void WithAgentIdentity_WithCustomAuthenticationOptionsName_UsesCustomConfigurationSection() + { + // Arrange + var options = new AuthorizationHeaderProviderOptions + { + AcquireTokenOptions = new AcquireTokenOptions + { + AuthenticationOptionsName = "MyEntraId" + } + }; + + // Act + options.WithAgentIdentity(TestAgentApplicationId); + + // Assert + Assert.NotNull(options.AcquireTokenOptions); + Assert.NotNull(options.AcquireTokenOptions.ExtraParameters); + Assert.True(options.AcquireTokenOptions.ExtraParameters.ContainsKey(Constants.MicrosoftIdentityOptionsParameter)); + + var microsoftIdentityOptions = options.AcquireTokenOptions.ExtraParameters[Constants.MicrosoftIdentityOptionsParameter] as MicrosoftEntraApplicationOptions; + Assert.NotNull(microsoftIdentityOptions); + + // Verify the ConfigurationSection respects the custom AuthenticationOptionsName + var clientCredential = Assert.Single(microsoftIdentityOptions.ClientCredentials!); + Assert.NotNull(clientCredential.CustomSignedAssertionProviderData); + Assert.True(clientCredential.CustomSignedAssertionProviderData.TryGetValue("ConfigurationSection", out var configSection)); + Assert.Equal("MyEntraId", configSection); + } + + [Theory] + [InlineData("EntraId")] + [InlineData("CustomSection")] + [InlineData("AzureAD_Prod")] + public void WithAgentIdentity_WithVariousCustomAuthenticationOptionsNames_UsesCorrectConfigurationSection(string authenticationOptionsName) + { + // Arrange + var options = new AuthorizationHeaderProviderOptions + { + AcquireTokenOptions = new AcquireTokenOptions + { + AuthenticationOptionsName = authenticationOptionsName + } + }; + + // Act + options.WithAgentIdentity(TestAgentApplicationId); + + // Assert + var microsoftIdentityOptions = options.AcquireTokenOptions.ExtraParameters![Constants.MicrosoftIdentityOptionsParameter] as MicrosoftEntraApplicationOptions; + Assert.NotNull(microsoftIdentityOptions); + + var clientCredential = Assert.Single(microsoftIdentityOptions.ClientCredentials!); + Assert.True(clientCredential.CustomSignedAssertionProviderData!.TryGetValue("ConfigurationSection", out var configSection)); + Assert.Equal(authenticationOptionsName, configSection); + } + + [Fact] + public void WithAgentIdentity_WithNullOptions_CreatesNewOptionsAndUsesAzureAdDefault() + { + // Arrange + AuthorizationHeaderProviderOptions? options = null; + + // Act + var result = options!.WithAgentIdentity(TestAgentApplicationId); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.AcquireTokenOptions); + Assert.NotNull(result.AcquireTokenOptions.ExtraParameters); + + var microsoftIdentityOptions = result.AcquireTokenOptions.ExtraParameters[Constants.MicrosoftIdentityOptionsParameter] as MicrosoftEntraApplicationOptions; + Assert.NotNull(microsoftIdentityOptions); + + var clientCredential = Assert.Single(microsoftIdentityOptions.ClientCredentials!); + Assert.True(clientCredential.CustomSignedAssertionProviderData!.TryGetValue("ConfigurationSection", out var configSection)); + Assert.Equal("AzureAd", configSection); + } + + [Fact] + public void WithAgentIdentity_AlwaysSetsRequiresSignedAssertionFmiPath() + { + // Arrange + var options = new AuthorizationHeaderProviderOptions + { + AcquireTokenOptions = new AcquireTokenOptions + { + AuthenticationOptionsName = "CustomSection" + } + }; + + // Act + options.WithAgentIdentity(TestAgentApplicationId); + + // Assert + var microsoftIdentityOptions = options.AcquireTokenOptions.ExtraParameters![Constants.MicrosoftIdentityOptionsParameter] as MicrosoftEntraApplicationOptions; + var clientCredential = Assert.Single(microsoftIdentityOptions!.ClientCredentials!); + Assert.True(clientCredential.CustomSignedAssertionProviderData!.TryGetValue("RequiresSignedAssertionFmiPath", out var fmiPathRequired)); + Assert.Equal(true, fmiPathRequired); + } + } +} diff --git a/tests/Microsoft.Identity.Web.Test/AuthorizationHeaderProviderTests.cs b/tests/Microsoft.Identity.Web.Test/AuthorizationHeaderProviderTests.cs index ca99b273f..142679f8d 100644 --- a/tests/Microsoft.Identity.Web.Test/AuthorizationHeaderProviderTests.cs +++ b/tests/Microsoft.Identity.Web.Test/AuthorizationHeaderProviderTests.cs @@ -14,6 +14,7 @@ using Microsoft.Identity.Web.Test.Common.Mocks; using Microsoft.Identity.Web.TestOnly; using Microsoft.IdentityModel.Tokens; +using NSubstitute.Extensions; using Xunit; namespace Microsoft.Identity.Web.Test @@ -25,7 +26,33 @@ public class AuthorizationHeaderProviderTests public async Task LongRunningSessionForDefaultAuthProviderForUserDefaultKeyTest() { // Arrange + // Create a test ClaimsPrincipal + var claims = new List + { + new Claim(ClaimTypes.Name, "testuser@contoso.com") + }; + + var identity = new CaseSensitiveClaimsIdentity(claims, "TestAuth"); + identity.BootstrapContext = CreateTestJwt(); + var claimsPrincipal = new ClaimsPrincipal(identity); + var tokenAcquirerFactory = InitTokenAcquirerFactoryForTest(); + bool argsNotNull = true; + + // Configure the extension option such that the event is subscribed to + // so the test can observe if the service provider is set in the extra parameters + tokenAcquirerFactory.Services.Configure(options => + { + options.OnBeforeTokenAcquisitionForOnBehalfOf += (builder, options, args) => + { + if (argsNotNull) + { + //verify that the ClaimsPrincipal passed in the event is the same as the one passed to CreateAuthorizationHeaderForUserAsync and that the BootstrapContext is preserved + Assert.Equal(((CaseSensitiveClaimsIdentity)claimsPrincipal.Identity!).BootstrapContext, ((CaseSensitiveClaimsIdentity)args?.User?.Identity!).BootstrapContext); + Assert.Equal(((CaseSensitiveClaimsIdentity)claimsPrincipal.Identity!).BootstrapContext, args.UserAssertionToken); + } + }; + }); IServiceProvider serviceProvider = tokenAcquirerFactory.Build(); IAuthorizationHeaderProvider authorizationHeaderProvider = @@ -34,16 +61,6 @@ public async Task LongRunningSessionForDefaultAuthProviderForUserDefaultKeyTest( using (mockHttpClient) { - // Create a test ClaimsPrincipal - var claims = new List - { - new Claim(ClaimTypes.Name, "testuser@contoso.com") - }; - - var identity = new CaseSensitiveClaimsIdentity(claims, "TestAuth"); - identity.BootstrapContext = CreateTestJwt(); - var claimsPrincipal = new ClaimsPrincipal(identity); - // Create options with LongRunningWebApiSessionKey var options = new AuthorizationHeaderProviderOptions { @@ -70,6 +87,7 @@ public async Task LongRunningSessionForDefaultAuthProviderForUserDefaultKeyTest( string key1 = options.AcquireTokenOptions.LongRunningWebApiSessionKey; // Step 4: Second call without ClaimsPrincipal should return the token from cache + argsNotNull = false; result = await authorizationHeaderProvider.CreateAuthorizationHeaderForUserAsync( scopes, options); @@ -79,6 +97,7 @@ public async Task LongRunningSessionForDefaultAuthProviderForUserDefaultKeyTest( Assert.Equal(key1, options.AcquireTokenOptions.LongRunningWebApiSessionKey); // Step 5: First call with ClaimsPrincipal to initiate LR session for CreateAuthorizationHeaderAsync + argsNotNull = true; scopes = new[] { "User.Write" }; mockHttpClient!.AddMockHandler(MockHttpCreator.CreateLrOboTokenHandler("User.Write")); result = await authorizationHeaderProvider.CreateAuthorizationHeaderAsync( @@ -90,6 +109,7 @@ public async Task LongRunningSessionForDefaultAuthProviderForUserDefaultKeyTest( Assert.NotEqual(options.AcquireTokenOptions.LongRunningWebApiSessionKey, TokenAcquisitionOptions.LongRunningWebApiSessionKeyAuto); key1 = options.AcquireTokenOptions.LongRunningWebApiSessionKey; + argsNotNull = false; // Step 6: Second call without ClaimsPrincipal should return the token from cache for CreateAuthorizationHeaderAsync result = await authorizationHeaderProvider.CreateAuthorizationHeaderAsync( scopes, diff --git a/tests/Microsoft.Identity.Web.Test/CertificateReloadLogicTests.cs b/tests/Microsoft.Identity.Web.Test/CertificateReloadLogicTests.cs new file mode 100644 index 000000000..8635fa156 --- /dev/null +++ b/tests/Microsoft.Identity.Web.Test/CertificateReloadLogicTests.cs @@ -0,0 +1,197 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Client; +using Microsoft.Identity.Web.Test.Common; +using Microsoft.Identity.Web.TestOnly; +using Xunit; + +namespace Microsoft.Identity.Web.Test +{ + /// + /// Tests for the certificate reload logic to ensure it only triggers on certificate-related errors. + /// This addresses the regression from PR #3430 where reload was triggered on all invalid_client errors. + /// + [Collection(nameof(TokenAcquirerFactorySingletonProtection))] + public class CertificateReloadLogicTests + { + private const string InvalidClientErrorCode = "invalid_client"; + + [Theory] + [InlineData("AADSTS700027", true)] // InvalidKeyError + [InlineData("AADSTS700024", true)] // SignedAssertionInvalidTimeRange + [InlineData("AADSTS7000214", true)] // CertificateHasBeenRevoked + [InlineData("AADSTS1000502", true)] // CertificateIsOutsideValidityWindow + [InlineData("AADSTS7000274", true)] // ClientAssertionContainsInvalidSignature + [InlineData("AADSTS7000277", true)] // CertificateWasRevoked + [InlineData("AADSTS7000215", false)] // Invalid client secret - should NOT trigger reload + [InlineData("AADSTS700016", false)] // Application not found - should NOT trigger reload + [InlineData("AADSTS7000222", false)] // Invalid client secret (expired) - should NOT trigger reload + [InlineData("AADSTS50011", false)] // Invalid reply address - should NOT trigger reload + [InlineData("AADSTS50012", false)] // Invalid client credentials - should NOT trigger reload + public void IsInvalidClientCertificateOrSignedAssertionError_ReturnsTrueOnlyForCertificateErrors( + string errorCode, + bool shouldTriggerReload) + { + // Arrange + var tokenAcquisition = CreateTokenAcquisition(); + var responseBody = $"{{\"error\":\"invalid_client\",\"error_description\":\"Error {errorCode}: Test error\"}}"; + var exception = CreateMsalServiceException(InvalidClientErrorCode, responseBody); + + // Act + bool result = InvokeIsInvalidClientCertificateOrSignedAssertionError(tokenAcquisition, exception); + + // Assert + Assert.Equal(shouldTriggerReload, result); + } + + [Fact] + public void IsInvalidClientCertificateOrSignedAssertionError_ReturnsFalseWhenErrorCodeIsNotInvalidClient() + { + // Arrange + var tokenAcquisition = CreateTokenAcquisition(); + var responseBody = "{\"error\":\"unauthorized_client\",\"error_description\":\"Test error\"}"; + var exception = CreateMsalServiceException("unauthorized_client", responseBody); + + // Act + bool result = InvokeIsInvalidClientCertificateOrSignedAssertionError(tokenAcquisition, exception); + + // Assert + Assert.False(result, "Should not trigger reload for non-invalid_client errors"); + } + + [Fact] + public void IsInvalidClientCertificateOrSignedAssertionError_ReturnsFalseForEmptyResponseBody() + { + // Arrange + var tokenAcquisition = CreateTokenAcquisition(); + var exception = CreateMsalServiceException(InvalidClientErrorCode, string.Empty); + + // Act + bool result = InvokeIsInvalidClientCertificateOrSignedAssertionError(tokenAcquisition, exception); + + // Assert + Assert.False(result, "Should not trigger reload when response body is empty"); + } + + [Theory] + [InlineData("AADSTS700027")] // Case sensitive check - should still work + [InlineData("aadsts700027")] // Lowercase + [InlineData("AaDsTs700027")] // Mixed case + public void IsInvalidClientCertificateOrSignedAssertionError_IsCaseInsensitive(string errorCodeCase) + { + // Arrange + var tokenAcquisition = CreateTokenAcquisition(); + var responseBody = $"{{\"error\":\"invalid_client\",\"error_description\":\"Error {errorCodeCase}: Test error\"}}"; + var exception = CreateMsalServiceException(InvalidClientErrorCode, responseBody); + + // Act + bool result = InvokeIsInvalidClientCertificateOrSignedAssertionError(tokenAcquisition, exception); + + // Assert + Assert.True(result, $"Should trigger reload regardless of case: {errorCodeCase}"); + } + + [Fact] + public void IsInvalidClientCertificateOrSignedAssertionError_WorksWithMultipleErrorCodesInResponse() + { + // Arrange + var tokenAcquisition = CreateTokenAcquisition(); + // Response might contain multiple error codes or descriptions + var responseBody = $"{{\"error\":\"invalid_client\",\"error_description\":\"Error {Constants.CertificateHasBeenRevoked}: Certificate has been revoked. Also note AADSTS7000215 in logs.\"}}"; + var exception = CreateMsalServiceException(InvalidClientErrorCode, responseBody); + + // Act + bool result = InvokeIsInvalidClientCertificateOrSignedAssertionError(tokenAcquisition, exception); + + // Assert + Assert.True(result, "Should trigger reload when certificate error code is present, even if other codes are also mentioned"); + } + + /// + /// Creates a TokenAcquisition instance for testing. + /// + private TokenAcquisition CreateTokenAcquisition() + { + TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest(); + var tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance(); + tokenAcquirerFactory.Services.Configure(options => + { + options.Instance = "https://login.microsoftonline.com/"; + options.TenantId = "10c419d4-4a50-45b2-aa4e-919fb84df24f"; + options.ClientId = "idu773ld-e38d-jud3-45lk-d1b09a74a8ca"; + options.ClientCredentials = [new CredentialDescription() + { + SourceType = CredentialSource.ClientSecret, + ClientSecret = "someSecret" + }]; + }); + + var serviceProvider = tokenAcquirerFactory.Build(); + + // Get the TokenAcquisition instance from the service provider + var tokenAcquisition = serviceProvider.GetService(typeof(ITokenAcquisitionInternal)) as TokenAcquisition; + + if (tokenAcquisition == null) + { + throw new InvalidOperationException("Failed to create TokenAcquisition instance for testing"); + } + + return tokenAcquisition; + } + + /// + /// Creates a MsalServiceException for testing. + /// + private MsalServiceException CreateMsalServiceException(string errorCode, string responseBody) + { + // Use the MsalServiceException constructor with errorCode and errorMessage + // The ResponseBody property is internal but can be accessed via reflection + var exception = new MsalServiceException(errorCode, $"Test exception: {errorCode}"); + + // Set the ResponseBody property using reflection + var responseBodyField = typeof(MsalServiceException).GetProperty("ResponseBody"); + if (responseBodyField != null && responseBodyField.CanWrite) + { + responseBodyField.SetValue(exception, responseBody); + } + else + { + // Try the backing field instead + var backingField = typeof(MsalServiceException).GetField("k__BackingField", + BindingFlags.NonPublic | BindingFlags.Instance); + if (backingField != null) + { + backingField.SetValue(exception, responseBody); + } + } + + return exception; + } + + /// + /// Invokes the private IsInvalidClientCertificateOrSignedAssertionError method using reflection. + /// + private bool InvokeIsInvalidClientCertificateOrSignedAssertionError( + TokenAcquisition tokenAcquisition, + MsalServiceException exception) + { + var method = typeof(TokenAcquisition).GetMethod( + "IsInvalidClientCertificateOrSignedAssertionError", + BindingFlags.NonPublic | BindingFlags.Instance); + + if (method == null) + { + throw new InvalidOperationException("Could not find IsInvalidClientCertificateOrSignedAssertionError method"); + } + + var result = method.Invoke(tokenAcquisition, new object[] { exception }); + return (bool)result!; + } + } +} diff --git a/tests/Microsoft.Identity.Web.Test/CertificateRetryCounterTests.cs b/tests/Microsoft.Identity.Web.Test/CertificateRetryCounterTests.cs new file mode 100644 index 000000000..f2bb31139 --- /dev/null +++ b/tests/Microsoft.Identity.Web.Test/CertificateRetryCounterTests.cs @@ -0,0 +1,306 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Client; +using Microsoft.Identity.Web.Test.Common; +using Microsoft.Identity.Web.TestOnly; +using Xunit; + +namespace Microsoft.Identity.Web.Test +{ + /// + /// Tests for the certificate retry counter logic to prevent infinite retry loops. + /// This addresses the regression where misconfigured credentials (wrong ClientID/Secret) + /// caused infinite retries when using WithAgentIdentities(). + /// + [Collection(nameof(TokenAcquirerFactorySingletonProtection))] + public class CertificateRetryCounterTests + { + private const string InvalidClientErrorCode = "invalid_client"; + private const int MaxCertificateRetries = 1; + + #region Certificate Error Detection Tests + + [Theory] + [InlineData("AADSTS700016")] // Application not found (wrong ClientID) + [InlineData("AADSTS7000215")] // Invalid client secret + public void IsInvalidClientCertificateOrSignedAssertionError_ReturnsFalseForNonRetryableConfigErrors(string errorCode) + { + // Arrange + var tokenAcquisition = CreateTokenAcquisition(); + var responseBody = $"{{\"error\":\"invalid_client\",\"error_description\":\"Error {errorCode}: Config error\"}}"; + var exception = CreateMsalServiceException(InvalidClientErrorCode, responseBody); + + // Act + bool result = InvokeIsInvalidClientCertificateOrSignedAssertionError(tokenAcquisition, exception); + + // Assert + Assert.False(result, $"Should NOT retry for non-retryable config error: {errorCode}"); + } + + [Theory] + [InlineData("AADSTS700027")] // InvalidKeyError + [InlineData("AADSTS700024")] // SignedAssertionInvalidTimeRange + [InlineData("AADSTS7000214")] // CertificateHasBeenRevoked + [InlineData("AADSTS1000502")] // CertificateIsOutsideValidityWindow + [InlineData("AADSTS7000274")] // ClientAssertionContainsInvalidSignature + [InlineData("AADSTS7000277")] // CertificateWasRevoked + public void IsInvalidClientCertificateOrSignedAssertionError_ReturnsTrueForCertificateErrors(string errorCode) + { + // Arrange + var tokenAcquisition = CreateTokenAcquisition(); + var responseBody = $"{{\"error\":\"invalid_client\",\"error_description\":\"Error {errorCode}: Cert error\"}}"; + var exception = CreateMsalServiceException(InvalidClientErrorCode, responseBody); + + // Act + bool result = InvokeIsInvalidClientCertificateOrSignedAssertionError(tokenAcquisition, exception); + + // Assert + Assert.True(result, $"Should retry for certificate error: {errorCode}"); + } + + #endregion + + #region Regression Test: Infinite Loop Prevention + + /// + /// Regression test: Verifies that bad certificate/config does NOT cause infinite retry loop. + /// This test simulates the exact scenario reported in the bug where .WithAgentIdentities() + /// with wrong ClientID caused the application to hang indefinitely. + /// + [Fact] + public async Task GetAuthenticationResultForAppInternalAsync_DoesNotRetryInfinitelyOnBadConfig() + { + // Arrange + var tokenAcquisition = CreateTokenAcquisition(); + + // Mock MSAL to always throw "Application Not Found" error + var mockMsalException = CreateMsalServiceException( + InvalidClientErrorCode, + "{\"error\":\"invalid_client\",\"error_description\":\"AADSTS700016: Application with identifier 'bad-client-id' was not found.\"}"); + + // Act & Assert + try + { + // This should NOT hang and should throw after MaxCertificateRetries + await Task.Run(async () => + { + // Use reflection to call GetAuthenticationResultForAppInternalAsync with retryCount = 0 + var method = typeof(TokenAcquisition).GetMethod( + "GetAuthenticationResultForAppInternalAsync", + BindingFlags.NonPublic | BindingFlags.Instance); + + if (method == null) + { + throw new InvalidOperationException("Could not find GetAuthenticationResultForAppInternalAsync method"); + } + + // This will throw because we don't have a real MSAL setup, but we're testing the retry logic + // The test is mainly to verify it doesn't hang + var task = method.Invoke(tokenAcquisition, new object?[] + { + "https://graph.microsoft.com/.default", + null, + null, + null, + 0 // Initial retryCount + }) as Task; + + if (task != null) + { + await task; + } + }); + + // If we get here without exception, something is wrong with the test setup + Assert.Fail("Expected an exception to be thrown"); + } + catch (TargetInvocationException ex) when (ex.InnerException != null) + { + // Expected: should throw after max retries + // Verify it's not hanging and completes quickly + Assert.NotNull(ex.InnerException); + // The actual exception type depends on the implementation + } + catch (Exception ex) + { + // Also acceptable - any exception is fine as long as it doesn't hang + Assert.NotNull(ex); + } + + // If we reach here, the method completed (didn't hang) + } + + /// + /// Regression test: Simulates the Agent Identities scenario where nested token acquisitions + /// with bad config should fail quickly, not hang. + /// + [Fact(Timeout = 5000)] // 5 second timeout - if it takes longer, it's likely hanging + public async Task GetAuthenticationResultForAppAsync_WithBadClientId_CompletesQuickly() + { + // Arrange + var tokenAcquisition = CreateTokenAcquisitionWithBadClientId(); + + // Act & Assert + await Assert.ThrowsAnyAsync(async () => + { + // This simulates the Agent Identities scenario + // Should fail quickly, not hang indefinitely + var result = await InvokeGetAuthenticationResultForAppAsync( + tokenAcquisition, + "https://graph.microsoft.com/.default"); + }); + + // If we reach here within the timeout, test passes + Assert.True(true, "Completed without hanging"); + } + + #endregion + + #region Helper Methods + + /// + /// Creates a TokenAcquisition instance for testing. + /// + private TokenAcquisition CreateTokenAcquisition() + { + TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest(); + var tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance(); + tokenAcquirerFactory.Services.Configure(options => + { + options.Instance = "https://login.microsoftonline.com/"; + options.TenantId = "f645ad92-e38d-4d1a-b510-d1b09a74a8ca"; + options.ClientId = "idu773ld-e38d-jud3-45lk-d1b09a74a8ca"; + options.ClientCredentials = [new CredentialDescription() + { + SourceType = CredentialSource.ClientSecret, + ClientSecret = "someSecret" + }]; + }); + + var serviceProvider = tokenAcquirerFactory.Build(); + + var tokenAcquisition = serviceProvider.GetService(typeof(ITokenAcquisitionInternal)) as TokenAcquisition; + + if (tokenAcquisition == null) + { + throw new InvalidOperationException("Failed to create TokenAcquisition instance for testing"); + } + + return tokenAcquisition; + } + + /// + /// Creates a TokenAcquisition instance with a bad ClientID to simulate the regression scenario. + /// + private TokenAcquisition CreateTokenAcquisitionWithBadClientId() + { + TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest(); + var tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance(); + tokenAcquirerFactory.Services.Configure(options => + { + options.Instance = "https://login.microsoftonline.com/"; + options.TenantId = "f645ad92-e38d-4d1a-b510-d1b09a74a8ca"; + options.ClientId = "bad-client-id-does-not-exist"; // Invalid ClientID + options.ClientCredentials = [new CredentialDescription() + { + SourceType = CredentialSource.ClientSecret, + ClientSecret = "someSecret" + }]; + }); + + var serviceProvider = tokenAcquirerFactory.Build(); + + var tokenAcquisition = serviceProvider.GetService(typeof(ITokenAcquisitionInternal)) as TokenAcquisition; + + if (tokenAcquisition == null) + { + throw new InvalidOperationException("Failed to create TokenAcquisition instance for testing"); + } + + return tokenAcquisition; + } + + /// + /// Creates a MsalServiceException for testing. + /// + private MsalServiceException CreateMsalServiceException(string errorCode, string responseBody) + { + var exception = new MsalServiceException(errorCode, $"Test exception: {errorCode}"); + + // Set the ResponseBody property using reflection + var responseBodyField = typeof(MsalServiceException).GetProperty("ResponseBody"); + if (responseBodyField != null && responseBodyField.CanWrite) + { + responseBodyField.SetValue(exception, responseBody); + } + else + { + var backingField = typeof(MsalServiceException).GetField("k__BackingField", + BindingFlags.NonPublic | BindingFlags.Instance); + if (backingField != null) + { + backingField.SetValue(exception, responseBody); + } + } + + return exception; + } + + /// + /// Invokes the private IsInvalidClientCertificateOrSignedAssertionError method using reflection. + /// + private bool InvokeIsInvalidClientCertificateOrSignedAssertionError( + TokenAcquisition tokenAcquisition, + MsalServiceException exception) + { + var method = typeof(TokenAcquisition).GetMethod( + "IsInvalidClientCertificateOrSignedAssertionError", + BindingFlags.NonPublic | BindingFlags.Instance); + + if (method == null) + { + throw new InvalidOperationException("Could not find IsInvalidClientCertificateOrSignedAssertionError method"); + } + + var result = method.Invoke(tokenAcquisition, new object[] { exception }); + return (bool)result!; + } + + /// + /// Invokes GetAuthenticationResultForAppAsync using reflection for testing. + /// + private async Task InvokeGetAuthenticationResultForAppAsync( + TokenAcquisition tokenAcquisition, + string scope) + { + var method = typeof(TokenAcquisition).GetMethod( + "GetAuthenticationResultForAppAsync", + BindingFlags.Public | BindingFlags.Instance); + + if (method == null) + { + throw new InvalidOperationException("Could not find GetAuthenticationResultForAppAsync method"); + } + + var task = method.Invoke(tokenAcquisition, new object?[] { scope, null, null, null }) as Task; + + if (task == null) + { + throw new InvalidOperationException("Method did not return a Task"); + } + + return await task; + } + + #endregion + } +} diff --git a/tests/Microsoft.Identity.Web.Test/Certificates/DefaultCertificateLoaderTests.cs b/tests/Microsoft.Identity.Web.Test/Certificates/DefaultCertificateLoaderTests.cs index 0913ffc81..d45d75b2e 100644 --- a/tests/Microsoft.Identity.Web.Test/Certificates/DefaultCertificateLoaderTests.cs +++ b/tests/Microsoft.Identity.Web.Test/Certificates/DefaultCertificateLoaderTests.cs @@ -18,7 +18,7 @@ namespace Microsoft.Identity.Web.Test.Certificates { public class DefaultCertificateLoaderTests { - // https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/Credentials/appId/9a192b78-6580-4f8a-aace-f36ffea4f7be/isMSAApp/ + // https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/Credentials/appId/a599ce88-0a5f-4a6e-beca-e67d3fc427f4/isMSAApp/ // [InlineData(CertificateSource.KeyVault, TestConstants.KeyVaultContainer, TestConstants.KeyVaultReference)] // [InlineData(CertificateSource.Path, @"c:\temp\WebAppCallingWebApiCert.pfx", "")] // [InlineData(CertificateSource.StoreWithDistinguishedName, "CurrentUser/My", "CN=WebAppCallingWebApiCert")] @@ -83,7 +83,7 @@ public async Task TextExtensibilityE2E() }); Assert.Equal("e2e-mock", cd?.CachedValue?.ToString()); - + } [Fact] @@ -157,7 +157,7 @@ public void TestDefaultCredentialsLoaderWithCustomLoaders() // Assert Assert.NotNull(loader.CredentialSourceLoaders); Assert.True(loader.CredentialSourceLoaders.ContainsKey(CredentialSource.Base64Encoded)); - + // Verify the custom loader overrode the built-in one var customLoader = loader.CredentialSourceLoaders[CredentialSource.Base64Encoded] as MockCredentialSourceLoader; Assert.NotNull(customLoader); @@ -179,7 +179,7 @@ public void TestDefaultCertificateLoaderWithCustomLoaders() // Assert Assert.NotNull(loader.CredentialSourceLoaders); Assert.True(loader.CredentialSourceLoaders.ContainsKey(CredentialSource.Path)); - + // Verify the custom loader overrode the built-in one var customLoader = loader.CredentialSourceLoaders[CredentialSource.Path] as MockCredentialSourceLoader; Assert.NotNull(customLoader); @@ -287,7 +287,7 @@ public void TestConstructorWithBothCustomSignedAssertionProvidersAndCredentialSo // Assert Assert.NotNull(credentialsLoader.CredentialSourceLoaders); Assert.NotNull(credentialsLoader.CustomSignedAssertionCredentialSourceLoaders); - + // Verify custom credential source loader is present Assert.True(credentialsLoader.CredentialSourceLoaders.ContainsKey(CredentialSource.Path)); var customLoader = credentialsLoader.CredentialSourceLoaders[CredentialSource.Path] as MockCredentialSourceLoader; diff --git a/tests/Microsoft.Identity.Web.Test/Certificates/WithClientCredentialsTests.cs b/tests/Microsoft.Identity.Web.Test/Certificates/WithClientCredentialsTests.cs index 176d802bf..43a4691f5 100644 --- a/tests/Microsoft.Identity.Web.Test/Certificates/WithClientCredentialsTests.cs +++ b/tests/Microsoft.Identity.Web.Test/Certificates/WithClientCredentialsTests.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Client; using Microsoft.Identity.Web.Test.Common; using NSubstitute; using NSubstitute.ExceptionExtensions; @@ -79,7 +80,7 @@ public async Task FailsForFic_ReturnsMeaningfulMessageAsync() var ficCredential = new CredentialDescription { SourceType = CredentialSource.SignedAssertionFromManagedIdentity, - ManagedIdentityClientId = "9a192b78-6580-4f8a-aace-f36ffea4f7be" + ManagedIdentityClientId = "a599ce88-0a5f-4a6e-beca-e67d3fc427f4" }; await RunFailToLoadLogicAsync(new[] { ficCredential }); @@ -112,7 +113,7 @@ public async Task FailsForFicAndCert_ReturnsMeaningfulMessageAsync() var ficCredential = new CredentialDescription { SourceType = CredentialSource.SignedAssertionFromManagedIdentity, - ManagedIdentityClientId = "9a192b78-6580-4f8a-aace-f36ffea4f7be" + ManagedIdentityClientId = "a599ce88-0a5f-4a6e-beca-e67d3fc427f4" }; var certCredential = new CredentialDescription @@ -138,7 +139,7 @@ public async Task FailsForCertAndFic_ReturnsMeaningfulMessageAsync() var ficCredential = new CredentialDescription { SourceType = CredentialSource.SignedAssertionFromManagedIdentity, - ManagedIdentityClientId = "9a192b78-6580-4f8a-aace-f36ffea4f7be" + ManagedIdentityClientId = "a599ce88-0a5f-4a6e-beca-e67d3fc427f4" }; await RunFailToLoadLogicAsync(new[] { ficCredential, certCredential }); @@ -195,5 +196,222 @@ private static async Task RunFailToLoadLogicAsync(IEnumerable(); + var credLoader = Substitute.For(); + var builder = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) + .WithAuthority(TestConstants.AuthorityCommonTenant); + + var credentialDescription = new CredentialDescription + { + SourceType = CredentialSource.StoreWithThumbprint, + CertificateThumbprint = "test-thumbprint", + CertificateStorePath = "CurrentUser/My" + }; + + var testCertificate = Base64EncodedCertificateLoader.LoadFromBase64Encoded( + TestConstants.CertificateX5cWithPrivateKey, + TestConstants.CertificateX5cWithPrivateKeyPassword, + X509KeyStorageFlags.DefaultKeySet); + + // Mock the credential loader to successfully load the certificate + credLoader.LoadCredentialsIfNeededAsync(Arg.Any(), Arg.Any()) + .Returns(args => + { + var cd = (args[0] as CredentialDescription)!; + cd.Certificate = testCertificate; + return Task.CompletedTask; + }); + + // Act + var result = await builder.WithClientCredentialsAsync( + new[] { credentialDescription }, + logger, + credLoader, + credentialSourceLoaderParameters: null, + isTokenBinding: true); + + // Assert + Assert.NotNull(result); + Assert.Same(builder, result); // Should return the same builder instance + await credLoader.Received(1).LoadCredentialsIfNeededAsync(credentialDescription, null); + } + + [Fact] + public async Task WithBindingCertificateAsync_NoValidCredentials_ThrowsException() + { + // Arrange + var logger = Substitute.For(); + var credLoader = Substitute.For(); + var builder = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) + .WithAuthority(TestConstants.AuthorityCommonTenant); + + var credentialDescription = new CredentialDescription + { + SourceType = CredentialSource.StoreWithThumbprint, + CertificateThumbprint = "test-thumbprint", + CertificateStorePath = "CurrentUser/My" + }; + + // Mock the credential loader to fail loading (Skip = true causes LoadCredentialForMsalOrFailAsync to throw) + credLoader.LoadCredentialsIfNeededAsync(Arg.Any(), Arg.Any()) + .Returns(args => + { + var cd = (args[0] as CredentialDescription)!; + cd.Skip = true; + return Task.CompletedTask; + }); + + // Act & Assert + // This should throw because LoadCredentialForMsalOrFailAsync throws when no credentials can be loaded + await Assert.ThrowsAsync( + () => builder.WithClientCredentialsAsync( + new[] { credentialDescription }, + logger, + credLoader, + credentialSourceLoaderParameters: null, + isTokenBinding: true)); + } + + [Fact] + public async Task WithBindingCertificateAsync_CredentialWithoutCertificate_ThrowsException() + { + // Arrange + var logger = Substitute.For(); + var credLoader = Substitute.For(); + var builder = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) + .WithAuthority(TestConstants.AuthorityCommonTenant); + + var credentialDescription = new CredentialDescription + { + SourceType = CredentialSource.ClientSecret, + ClientSecret = "test-secret" + }; + + // Mock the credential loader to load a credential without a certificate + credLoader.LoadCredentialsIfNeededAsync(Arg.Any(), Arg.Any()) + .Returns(args => + { + var cd = (args[0] as CredentialDescription)!; + // Certificate is null by default + return Task.CompletedTask; + }); + + // Act & Assert + await Assert.ThrowsAsync( + () => builder.WithClientCredentialsAsync( + new[] { credentialDescription }, + logger, + credLoader, + credentialSourceLoaderParameters: null, + isTokenBinding: true)); + } + + [Fact] + public async Task WithBindingCertificateAsync_CredentialLoadingFails_PropagatesException() + { + // Arrange + var logger = Substitute.For(); + var credLoader = Substitute.For(); + var builder = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) + .WithAuthority(TestConstants.AuthorityCommonTenant); + + var credentialDescription = new CredentialDescription + { + SourceType = CredentialSource.StoreWithThumbprint, + CertificateThumbprint = "invalid-thumbprint", + CertificateStorePath = "CurrentUser/My" + }; + + var expectedException = new Exception("Certificate not found"); + + // Mock the credential loader to throw an exception + credLoader.LoadCredentialsIfNeededAsync(Arg.Any(), Arg.Any()) + .ThrowsAsync(expectedException); + + // Act & Assert + var actualException = await Assert.ThrowsAsync( + () => builder.WithClientCredentialsAsync( + new[] { credentialDescription }, + logger, + credLoader, + credentialSourceLoaderParameters: null, + isTokenBinding: true)); + + // Verify the exception is propagated from LoadCredentialForMsalOrFailAsync + Assert.Contains("Certificate not found", actualException.Message, StringComparison.Ordinal); + } + + [Fact] + public async Task WithBindingCertificateAsync_EmptyCredentialsList_ThrowsException() + { + // Arrange + var logger = Substitute.For(); + var credLoader = Substitute.For(); + var builder = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) + .WithAuthority(TestConstants.AuthorityCommonTenant); + + // Act & Assert + await Assert.ThrowsAsync( + () => builder.WithClientCredentialsAsync( + new CredentialDescription[0], + logger, + credLoader, + credentialSourceLoaderParameters: null, + isTokenBinding: true)); + } + + [Fact] + public async Task WithBindingCertificateAsync_WithCredentialSourceLoaderParameters_PassesParametersCorrectly() + { + // Arrange + var logger = Substitute.For(); + var credLoader = Substitute.For(); + var builder = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) + .WithAuthority(TestConstants.AuthorityCommonTenant); + + var credentialDescription = new CredentialDescription + { + SourceType = CredentialSource.StoreWithThumbprint, + CertificateThumbprint = "test-thumbprint", + CertificateStorePath = "CurrentUser/My" + }; + + var testCertificate = Base64EncodedCertificateLoader.LoadFromBase64Encoded( + TestConstants.CertificateX5cWithPrivateKey, + TestConstants.CertificateX5cWithPrivateKeyPassword, + X509KeyStorageFlags.DefaultKeySet); + + var credentialSourceLoaderParameters = new CredentialSourceLoaderParameters("test-client-id", "test-tenant-id"); + + // Mock the credential loader to successfully load the certificate + credLoader.LoadCredentialsIfNeededAsync(Arg.Any(), Arg.Any()) + .Returns(args => + { + var cd = (args[0] as CredentialDescription)!; + cd.Certificate = testCertificate; + return Task.CompletedTask; + }); + + // Act + var result = await builder.WithClientCredentialsAsync( + new[] { credentialDescription }, + logger, + credLoader, + credentialSourceLoaderParameters, + isTokenBinding: true); + + // Assert + Assert.NotNull(result); + await credLoader.Received(1).LoadCredentialsIfNeededAsync(credentialDescription, credentialSourceLoaderParameters); + } + + #endregion + } } diff --git a/tests/Microsoft.Identity.Web.Test/CertificatesObserverTests.cs b/tests/Microsoft.Identity.Web.Test/CertificatesObserverTests.cs index 7f337c34e..d7f084287 100644 --- a/tests/Microsoft.Identity.Web.Test/CertificatesObserverTests.cs +++ b/tests/Microsoft.Identity.Web.Test/CertificatesObserverTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Net; @@ -49,8 +50,11 @@ static void RemoveCertificate(X509Certificate2? certificate) var instance = "https://login.microsoftonline.com/"; var authority = instance + tenantId; - string certName = "CN=TestCert"; + string certName = $"CN=TestCert-{Guid.NewGuid():N}"; cert1 = CreateAndInstallCertificate(certName); + + // Verify certificate is properly installed in store with timeout + await VerifyCertificateInStoreAsync(cert1, TimeSpan.FromSeconds(5)); var description = new CredentialDescription { SourceType = CredentialSource.StoreWithDistinguishedName, @@ -130,6 +134,9 @@ static void RemoveCertificate(X509Certificate2? certificate) RemoveCertificate(cert1); cert2 = CreateAndInstallCertificate(certName); + // Verify certificate is properly installed in store with timeout + await VerifyCertificateInStoreAsync(cert2, TimeSpan.FromSeconds(5)); + // Rerun but it fails this time mockHttpFactory.ValidCertificates.Clear(); mockHttpFactory.ValidCertificates.Add(cert2); @@ -192,6 +199,31 @@ internal X509Certificate2 CreateAndInstallCertificate(string certName) return certWithPrivateKey; } + /// + /// Verifies that a certificate is properly installed in the certificate store. + /// + /// The certificate to verify. + /// Maximum time to wait for the certificate to appear in the store. + private static async Task VerifyCertificateInStoreAsync(X509Certificate2 certificate, TimeSpan timeout) + { + var stopwatch = Stopwatch.StartNew(); + var minWaitTime = TimeSpan.FromSeconds(2); // Minimum wait to ensure store operations complete + do + { + using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); + store.Open(OpenFlags.ReadOnly); + var foundCerts = store.Certificates.Find(X509FindType.FindByThumbprint, certificate.Thumbprint, false); + if (foundCerts.Count > 0 && stopwatch.Elapsed >= minWaitTime) + { + return; // Certificate found and minimum wait time elapsed + } + + await Task.Delay(100); // Wait 100ms before checking again + } + while (stopwatch.Elapsed < timeout); + throw new TimeoutException($"Certificate with thumbprint {certificate.Thumbprint} was not found in the certificate store within {timeout.TotalSeconds} seconds."); + } + private class TestCertificatesObserver : ICertificatesObserver { public Queue Events { get; } = new(); @@ -327,13 +359,13 @@ protected override Task SendAsync(HttpRequestMessage reques if (uri.StartsWith(kvp.Key, StringComparison.OrdinalIgnoreCase)) { if (this.description.Certificate == null || - !this.ValidCertificates.Contains(this.description.Certificate)) + !this.ValidCertificates.Any(cert => cert.Thumbprint.Equals(this.description.Certificate?.Thumbprint, StringComparison.OrdinalIgnoreCase))) { var errorResponse = new { error = "invalid_client", - error_description = $"Invalid certificate: {this.description.CachedValue}", - error_codes = new[] { 50000 }, + error_description = $"AADSTS700027: Invalid certificate: {this.description.CachedValue}", + error_codes = new[] { 700027 }, timestamp = DateTime.UtcNow, }; diff --git a/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs b/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs index 4e2770e37..badf6667f 100644 --- a/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs +++ b/tests/Microsoft.Identity.Web.Test/CustomSignedAssertionProviderTests.cs @@ -32,30 +32,35 @@ public async Task ProcessCustomSignedAssertionAsync_Tests(CustomSignedAssertionP var loader = new DefaultCredentialsLoader(data.AssertionProviderList, loggerMock); // Act - try + if (data.ExpectedExceptionType != null) { - await loader.LoadCredentialsIfNeededAsync(data.CredentialDescription, null); - } - catch (Exception ex) - { - Assert.Equal(data.ExpectedExceptionMessage, ex.Message); + var ex = await Assert.ThrowsAsync(data.ExpectedExceptionType, async () => await loader.LoadCredentialsIfNeededAsync(data.CredentialDescription, null)); + + if (data.ExpectedExceptionMessage != null) + { + Assert.Equal(data.ExpectedExceptionMessage, ex.Message); + } // This is validating the logging behavior defined by DefaultCredentialsLoader.Logger.CustomSignedAssertionProviderLoadingFailure if (data.ExpectedLogMessage is not null) { Assert.Contains(loggerMock.LoggedMessages, log => log.LogLevel == data.ExpectedLogLevel && log.Message.Contains(data.ExpectedLogMessage, StringComparison.InvariantCulture)); } - return; - } - - // Assert - if (data.ExpectedLogMessage is not null) - { - Assert.Contains(loggerMock.LoggedMessages, log => log.LogLevel == data.ExpectedLogLevel && log.Message.Contains(data.ExpectedLogMessage, StringComparison.InvariantCulture)); } else { - Assert.DoesNotContain(loggerMock.LoggedMessages, log => log.LogLevel == data.ExpectedLogLevel); + // No exception expected + await loader.LoadCredentialsIfNeededAsync(data.CredentialDescription, null); + + // Assert + if (data.ExpectedLogMessage is not null) + { + Assert.Contains(loggerMock.LoggedMessages, log => log.LogLevel == data.ExpectedLogLevel && log.Message.Contains(data.ExpectedLogMessage, StringComparison.InvariantCulture)); + } + else + { + Assert.DoesNotContain(loggerMock.LoggedMessages, log => log.LogLevel == data.ExpectedLogLevel); + } } } @@ -74,7 +79,9 @@ public static TheoryData CustomSignedAs Skip = false }, ExpectedLogLevel = LogLevel.Error, - ExpectedLogMessage = CertificateErrorMessage.CustomProviderSourceLoaderNullOrEmpty + ExpectedLogMessage = CertificateErrorMessage.CustomProviderSourceLoaderNullOrEmpty, + ExpectedExceptionType = typeof(InvalidOperationException), + ExpectedExceptionMessage = CertificateErrorMessage.CustomProviderSourceLoaderNullOrEmpty }, // No provider name given @@ -87,7 +94,9 @@ public static TheoryData CustomSignedAs SourceType = CredentialSource.CustomSignedAssertion }, ExpectedLogLevel = LogLevel.Error, - ExpectedLogMessage = CertificateErrorMessage.CustomProviderNameNullOrEmpty + ExpectedLogMessage = CertificateErrorMessage.CustomProviderNameNullOrEmpty, + ExpectedExceptionType = typeof(InvalidOperationException), + ExpectedExceptionMessage = CertificateErrorMessage.CustomProviderNameNullOrEmpty }, // Given provider name not found @@ -100,7 +109,9 @@ public static TheoryData CustomSignedAs SourceType = CredentialSource.CustomSignedAssertion }, ExpectedLogLevel = LogLevel.Error, - ExpectedLogMessage = string.Format(CultureInfo.InvariantCulture, CertificateErrorMessage.CustomProviderNotFound, "Provider3") + ExpectedLogMessage = string.Format(CultureInfo.InvariantCulture, CertificateErrorMessage.CustomProviderNotFound, "Provider3"), + ExpectedExceptionType = typeof(InvalidOperationException), + ExpectedExceptionMessage = string.Format(CultureInfo.InvariantCulture, CertificateErrorMessage.CustomProviderNotFound, "Provider3") }, // Happy path (no logging expected) @@ -134,6 +145,7 @@ public static TheoryData CustomSignedAs false.ToString() ) ), + ExpectedExceptionType = typeof(Exception), ExpectedExceptionMessage = FailingCustomSignedAssertionProvider.ExceptionMessage }, @@ -160,6 +172,7 @@ public class CustomSignedAssertionProviderTheoryData public CredentialDescription CredentialDescription { get; set; } = new CredentialDescription(); public LogLevel ExpectedLogLevel { get; set; } public string? ExpectedLogMessage { get; set; } + public Type? ExpectedExceptionType { get; set; } public string? ExpectedExceptionMessage { get; set; } } diff --git a/tests/Microsoft.Identity.Web.Test/DefaultAuthorizationHeaderProviderTests.cs b/tests/Microsoft.Identity.Web.Test/DefaultAuthorizationHeaderProviderTests.cs new file mode 100644 index 000000000..a09695376 --- /dev/null +++ b/tests/Microsoft.Identity.Web.Test/DefaultAuthorizationHeaderProviderTests.cs @@ -0,0 +1,648 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Client; +using Microsoft.Identity.Web.Test.Common; +using NSubstitute; +using Xunit; + +namespace Microsoft.Identity.Web.Test +{ + [Collection(nameof(TokenAcquirerFactorySingletonProtection))] + public class DefaultAuthorizationHeaderProviderTests + { + private readonly ITokenAcquisition _mockTokenAcquisition; + private readonly DefaultAuthorizationHeaderProvider _provider; + + public DefaultAuthorizationHeaderProviderTests() + { + _mockTokenAcquisition = Substitute.For(); + _provider = new DefaultAuthorizationHeaderProvider(_mockTokenAcquisition); + } + + [Fact] + public void Constructor_WithValidParameters_InitializesCorrectly() + { + // Arrange & Act + var provider = new DefaultAuthorizationHeaderProvider(_mockTokenAcquisition); + + // Assert + Assert.NotNull(provider); + } + + [Fact] + public async Task CreateAuthorizationHeaderAsync_ForBoundHeaderProviderWithNonMtlsProtocolAndUserFlow_ReturnsValidResult() + { + // Arrange + var downstreamApiOptions = new DownstreamApiOptions + { + Scopes = new[] { "https://graph.microsoft.com/.default" } + }; + + var mockAuthenticationResult = new AuthenticationResult( + "test_access_token", + false, + null, + DateTimeOffset.UtcNow.AddHours(1), + DateTimeOffset.UtcNow.AddHours(1), + "test_tenant_id", + null, + null, + new[] { "scope1" }, + Guid.NewGuid()); + + _mockTokenAcquisition + .GetAuthenticationResultForUserAsync( + Arg.Any>(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(Task.FromResult(mockAuthenticationResult)); + + var claimsPrincipal = new ClaimsPrincipal(); + var cancellationToken = CancellationToken.None; + + // Act + var result = await ((IBoundAuthorizationHeaderProvider)_provider).CreateBoundAuthorizationHeaderAsync( + downstreamApiOptions, + claimsPrincipal, + cancellationToken); + + // Assert + Assert.NotNull(result.Result); + Assert.Equal("Bearer test_access_token", result.Result.AuthorizationHeaderValue); + Assert.Null(result.Result.BindingCertificate); + + await _mockTokenAcquisition.Received(1).GetAuthenticationResultForUserAsync( + Arg.Any>(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()); + } + + [Fact] + public async Task CreateAuthorizationHeaderAsync_ForBoundHeaderProviderWithNonMtlsProtocolAndAppFlow_ReturnsValidResult() + { + // Arrange + var downstreamApiOptions = new DownstreamApiOptions + { + Scopes = new[] { "https://graph.microsoft.com/.default" }, + RequestAppToken = true + }; + + var mockAuthenticationResult = new AuthenticationResult( + "test_access_token", + false, + null, + DateTimeOffset.UtcNow.AddHours(1), + DateTimeOffset.UtcNow.AddHours(1), + "test_tenant_id", + null, + null, + new[] { "scope1" }, + Guid.NewGuid()); + + _mockTokenAcquisition + .GetAuthenticationResultForAppAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(Task.FromResult(mockAuthenticationResult)); + + var claimsPrincipal = new ClaimsPrincipal(); + var cancellationToken = CancellationToken.None; + + // Act + var result = await ((IBoundAuthorizationHeaderProvider)_provider).CreateBoundAuthorizationHeaderAsync( + downstreamApiOptions, + claimsPrincipal, + cancellationToken); + + // Assert + Assert.NotNull(result.Result); + Assert.Equal("Bearer test_access_token", result.Result.AuthorizationHeaderValue); + Assert.Null(result.Result.BindingCertificate); + + await _mockTokenAcquisition.Received(1).GetAuthenticationResultForAppAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()); + } + + [Fact] + public async Task CreateAuthorizationHeaderAsync_ForBoundHeaderProviderWithMtlsProtocolAndAppFlow_ReturnsValidResult() + { + // Arrange + var downstreamApiOptions = new DownstreamApiOptions + { + Scopes = new[] { "https://graph.microsoft.com/.default" }, + ProtocolScheme = "MTLS_POP", + RequestAppToken = true + }; + + var mockAuthenticationResult = new AuthenticationResult( + "test_access_token", + false, + null, + DateTimeOffset.UtcNow.AddHours(1), + DateTimeOffset.UtcNow.AddHours(1), + "test_tenant_id", + null, + null, + new[] { "scope1" }, + Guid.NewGuid(), + "MTLS_POP"); + + _mockTokenAcquisition + .GetAuthenticationResultForAppAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(Task.FromResult(mockAuthenticationResult)); + + var claimsPrincipal = new ClaimsPrincipal(); + var cancellationToken = CancellationToken.None; + + // Act + var result = await ((IBoundAuthorizationHeaderProvider)_provider).CreateBoundAuthorizationHeaderAsync( + downstreamApiOptions, + claimsPrincipal, + cancellationToken); + + // Assert + Assert.NotNull(result.Result); + Assert.Equal("MTLS_POP test_access_token", result.Result.AuthorizationHeaderValue); + + await _mockTokenAcquisition.Received(1).GetAuthenticationResultForAppAsync( + "https://graph.microsoft.com/.default", + null, + null, + Arg.Is(o => + o.ExtraParameters != null && + o.ExtraParameters.ContainsKey("IsTokenBinding") && + o.ExtraParameters["IsTokenBinding"] is bool && + (bool)o.ExtraParameters["IsTokenBinding"] == true)); + } + + [Theory] + [InlineData(false)] + [InlineData(null)] + public async Task CreateAuthorizationHeaderAsync_ForBoundHeaderProviderWithMtlsProtocolForUserFlow_ThrowsArgumentException(bool? requestAppToken) + { + // Arrange + var downstreamApiOptions = new DownstreamApiOptions + { + Scopes = new[] { "https://graph.microsoft.com/.default" }, + ProtocolScheme = "MTLS_POP" + }; + + if (requestAppToken.HasValue) + { + downstreamApiOptions.RequestAppToken = requestAppToken.Value; + } + + var claimsPrincipal = new ClaimsPrincipal(); + var cancellationToken = CancellationToken.None; + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => ((IBoundAuthorizationHeaderProvider)_provider).CreateBoundAuthorizationHeaderAsync( + downstreamApiOptions, + claimsPrincipal, + cancellationToken)); + + Assert.Contains("Token binding requires enabled app token acquisition", exception.Message, StringComparison.OrdinalIgnoreCase); + Assert.Equal("RequestAppToken", exception.ParamName); + } + + [Fact] + public async Task CreateAuthorizationHeaderAsync_ForBoundHeaderProviderWithBindingCertificate_ReturnsBindingCertificate() + { + // Arrange + var downstreamApiOptions = new DownstreamApiOptions + { + Scopes = new[] { "https://graph.microsoft.com/.default" }, + ProtocolScheme = "MTLS_POP", + RequestAppToken = true + }; + + // Create test certificate + var bytes = Convert.FromBase64String(TestConstants.CertificateX5c); +#if NET9_0_OR_GREATER + var bindingCertificate = X509CertificateLoader.LoadCertificate(bytes); +#else +#pragma warning disable SYSLIB0057 // Type or member is obsolete + var bindingCertificate = new X509Certificate2(bytes); +#pragma warning restore SYSLIB0057 // Type or member is obsolete +#endif + + var mockAuthenticationResult = new AuthenticationResult( + "test_access_token", + false, + null, + DateTimeOffset.UtcNow.AddHours(1), + DateTimeOffset.UtcNow.AddHours(1), + "test_tenant_id", + null, + null, + new[] { "scope1" }, + Guid.NewGuid(), + "MTLS_POP") + { + BindingCertificate = bindingCertificate + }; + + _mockTokenAcquisition + .GetAuthenticationResultForAppAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(Task.FromResult(mockAuthenticationResult)); + + // Act + var result = await ((IBoundAuthorizationHeaderProvider)_provider).CreateBoundAuthorizationHeaderAsync( + downstreamApiOptions, + null, + CancellationToken.None); + + // Assert + Assert.NotNull(result.Result); + Assert.Equal("MTLS_POP test_access_token", result.Result.AuthorizationHeaderValue); + Assert.Same(bindingCertificate, result.Result.BindingCertificate); + } + + [Fact] + public async Task CreateAuthorizationHeaderAsync_ForBoundHeaderProviderWithNullScopes_HandlesGracefully() + { + // Arrange + var downstreamApiOptions = new DownstreamApiOptions + { + Scopes = null, // Null scopes + ProtocolScheme = "MTLS_POP", + RequestAppToken = true + }; + + var mockAuthenticationResult = new AuthenticationResult( + "test_access_token", + false, + null, + DateTimeOffset.UtcNow.AddHours(1), + DateTimeOffset.UtcNow.AddHours(1), + "test_tenant_id", + null, + null, + new[] { "scope1" }, + Guid.NewGuid()); + + _mockTokenAcquisition + .GetAuthenticationResultForAppAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(Task.FromResult(mockAuthenticationResult)); + + // Act & Assert - Should not throw + var result = await ((IBoundAuthorizationHeaderProvider)_provider).CreateBoundAuthorizationHeaderAsync( + downstreamApiOptions, + null, + CancellationToken.None); + + Assert.NotNull(result.Result); + } + + [Fact] + public async Task CreateAuthorizationHeaderAsync_WithUserScopes_AcquiresUserToken() + { + // Arrange + var scopes = new[] { "User.Read", "Mail.Read" }; + var options = new AuthorizationHeaderProviderOptions(); + var claimsPrincipal = new ClaimsPrincipal(); + var cancellationToken = CancellationToken.None; + var expectedHeader = "Bearer test-token"; + + var mockAuthenticationResult = new AuthenticationResult( + "test-token", + false, + null, + DateTimeOffset.UtcNow.AddHours(1), + DateTimeOffset.UtcNow.AddHours(1), + "tenant_id", + null, + null, + scopes, + Guid.NewGuid()); + + _mockTokenAcquisition + .GetAuthenticationResultForUserAsync( + scopes, + null, + null, + null, + claimsPrincipal, + Arg.Any()) + .Returns(Task.FromResult(mockAuthenticationResult)); + + // Act + var result = await ((IAuthorizationHeaderProvider)_provider).CreateAuthorizationHeaderAsync(scopes, options, claimsPrincipal, cancellationToken); + + // Assert + Assert.Equal(expectedHeader, result); + await _mockTokenAcquisition.Received(1) + .GetAuthenticationResultForUserAsync(scopes, null, null, null, claimsPrincipal, Arg.Any()); + } + + [Fact] + public async Task CreateAuthorizationHeaderForAppAsync_AcquiresAppToken() + { + // Arrange + var scopes = "https://graph.microsoft.com/.default"; + var options = new AuthorizationHeaderProviderOptions(); + var cancellationToken = CancellationToken.None; + var expectedHeader = "Bearer app-token"; + + var mockAuthenticationResult = new AuthenticationResult( + "app-token", + false, + null, + DateTimeOffset.UtcNow.AddHours(1), + DateTimeOffset.UtcNow.AddHours(1), + "tenant_id", + null, + null, + new[] { scopes }, + Guid.NewGuid()); + + _mockTokenAcquisition + .GetAuthenticationResultForAppAsync( + scopes, + null, + null, + Arg.Any()) + .Returns(Task.FromResult(mockAuthenticationResult)); + + // Act + var result = await ((IAuthorizationHeaderProvider)_provider).CreateAuthorizationHeaderForAppAsync(scopes, options, cancellationToken); + + // Assert + Assert.Equal(expectedHeader, result); + await _mockTokenAcquisition.Received(1) + .GetAuthenticationResultForAppAsync(scopes, null, null, Arg.Any()); + } + + [Fact] + public async Task CreateAuthorizationHeaderForUserAsync_AcquiresUserToken() + { + // Arrange + var scopes = new[] { "User.Read", "Mail.Read" }; + var options = new AuthorizationHeaderProviderOptions(); + var claimsPrincipal = new ClaimsPrincipal(); + var cancellationToken = CancellationToken.None; + var expectedHeader = "Bearer user-token"; + + var mockAuthenticationResult = new AuthenticationResult( + "user-token", + false, + null, + DateTimeOffset.UtcNow.AddHours(1), + DateTimeOffset.UtcNow.AddHours(1), + "tenant_id", + null, + null, + scopes, + Guid.NewGuid()); + + _mockTokenAcquisition + .GetAuthenticationResultForUserAsync( + scopes, + null, + null, + null, + claimsPrincipal, + Arg.Any()) + .Returns(Task.FromResult(mockAuthenticationResult)); + + // Act + var result = await ((IAuthorizationHeaderProvider)_provider).CreateAuthorizationHeaderForUserAsync(scopes, options, claimsPrincipal, cancellationToken); + + // Assert + Assert.Equal(expectedHeader, result); + await _mockTokenAcquisition.Received(1) + .GetAuthenticationResultForUserAsync(scopes, null, null, null, claimsPrincipal, Arg.Any()); + } + + [Fact] + public async Task CreateAuthorizationHeaderAsync_WithNullParameters_AcquiresUserToken() + { + // Arrange + var scopes = new[] { "User.Read" }; + var expectedHeader = "Bearer test-token"; + + var mockAuthenticationResult = new AuthenticationResult( + "test-token", + false, + null, + DateTimeOffset.UtcNow.AddHours(1), + DateTimeOffset.UtcNow.AddHours(1), + "tenant_id", + null, + null, + scopes, + Guid.NewGuid()); + + _mockTokenAcquisition + .GetAuthenticationResultForUserAsync( + scopes, + null, + null, + null, + null, + Arg.Any()) + .Returns(Task.FromResult(mockAuthenticationResult)); + + // Act + var result = await ((IAuthorizationHeaderProvider)_provider).CreateAuthorizationHeaderAsync(scopes, null, null, CancellationToken.None); + + // Assert + Assert.Equal(expectedHeader, result); + await _mockTokenAcquisition.Received(1) + .GetAuthenticationResultForUserAsync(scopes, null, null, null, null, Arg.Any()); + } + + [Fact] + public async Task CreateAuthorizationHeaderForAppAsync_WithNullOptions_AcquiresAppToken() + { + // Arrange + var scopes = "https://graph.microsoft.com/.default"; + var expectedHeader = "Bearer app-token"; + + var mockAuthenticationResult = new AuthenticationResult( + "app-token", + false, + null, + DateTimeOffset.UtcNow.AddHours(1), + DateTimeOffset.UtcNow.AddHours(1), + "tenant_id", + null, + null, + new[] { scopes }, + Guid.NewGuid()); + + _mockTokenAcquisition + .GetAuthenticationResultForAppAsync( + scopes, + null, + null, + Arg.Any()) + .Returns(Task.FromResult(mockAuthenticationResult)); + + // Act + var result = await ((IAuthorizationHeaderProvider)_provider).CreateAuthorizationHeaderForAppAsync(scopes, null, CancellationToken.None); + + // Assert + Assert.Equal(expectedHeader, result); + await _mockTokenAcquisition.Received(1) + .GetAuthenticationResultForAppAsync(scopes, null, null, Arg.Any()); + } + + [Fact] + public async Task CreateAuthorizationHeaderForUserAsync_WithNullParameters_AcquiresUserToken() + { + // Arrange + var scopes = new[] { "User.Read" }; + var expectedHeader = "Bearer user-token"; + + var mockAuthenticationResult = new AuthenticationResult( + "user-token", + false, + null, + DateTimeOffset.UtcNow.AddHours(1), + DateTimeOffset.UtcNow.AddHours(1), + "tenant_id", + null, + null, + scopes, + Guid.NewGuid()); + + _mockTokenAcquisition + .GetAuthenticationResultForUserAsync( + scopes, + null, + null, + null, + null, + Arg.Any()) + .Returns(Task.FromResult(mockAuthenticationResult)); + + // Act + var result = await ((IAuthorizationHeaderProvider)_provider).CreateAuthorizationHeaderForUserAsync(scopes, null, null, CancellationToken.None); + + // Assert + Assert.Equal(expectedHeader, result); + await _mockTokenAcquisition.Received(1) + .GetAuthenticationResultForUserAsync(scopes, null, null, null, null, Arg.Any()); + } + + [Fact] + public async Task CreateAuthorizationHeaderAsync_ForBoundHeaderProvider_TokenAcquisitionThrows_PropagatesException() + { + // Arrange + var downstreamApiOptions = new DownstreamApiOptions + { + Scopes = new[] { "https://graph.microsoft.com/.default" }, + ProtocolScheme = "MTLS_POP", + RequestAppToken = true + }; + + var expectedException = new MsalServiceException("test-error", "Test error message"); + _mockTokenAcquisition + .GetAuthenticationResultForAppAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(Task.FromException(expectedException)); + + // Act & Assert + var actualException = await Assert.ThrowsAsync( + () => ((IBoundAuthorizationHeaderProvider)_provider).CreateBoundAuthorizationHeaderAsync(downstreamApiOptions, null, CancellationToken.None)); + + Assert.Equal(expectedException.ErrorCode, actualException.ErrorCode); + Assert.Equal(expectedException.Message, actualException.Message); + } + + [Fact] + public async Task CreateAuthorizationHeaderAsync_ForBoundHeaderProvider_WithExistingExtraParameters_MergesExtraParameters() + { + // Arrange + var existingParameters = new Dictionary + { + { "custom_param", "custom_value" } + }; + + var downstreamApiOptions = new DownstreamApiOptions + { + Scopes = new[] { "https://graph.microsoft.com/.default" }, + ProtocolScheme = "MTLS_POP", + RequestAppToken = true, + AcquireTokenOptions = new AcquireTokenOptions + { + ExtraParameters = existingParameters + } + }; + + var mockAuthenticationResult = new AuthenticationResult( + "test_access_token", + false, + null, + DateTimeOffset.UtcNow.AddHours(1), + DateTimeOffset.UtcNow.AddHours(1), + "test_tenant_id", + null, + null, + new[] { "scope1" }, + Guid.NewGuid()); + + _mockTokenAcquisition + .GetAuthenticationResultForAppAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(Task.FromResult(mockAuthenticationResult)); + + // Act + await ((IBoundAuthorizationHeaderProvider)_provider).CreateBoundAuthorizationHeaderAsync( + downstreamApiOptions, + null, + CancellationToken.None); + + // Assert + await _mockTokenAcquisition.Received(1).GetAuthenticationResultForAppAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Is(o => + o.ExtraParameters != null && + o.ExtraParameters.ContainsKey("IsTokenBinding") && + o.ExtraParameters.ContainsKey("custom_param") && + (bool)o.ExtraParameters["IsTokenBinding"] == true && + (string)o.ExtraParameters["custom_param"] == "custom_value")); + } + } +} diff --git a/tests/Microsoft.Identity.Web.Test/DefaultTokenAcquirerFactoryImplementationTests.cs b/tests/Microsoft.Identity.Web.Test/DefaultTokenAcquirerFactoryImplementationTests.cs new file mode 100644 index 000000000..7fb1b0dc7 --- /dev/null +++ b/tests/Microsoft.Identity.Web.Test/DefaultTokenAcquirerFactoryImplementationTests.cs @@ -0,0 +1,165 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Web.Test.Common; +using Xunit; + +namespace Microsoft.Identity.Web.Test +{ + [Collection(nameof(TokenAcquirerFactorySingletonProtection))] + public class DefaultTokenAcquirerFactoryImplementationTests + { + [Fact] + public void GetTokenAcquirer_WithMicrosoftEntraApplicationOptions_PropagatesAllOptions() + { + // Arrange + var taf = new CustomTAF(); + var provider = taf.Build(); + var factory = provider.GetRequiredService(); + + var options = new MicrosoftEntraApplicationOptions + { + // IdentityApplicationOptions properties + ClientId = "test-client-id", + Authority = "https://login.microsoftonline.com/test-tenant", + EnablePiiLogging = true, + AllowWebApiToBeAuthorizedByACL = true, + Audience = "test-audience", + ClientCredentials = new List + { + new CredentialDescription { ClientSecret = "test-secret", SourceType = CredentialSource.ClientSecret } + }, + + // MicrosoftEntraApplicationOptions properties + Name = "test-name", + Instance = "https://login.microsoftonline.com/", + TenantId = "test-tenant-id", + AppHomeTenantId = "test-home-tenant-id", + AzureRegion = "westus", + ClientCapabilities = new List { "cp1" }, + SendX5C = true + }; + + // Act + var tokenAcquirer = factory.GetTokenAcquirer(options); + + // Assert + Assert.NotNull(tokenAcquirer); + + // Verify the options were properly stored in the merged options + var mergedOptionsStore = provider.GetRequiredService(); + var key = DefaultTokenAcquirerFactoryImplementation.GetKey(options.Authority, options.ClientId, options.AzureRegion); + var mergedOptions = mergedOptionsStore.Get(key); + + Assert.Equal(options.ClientId, mergedOptions.ClientId); + Assert.Equal(options.EnablePiiLogging, mergedOptions.EnablePiiLogging); + Assert.Equal(options.AllowWebApiToBeAuthorizedByACL, mergedOptions.AllowWebApiToBeAuthorizedByACL); + Assert.Equal(options.Instance, mergedOptions.Instance); + Assert.Equal(options.TenantId, mergedOptions.TenantId); + Assert.Equal(options.AppHomeTenantId, mergedOptions.AppHomeTenantId); + Assert.Equal(options.AzureRegion, mergedOptions.AzureRegion); + Assert.Equal(options.ClientCapabilities, mergedOptions.ClientCapabilities); + Assert.Equal(options.SendX5C, mergedOptions.SendX5C); + } + + [Fact] + public void GetTokenAcquirer_WithIdentityApplicationOptions_PropagatesBaseOptions() + { + // Arrange + var taf = new CustomTAF(); + var provider = taf.Build(); + var factory = provider.GetRequiredService(); + + var options = new IdentityApplicationOptions + { + ClientId = "test-client-id", + Authority = "https://login.microsoftonline.com/test-tenant", + EnablePiiLogging = true, + AllowWebApiToBeAuthorizedByACL = true, + Audience = "test-audience", + ClientCredentials = new List + { + new CredentialDescription { ClientSecret = "test-secret", SourceType = CredentialSource.ClientSecret } + } + }; + + // Act + var tokenAcquirer = factory.GetTokenAcquirer(options); + + // Assert + Assert.NotNull(tokenAcquirer); + + // Verify the options were properly stored in the merged options + var mergedOptionsStore = provider.GetRequiredService(); + var key = DefaultTokenAcquirerFactoryImplementation.GetKey(options.Authority, options.ClientId, null); + var mergedOptions = mergedOptionsStore.Get(key); + + Assert.Equal(options.ClientId, mergedOptions.ClientId); + Assert.Equal(options.EnablePiiLogging, mergedOptions.EnablePiiLogging); + Assert.Equal(options.AllowWebApiToBeAuthorizedByACL, mergedOptions.AllowWebApiToBeAuthorizedByACL); + } + + [Fact] + public void GetTokenAcquirer_WithMicrosoftIdentityApplicationOptions_UsesAsIs() + { + // Arrange + var taf = new CustomTAF(); + var provider = taf.Build(); + var factory = provider.GetRequiredService(); + + var options = new MicrosoftIdentityApplicationOptions + { + ClientId = "test-client-id", + Authority = "https://login.microsoftonline.com/test-tenant", + EnablePiiLogging = true, + AllowWebApiToBeAuthorizedByACL = true, + Instance = "https://login.microsoftonline.com/", + TenantId = "test-tenant-id", + AzureRegion = "westus", + SendX5C = true, + Domain = "test-domain.com", + SignUpSignInPolicyId = "B2C_1_signupsignin" + }; + + // Act + var tokenAcquirer = factory.GetTokenAcquirer(options); + + // Assert + Assert.NotNull(tokenAcquirer); + + // Verify the options were properly stored in the merged options + var mergedOptionsStore = provider.GetRequiredService(); + var key = DefaultTokenAcquirerFactoryImplementation.GetKey(options.Authority, options.ClientId, options.AzureRegion); + var mergedOptions = mergedOptionsStore.Get(key); + + Assert.Equal(options.ClientId, mergedOptions.ClientId); + Assert.Equal(options.EnablePiiLogging, mergedOptions.EnablePiiLogging); + Assert.Equal(options.Instance, mergedOptions.Instance); + Assert.Equal(options.TenantId, mergedOptions.TenantId); + Assert.Equal(options.AzureRegion, mergedOptions.AzureRegion); + Assert.Equal(options.SendX5C, mergedOptions.SendX5C); + Assert.Equal(options.Domain, mergedOptions.Domain); + Assert.Equal(options.SignUpSignInPolicyId, mergedOptions.SignUpSignInPolicyId); + } + + private class CustomTAF : TokenAcquirerFactory + { + public CustomTAF() + { + this.Services.AddTokenAcquisition(); + this.Services.AddHttpClient(); + this.Services.AddSingleton(); + } + + protected override string DefineConfiguration(IConfigurationBuilder builder) + { + return AppContext.BaseDirectory; + } + } + } +} diff --git a/tests/Microsoft.Identity.Web.Test/DownstreamWebApiSupport/DownstreamApiTests.cs b/tests/Microsoft.Identity.Web.Test/DownstreamWebApiSupport/DownstreamApiTests.cs index 774ec159e..243d186a5 100644 --- a/tests/Microsoft.Identity.Web.Test/DownstreamWebApiSupport/DownstreamApiTests.cs +++ b/tests/Microsoft.Identity.Web.Test/DownstreamWebApiSupport/DownstreamApiTests.cs @@ -8,15 +8,21 @@ using System.Net; using System.Net.Http; using System.Security.Claims; +using System.Security.Cryptography.X509Certificates; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Client; +using Microsoft.Identity.Web.Test.Common; +using Microsoft.Identity.Web.Test.Common.Mocks; using Microsoft.Identity.Web.Test.Resource; +using NSubstitute; using Xunit; namespace Microsoft.Identity.Web.Tests @@ -77,10 +83,10 @@ public async Task UpdateRequestAsync_AddsToExtraQP() // Arrange var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, "https://example.com"); var content = new StringContent("test content"); - var options = new DownstreamApiOptions() { - AcquireTokenOptions = new AcquireTokenOptions() { - ExtraQueryParameters = new Dictionary() - { + var options = new DownstreamApiOptions() { + AcquireTokenOptions = new AcquireTokenOptions() { + ExtraQueryParameters = new Dictionary() + { { "n1", "v1" }, { "n2", "v2" }, { "caller-sdk-id", "bogus" } // value will be overwritten by the SDK @@ -97,7 +103,7 @@ public async Task UpdateRequestAsync_AddsToExtraQP() Assert.Equal("v1", options.AcquireTokenOptions.ExtraQueryParameters["n1"]); Assert.Equal("v2", options.AcquireTokenOptions.ExtraQueryParameters["n2"]); Assert.Equal( - DownstreamApi.CallerSDKDetails["caller-sdk-id"], + DownstreamApi.CallerSDKDetails["caller-sdk-id"], options.AcquireTokenOptions.ExtraQueryParameters["caller-sdk-id"] ); Assert.Equal( DownstreamApi.CallerSDKDetails["caller-sdk-ver"], @@ -464,11 +470,375 @@ public async Task ReadErrorResponseContentAsync_ReturnsMessage_WhenContentLength Assert.NotNull(result); // Either we get the truncation message about size, or the actual content is truncated Assert.True( - result.Contains("[Error response too large:", StringComparison.Ordinal) || + result.Contains("[Error response too large:", StringComparison.Ordinal) || result.EndsWith("... (truncated)", StringComparison.Ordinal) || result.Length <= 4096 + "... (truncated)".Length, "Error response should be limited in size"); } + + [Fact] + public void DownstreamApi_Constructor_WithBoundProvider_AcceptsIMsalMtlsHttpClientFactory() + { + // Arrange + var mockBoundProvider = Substitute.For(); + var mockMtlsHttpClientFactory = Substitute.For(); + + // Act & Assert - Should not throw + var downstreamApi = new DownstreamApi( + mockBoundProvider, + _namedDownstreamApiOptions, + mockMtlsHttpClientFactory, + _logger); + + Assert.NotNull(downstreamApi); + } + + [Theory] + [InlineData(null, false)] + [InlineData("", false)] + [InlineData("Bearer", false)] + [InlineData("mtls_pop", true)] + [InlineData("Mtls_Pop", true)] + [InlineData("MTLS_POP", true)] + public async Task UpdateRequestAsync_WithAuthorizationHeaderBoundProvider_CallsCorrectInterface(string? ProtocolScheme, bool shouldCallAuthorizationHeaderProvider2) + { + // Arrange + var mockBoundProvider = Substitute.For(); + var testCertificate = Substitute.For(); + + var downstreamApi = new DownstreamApi( + mockBoundProvider, + _namedDownstreamApiOptions, + _httpClientFactory, + _logger); + + var options = new DownstreamApiOptions + { + Scopes = new[] { "https://api.example.com/.default" }, + }; + + if (ProtocolScheme != null) + { + options.ProtocolScheme = ProtocolScheme; + } + + var authHeaderInfo = new AuthorizationHeaderInformation + { + AuthorizationHeaderValue = "MTLS_POP test-token", + BindingCertificate = testCertificate + }; + + var mockResult = new OperationResult(authHeaderInfo); + + ((IAuthorizationHeaderProvider)mockBoundProvider) + .CreateAuthorizationHeaderAsync( + Arg.Any>(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns("Bearer test-token"); + + ((IBoundAuthorizationHeaderProvider)mockBoundProvider) + .CreateBoundAuthorizationHeaderAsync( + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(mockResult); + + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "https://api.example.com"); + + // Act + var result = await downstreamApi.UpdateRequestAsync( + httpRequestMessage, + null, + options, + false, + null, + CancellationToken.None); + + // Assert + if (shouldCallAuthorizationHeaderProvider2) + { + await ((IBoundAuthorizationHeaderProvider)mockBoundProvider).Received(1).CreateBoundAuthorizationHeaderAsync( + Arg.Any(), + Arg.Any(), + Arg.Any()); + + await mockBoundProvider.DidNotReceive().CreateAuthorizationHeaderAsync( + Arg.Any>(), + Arg.Any(), + Arg.Any(), + Arg.Any()); + + Assert.NotNull(result); + Assert.Equal("MTLS_POP test-token", result.AuthorizationHeaderValue); + Assert.Equal(testCertificate, result.BindingCertificate); + Assert.Equal("MTLS_POP test-token", httpRequestMessage.Headers.Authorization?.ToString()); + } + else + { + await ((IBoundAuthorizationHeaderProvider)mockBoundProvider).DidNotReceive().CreateBoundAuthorizationHeaderAsync( + Arg.Any(), + Arg.Any(), + Arg.Any()); + + await mockBoundProvider.Received(1).CreateAuthorizationHeaderAsync( + Arg.Any>(), + Arg.Any(), + Arg.Any(), + Arg.Any()); + + Assert.Null(result); + Assert.Equal("Bearer test-token", httpRequestMessage.Headers.Authorization?.ToString()); + } + } + + [Fact] + public async Task UpdateRequestAsync_WithRegularAuthorizationHeaderProvider_FallsBackCorrectly() + { + // Arrange - Using the existing regular provider from the constructor + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "https://api.example.com"); + var options = new DownstreamApiOptions + { + Scopes = new[] { "https://api.example.com/.default" } + }; + + // Act + var result = await _input.UpdateRequestAsync( + httpRequestMessage, + null, + options, + false, + null, + CancellationToken.None); + + // Assert + Assert.Null(result); // Regular provider doesn't return AuthorizationHeaderInformation + Assert.Equal("Bearer ey", httpRequestMessage.Headers.Authorization?.ToString()); + } + + [Fact] + public async Task CallApiInternalAsync_WithRegularAuthorizationHeaderProvider_UsesRegularHttpClientFactory() + { + // Arrange + var mockHttpClientFactory = Substitute.For(); + var mockHandler = new MockHttpMessageHandler() + { + ExpectedMethod = HttpMethod.Get, + ResponseMessage = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("{\"result\": \"success\"}") + } + }; + var mockHttpClient = new HttpClient(mockHandler); + + mockHttpClientFactory.CreateClient(Arg.Any()).Returns(mockHttpClient); + + var downstreamApi = new DownstreamApi( + _authorizationHeaderProvider, // Regular provider + _namedDownstreamApiOptions, + mockHttpClientFactory, + _logger); + + var options = new DownstreamApiOptions + { + BaseUrl = "https://api.example.com", + Scopes = new[] { "https://api.example.com/.default" }, + HttpMethod = "GET" + }; + + // Act + await downstreamApi.CallApiInternalAsync(null, options, false, null, null, CancellationToken.None); + + // Assert + mockHttpClientFactory.Received(1).CreateClient(Arg.Any()); + + // Note: HttpClient is disposed by DownstreamApi, no manual disposal needed + } + + [Fact] + public async Task CallApiInternalAsync_WithAuthorizationHeaderBoundProviderAndWithBindingCertificate_UsesMtlsHttpClientFactory() + { + // Arrange + var mockBoundProvider = Substitute.For(); + var mockMtlsHttpClientFactory = Substitute.For(); + var testCertificate = CreateTestCertificate(); + + var mockHandler = new MockHttpMessageHandler() + { + ExpectedMethod = HttpMethod.Get, + ResponseMessage = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("{\"result\": \"success\"}") + } + }; + + // Create HttpClient with our mock handler + var mockMtlsHttpClient = new HttpClient(mockHandler); + + var downstreamApi = new DownstreamApi( + mockBoundProvider, + _namedDownstreamApiOptions, + mockMtlsHttpClientFactory, + _logger, + (IMsalHttpClientFactory)mockMtlsHttpClientFactory); + + var options = new DownstreamApiOptions + { + BaseUrl = "https://api.example.com", + Scopes = new[] { "https://api.example.com/.default" }, + HttpMethod = "GET", + ProtocolScheme = "MTLS_POP" + }; + + var authHeaderInfo = new AuthorizationHeaderInformation + { + AuthorizationHeaderValue = "MTLS_POP test-token", + BindingCertificate = testCertificate + }; + + var mockResult = new OperationResult(authHeaderInfo); + + ((IBoundAuthorizationHeaderProvider)mockBoundProvider) + .CreateBoundAuthorizationHeaderAsync( + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(mockResult); + + // Setup mTLS HTTP client factory to return our pre-configured HttpClient + ((IMsalMtlsHttpClientFactory)mockMtlsHttpClientFactory) + .GetHttpClient(testCertificate) + .Returns(mockMtlsHttpClient); + + // Act + await downstreamApi.CallApiInternalAsync(null, options, false, null, null, CancellationToken.None); + + // Assert - Verify mTLS HTTP client factory was used + var _ = ((IMsalMtlsHttpClientFactory)mockMtlsHttpClientFactory).Received(1).GetHttpClient(testCertificate); + + // Verify regular HTTP client factory was NOT used + ((IHttpClientFactory)mockMtlsHttpClientFactory).DidNotReceive().CreateClient(Arg.Any()); + } + + [Fact] + public async Task CallApiInternalAsync_WithAuthorizationHeaderBoundProviderButWithoutBindingCertificate_UsesRegularHttpClientFactory() + { + // Arrange + var mockBoundProvider = Substitute.For(); + var mockMtlsHttpClientFactory = Substitute.For(); + + var mockHandler = new MockHttpMessageHandler() + { + ExpectedMethod = HttpMethod.Get, + ResponseMessage = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("{\"result\": \"success\"}") + } + }; + var mockRegularHttpClient = new HttpClient(mockHandler); + + var downstreamApi = new DownstreamApi( + mockBoundProvider, + _namedDownstreamApiOptions, + mockMtlsHttpClientFactory, + _logger, + (IMsalHttpClientFactory)mockMtlsHttpClientFactory); + + var options = new DownstreamApiOptions + { + BaseUrl = "https://api.example.com", + Scopes = new[] { "https://api.example.com/.default" }, + HttpMethod = "GET", + ProtocolScheme = "MTLS_POP" + }; + + var authHeaderInfo = new AuthorizationHeaderInformation + { + AuthorizationHeaderValue = "MTLS_POP test-token", + BindingCertificate = null // No binding certificate + }; + + var mockResult = new OperationResult(authHeaderInfo); + + ((IBoundAuthorizationHeaderProvider)mockBoundProvider) + .CreateBoundAuthorizationHeaderAsync( + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(mockResult); + + // Setup regular HTTP client factory to return our mocked HttpClient + ((IHttpClientFactory)mockMtlsHttpClientFactory) + .CreateClient(Arg.Any()) + .Returns(mockRegularHttpClient); + + // Act + await downstreamApi.CallApiInternalAsync(null, options, false, null, null, CancellationToken.None); + + // Assert - Verify regular HTTP client factory was used + ((IHttpClientFactory)mockMtlsHttpClientFactory).Received(1).CreateClient(Arg.Any()); + + // Verify mTLS HTTP client factory was NOT used + ((IMsalMtlsHttpClientFactory)mockMtlsHttpClientFactory).DidNotReceive().GetHttpClient(Arg.Any()); + + // Note: HttpClient is disposed by DownstreamApi, no manual disposal needed + } + + [Fact] + public async Task CallApiInternalAsync_WithAuthorizationHeaderBoundProviderFailure_ThrowsException() + { + // Arrange + var mockBoundProvider = Substitute.For(); + var mockHttpClientFactory = Substitute.For(); + + var downstreamApi = new DownstreamApi( + mockBoundProvider, + _namedDownstreamApiOptions, + mockHttpClientFactory, + _logger); + + var options = new DownstreamApiOptions + { + BaseUrl = "https://api.example.com", + Scopes = new[] { "https://api.example.com/.default" }, + ProtocolScheme = "MTLS_POP" + }; + + // Mock authentication failure + var mockResult = new OperationResult( + new AuthorizationHeaderError("token_acquisition_failed", "Failed to acquire token")); + + ((IBoundAuthorizationHeaderProvider)mockBoundProvider) + .CreateBoundAuthorizationHeaderAsync( + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(mockResult); + + // Act & Assert + var exception = await Assert.ThrowsAnyAsync(() => + downstreamApi.CallApiInternalAsync(null, options, false, null, null, CancellationToken.None)); + + Assert.Equal("Cannot acquire bound authorization header.", exception.Message); + } + + private static X509Certificate2 CreateTestCertificate() + { + // Create a simple test certificate for mocking purposes + // We don't need a real certificate with private key for HTTP client factory testing + var bytes = Convert.FromBase64String(TestConstants.CertificateX5c); + +#if NET9_0_OR_GREATER + // Use the new X509CertificateLoader for .NET 9.0+ + return X509CertificateLoader.LoadCertificate(bytes); +#else + // Use the legacy constructor for older frameworks +#pragma warning disable SYSLIB0057 // Type or member is obsolete + return new X509Certificate2(bytes); +#pragma warning restore SYSLIB0057 // Type or member is obsolete +#endif + } } public class Person diff --git a/tests/Microsoft.Identity.Web.Test/DownstreamWebApiSupport/ExtraParametersTests.cs b/tests/Microsoft.Identity.Web.Test/DownstreamWebApiSupport/ExtraParametersTests.cs index 9714e3ea0..e63810920 100644 --- a/tests/Microsoft.Identity.Web.Test/DownstreamWebApiSupport/ExtraParametersTests.cs +++ b/tests/Microsoft.Identity.Web.Test/DownstreamWebApiSupport/ExtraParametersTests.cs @@ -12,6 +12,7 @@ using Microsoft.Extensions.Options; using Microsoft.Identity.Abstractions; using Microsoft.Identity.Web.Test.Resource; +using NSubstitute; using Xunit; namespace Microsoft.Identity.Web.Tests diff --git a/tests/Microsoft.Identity.Web.Test/FederatedIdentityCaeTests.cs b/tests/Microsoft.Identity.Web.Test/FederatedIdentityCaeTests.cs new file mode 100644 index 000000000..80d930a81 --- /dev/null +++ b/tests/Microsoft.Identity.Web.Test/FederatedIdentityCaeTests.cs @@ -0,0 +1,273 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Client; +using Microsoft.Identity.Web; +using Microsoft.Identity.Web.Test; +using Microsoft.Identity.Web.Test.Common; +using Microsoft.Identity.Web.Test.Common.Mocks; +using Microsoft.Identity.Web.TestOnly; +using Xunit; + +namespace Microsoft.Identity.Web.Tests.Certificateless +{ + [Collection(nameof(TokenAcquirerFactorySingletonProtection))] + public class FederatedIdentityCaeTests + { + private const string Scope = "https://graph.microsoft.com/.default"; + private const string CaeClaims = @"{""access_token"":{""xms_cc"":{""values"":[""claims1""]}}}"; + private const string UamiClientId = "04ca4d6a-c720-4ba1-aa06-f6634b73fe7a"; + + [Fact(Skip = "See https://github.com/AzureAD/microsoft-identity-web/issues/3669")] + public async Task ManagedIdentityWithFic_WithClaims_BypassesCache() + { + TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest(); + var factory = TokenAcquirerFactory.GetDefaultInstance(); + + // Configure FIC via Managed Identity–signed assertion + factory.Services.Configure(opts => + { + opts.Instance = "https://login.microsoftonline.com/"; + opts.TenantId = "11111111-1111-1111-1111-111111111111"; + opts.ClientId = "00000000-0000-0000-0000-000000000000"; + + // Make this a CP1-capable app + opts.ClientCapabilities = new[] { "cp1" }; + opts.ClientCredentials = new[] + { + new CredentialDescription + { + SourceType = CredentialSource.SignedAssertionFromManagedIdentity, + ManagedIdentityClientId = UamiClientId + } + }; + }); + + // Mock IMDS (for the MI assertion) + var mockMiHttp = new MockHttpClientFactory(); + mockMiHttp.AddMockHandler(MockHttpCreator.CreateMsiTokenHandler("mi-assertion-token")); + + var miTestFactory = new TestManagedIdentityHttpFactory(mockMiHttp); + ManagedIdentityClientAssertionTestHook.HttpClientFactoryForTests = miTestFactory.Create(); + factory.Services.AddSingleton(_ => miTestFactory); + + // Mock AAD token responses for client credentials + var mockMsalHttp = new MockHttpClientFactory(); + + // 1st HTTP call to AAD -> token1 + var firstTokenHandler = mockMsalHttp.AddMockHandler( + MockHttpCreator.CreateClientCredentialTokenHandler("token1")); + + // 2nd HTTP call to AAD -> token2 + var secondTokenHandler = mockMsalHttp.AddMockHandler( + MockHttpCreator.CreateClientCredentialTokenHandler("token2")); + + factory.Services.AddSingleton(_ => mockMsalHttp); + + var acquirer = factory.Build().GetRequiredService(); + + // ---------- 1) First call – no custom claims, must hit IdP ---------- + var r1 = await acquirer.GetAuthenticationResultForAppAsync( + Scope, + tokenAcquisitionOptions: new TokenAcquisitionOptions()); + + Assert.Equal("token1", r1.AccessToken); + Assert.Equal(TokenSource.IdentityProvider, r1.AuthenticationResultMetadata.TokenSource); + + // First HTTP request already contains CP1 in xms_cc because of ClientCapabilities + Assert.True(firstTokenHandler.ActualRequestPostData.TryGetValue("claims", out var firstClaimsJson)); + using (var doc = JsonDocument.Parse(firstClaimsJson)) + { + var values = doc.RootElement + .GetProperty("access_token") + .GetProperty("xms_cc") + .GetProperty("values"); + + bool hasCp1 = false; + bool hasClaims1 = false; + + foreach (var v in values.EnumerateArray()) + { + var s = v.GetString(); + if (s == "cp1") hasCp1 = true; + if (s == "claims1") hasClaims1 = true; + } + + Assert.True(hasCp1); // capability propagated + Assert.False(hasClaims1); // custom CAE claim NOT present yet + } + + // ---------- 2) Second call – still no claims, should come from CACHE ---------- + var r2 = await acquirer.GetAuthenticationResultForAppAsync( + Scope, + tokenAcquisitionOptions: new TokenAcquisitionOptions()); + + // Same token as r1 and came from cache (no extra HTTP call to AAD) + Assert.Equal("token1", r2.AccessToken); + Assert.Equal(TokenSource.Cache, r2.AuthenticationResultMetadata.TokenSource); + + // ---------- 3) Third call – WITH claims, must bypass cache and hit IdP ---------- + var r3 = await acquirer.GetAuthenticationResultForAppAsync( + Scope, + tokenAcquisitionOptions: new TokenAcquisitionOptions { Claims = CaeClaims }); + + // New token & explicitly from IdentityProvider + Assert.Equal("token2", r3.AccessToken); + Assert.Equal(TokenSource.IdentityProvider, r3.AuthenticationResultMetadata.TokenSource); + + // And the actual HTTP POST for that second AAD call contained the merged claims (cp1 + mark1) + Assert.True(secondTokenHandler.ActualRequestPostData.TryGetValue("claims", out var secondClaimsJson)); + + using (var doc = JsonDocument.Parse(secondClaimsJson)) + { + var values = doc.RootElement + .GetProperty("access_token") + .GetProperty("xms_cc") + .GetProperty("values"); + + bool hasCp1 = false; + bool hasClaims1 = false; + + foreach (var v in values.EnumerateArray()) + { + var s = v.GetString(); + if (s == "cp1") hasCp1 = true; + if (s == "claims1") hasClaims1 = true; + } + + Assert.True(hasCp1); // capability kept + Assert.True(hasClaims1); // custom CAE claim merged in + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Fic_CustomSignedAssertion_ClaimsAndCapabilities_AreSent_OnSecondRequest(bool withFmiPath) + { + using var httpFactoryForTest = new MockHttpClientFactory(); + // First request (credential exchange) + var credentialRequestHttpHandler = httpFactoryForTest.AddMockHandler( + MockHttpCreator.CreateClientCredentialTokenHandler("token-exchange-1")); + // Second request (actual token acquisition) + var tokenRequestHttpHandler = httpFactoryForTest.AddMockHandler( + MockHttpCreator.CreateClientCredentialTokenHandler("final-access-token")); + + TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest(); + var tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance(); + tokenAcquirerFactory.Services.AddOidcFic(); + tokenAcquirerFactory.Services.AddSingleton(httpFactoryForTest); + + // Source app (provides assertion) + tokenAcquirerFactory.Services.Configure("AzureAd2", options => + { + options.Instance = "https://login.microsoftonline.us/"; + options.TenantId = "t1"; + options.ClientId = "c1"; + options.ClientCredentials = new[] + { + new CredentialDescription + { + SourceType = CredentialSource.ClientSecret, + ClientSecret = TestConstants.ClientSecret + } + }; + }); + + // Target app (uses custom signed assertion and carries cp1) + var customAssertionProvidedData = new Dictionary + { + ["ConfigurationSection"] = "AzureAd2" + }; + if (withFmiPath) + { + customAssertionProvidedData["RequiresSignedAssertionFmiPath"] = true; + } + + tokenAcquirerFactory.Services.Configure(options => + { + options.Instance = "https://login.microsoftonline.com/"; + options.TenantId = "t2"; + options.ClientId = "c2"; + options.ClientCapabilities = new[] { "cp1" }; + options.ClientCredentials = new[] + { + new CredentialDescription + { + SourceType = CredentialSource.CustomSignedAssertion, + CustomSignedAssertionProviderName = "OidcIdpSignedAssertion", + CustomSignedAssertionProviderData = customAssertionProvidedData + } + }; + }); + + var serviceProvider = tokenAcquirerFactory.Build(); + var authorizationHeaderProvider = serviceProvider.GetRequiredService(); + + // Second call will carry claims (with cp1 in xms_cc) + var claimsPayload = "{\"access_token\":{\"xms_cc\":{\"values\":[\"cp1\"]}}}"; + + var header = await authorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync( + TestConstants.s_scopeForApp, + new AuthorizationHeaderProviderOptions + { + AcquireTokenOptions = new AcquireTokenOptions + { + Claims = claimsPayload, + ExtraParameters = withFmiPath + ? new Dictionary + { + [Constants.FmiPathForClientAssertion] = "myFmiPathForSignedAssertion" + } + : null + } + }); + + // Assert endpoints, scopes, client IDs + Assert.Equal("api://AzureADTokenExchange/.default", credentialRequestHttpHandler.ActualRequestPostData["scope"]); + Assert.Equal(TestConstants.s_scopeForApp, tokenRequestHttpHandler.ActualRequestPostData["scope"]); + Assert.Equal("c1", credentialRequestHttpHandler.ActualRequestPostData["client_id"]); + Assert.Equal("https://login.microsoftonline.us/t1/oauth2/v2.0/token", + credentialRequestHttpHandler.ActualRequestMessage?.RequestUri?.AbsoluteUri); + Assert.Equal("c2", tokenRequestHttpHandler.ActualRequestPostData["client_id"]); + Assert.Equal("https://login.microsoftonline.com/t2/oauth2/v2.0/token", + tokenRequestHttpHandler.ActualRequestMessage?.RequestUri?.AbsoluteUri); + + if (withFmiPath) + { + Assert.Equal("myFmiPathForSignedAssertion", credentialRequestHttpHandler.ActualRequestPostData["fmi_path"]); + } + + // Claims: absent on first request, present on second with cp1 + Assert.False(credentialRequestHttpHandler.ActualRequestPostData.ContainsKey("claims")); + Assert.True(tokenRequestHttpHandler.ActualRequestPostData.ContainsKey("claims")); + + var claimsJson = tokenRequestHttpHandler.ActualRequestPostData["claims"]; + using var doc = JsonDocument.Parse(claimsJson); + var cp = doc.RootElement + .GetProperty("access_token") + .GetProperty("xms_cc") + .GetProperty("values")[0] + .GetString(); + Assert.Equal("cp1", cp); + + // First token is reused as client_assertion on second request + string accessTokenFromRequest1; + using (var document = JsonDocument.Parse(credentialRequestHttpHandler.ResponseString)) + { + accessTokenFromRequest1 = document.RootElement.GetProperty("access_token").GetString()!; + } + Assert.Equal(accessTokenFromRequest1, tokenRequestHttpHandler.ActualRequestPostData["client_assertion"]); + + // Bearer header returned + Assert.StartsWith("Bearer", header, StringComparison.Ordinal); + } + } +} diff --git a/tests/Microsoft.Identity.Web.Test/FmiTests.cs b/tests/Microsoft.Identity.Web.Test/FmiTests.cs index da017010f..2563365d1 100644 --- a/tests/Microsoft.Identity.Web.Test/FmiTests.cs +++ b/tests/Microsoft.Identity.Web.Test/FmiTests.cs @@ -49,7 +49,7 @@ private TokenAcquirerFactory InitTokenAcquirerFactoryForFmi() tokenAcquirerFactory.Services.Configure(options => { options.Instance = "https://login.microsoftonline.com/"; - options.TenantId = "f645ad92-e38d-4d1a-b510-d1b09a74a8ca"; + options.TenantId = "10c419d4-4a50-45b2-aa4e-919fb84df24f"; options.ClientId = "urn:microsoft:identity:fmi"; options.ExtraQueryParameters = new Dictionary { diff --git a/tests/Microsoft.Identity.Web.Test/ManagedIdentityCaeTests.cs b/tests/Microsoft.Identity.Web.Test/ManagedIdentityCaeTests.cs index d230814d1..0acbc7365 100644 --- a/tests/Microsoft.Identity.Web.Test/ManagedIdentityCaeTests.cs +++ b/tests/Microsoft.Identity.Web.Test/ManagedIdentityCaeTests.cs @@ -38,7 +38,7 @@ public class ManagedIdentityTests private sealed record VaultSecret(string Value); - [Fact] + [Fact(Skip = "See https://github.com/AzureAD/microsoft-identity-web/issues/3669")] public async Task ManagedIdentity_ReturnsBearerHeader() { TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest(); @@ -70,7 +70,7 @@ public async Task ManagedIdentity_ReturnsBearerHeader() Assert.Equal($"Bearer {MockToken}", header); } - [Fact] + [Fact(Skip = "See https://github.com/AzureAD/microsoft-identity-web/issues/3669")] public async Task ManagedIdentity_WithClaims_HeaderBypassesCache() { // Arrange @@ -120,7 +120,7 @@ public async Task ManagedIdentity_WithClaims_HeaderBypassesCache() Assert.Equal("Bearer token2", header2); } - [Fact] + [Fact(Skip = "See https://github.com/AzureAD/microsoft-identity-web/issues/3669")] public async Task UserAssigned_MI_Caching_and_Claims() { // Arrange @@ -172,7 +172,7 @@ public async Task UserAssigned_MI_Caching_and_Claims() Assert.Equal("token-3", r4.AccessToken); } - [Fact] + [Fact(Skip = "See https://github.com/AzureAD/microsoft-identity-web/issues/3669")] public async Task SystemAssigned_MSI_Forwards_ClientCapabilities_InQuery() { // Arrange diff --git a/tests/Microsoft.Identity.Web.Test/MergedOptionsAuthorityConflictTests.cs b/tests/Microsoft.Identity.Web.Test/MergedOptionsAuthorityConflictTests.cs new file mode 100644 index 000000000..c9baed5db --- /dev/null +++ b/tests/Microsoft.Identity.Web.Test/MergedOptionsAuthorityConflictTests.cs @@ -0,0 +1,218 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Xunit; + +namespace Microsoft.Identity.Web.Test +{ + public class MergedOptionsAuthorityConflictTests + { + private readonly TestLogger _testLogger; + + public MergedOptionsAuthorityConflictTests() + { + _testLogger = new TestLogger(); + } + + [Fact] + public void ParseAuthorityIfNecessary_AuthorityAndInstance_LogsWarning() + { + // Arrange + var mergedOptions = new MergedOptions + { + Authority = "https://login.microsoftonline.com/common", + Instance = "https://login.microsoftonline.com/", + Logger = _testLogger + }; + + // Act + MergedOptions.ParseAuthorityIfNecessary(mergedOptions, _testLogger); + + // Assert + Assert.Single(_testLogger.LogMessages); + Assert.Contains("Authority", _testLogger.LogMessages[0], StringComparison.OrdinalIgnoreCase); + Assert.Contains("ignored", _testLogger.LogMessages[0], StringComparison.OrdinalIgnoreCase); + Assert.Equal(LogLevel.Warning, _testLogger.LogLevel); + } + + [Fact] + public void ParseAuthorityIfNecessary_AuthorityAndTenantId_LogsWarning() + { + // Arrange + var mergedOptions = new MergedOptions + { + Authority = "https://login.microsoftonline.com/common", + TenantId = "organizations", + Logger = _testLogger + }; + + // Act + MergedOptions.ParseAuthorityIfNecessary(mergedOptions, _testLogger); + + // Assert + Assert.Single(_testLogger.LogMessages); + Assert.Contains("Authority", _testLogger.LogMessages[0], StringComparison.OrdinalIgnoreCase); + Assert.Contains("ignored", _testLogger.LogMessages[0], StringComparison.OrdinalIgnoreCase); + Assert.Equal(LogLevel.Warning, _testLogger.LogLevel); + } + + [Fact] + public void ParseAuthorityIfNecessary_AuthorityAndInstanceAndTenantId_LogsWarning() + { + // Arrange + var mergedOptions = new MergedOptions + { + Authority = "https://login.microsoftonline.com/common", + Instance = "https://login.microsoftonline.com/", + TenantId = "organizations", + Logger = _testLogger + }; + + // Act + MergedOptions.ParseAuthorityIfNecessary(mergedOptions, _testLogger); + + // Assert + Assert.Single(_testLogger.LogMessages); + Assert.Contains("Authority", _testLogger.LogMessages[0], StringComparison.OrdinalIgnoreCase); + Assert.Contains("ignored", _testLogger.LogMessages[0], StringComparison.OrdinalIgnoreCase); + Assert.Equal(LogLevel.Warning, _testLogger.LogLevel); + } + + [Fact] + public void ParseAuthorityIfNecessary_AuthorityOnly_NoWarning() + { + // Arrange + var mergedOptions = new MergedOptions + { + Authority = "https://login.microsoftonline.com/common", + Logger = _testLogger + }; + + // Act + MergedOptions.ParseAuthorityIfNecessary(mergedOptions, _testLogger); + + // Assert - No warning should be logged, authority should be parsed + Assert.Empty(_testLogger.LogMessages); + Assert.Equal("https://login.microsoftonline.com", mergedOptions.Instance); + Assert.Equal("common", mergedOptions.TenantId); + } + + [Fact] + public void ParseAuthorityIfNecessary_InstanceAndTenantIdOnly_NoWarning() + { + // Arrange + var mergedOptions = new MergedOptions + { + Instance = "https://login.microsoftonline.com/", + TenantId = "organizations", + Logger = _testLogger + }; + + // Act + MergedOptions.ParseAuthorityIfNecessary(mergedOptions, _testLogger); + + // Assert - No warning should be logged + Assert.Empty(_testLogger.LogMessages); + Assert.Equal("https://login.microsoftonline.com/", mergedOptions.Instance); + Assert.Equal("organizations", mergedOptions.TenantId); + } + + [Fact] + public void ParseAuthorityIfNecessary_B2CAuthorityAndInstance_LogsWarning() + { + // Arrange - B2C scenario + var mergedOptions = new MergedOptions + { + Authority = "https://fabrikamb2c.b2clogin.com/fabrikamb2c.onmicrosoft.com/b2c_1_susi", + Instance = "https://fabrikamb2c.b2clogin.com/", + Logger = _testLogger + }; + + // Act + MergedOptions.ParseAuthorityIfNecessary(mergedOptions, _testLogger); + + // Assert + Assert.Single(_testLogger.LogMessages); + Assert.Contains("Authority", _testLogger.LogMessages[0], StringComparison.OrdinalIgnoreCase); + Assert.Contains("ignored", _testLogger.LogMessages[0], StringComparison.OrdinalIgnoreCase); + Assert.Equal(LogLevel.Warning, _testLogger.LogLevel); + } + + [Fact] + public void ParseAuthorityIfNecessary_CiamAuthorityAndInstance_LogsWarning() + { + // Arrange - CIAM scenario + var mergedOptions = new MergedOptions + { + Authority = "https://contoso.ciamlogin.com/contoso.onmicrosoft.com", + Instance = "https://contoso.ciamlogin.com/", + Logger = _testLogger + }; + + // Act + MergedOptions.ParseAuthorityIfNecessary(mergedOptions, _testLogger); + + // Assert + Assert.Single(_testLogger.LogMessages); + Assert.Contains("Authority", _testLogger.LogMessages[0], StringComparison.OrdinalIgnoreCase); + Assert.Contains("ignored", _testLogger.LogMessages[0], StringComparison.OrdinalIgnoreCase); + Assert.Equal(LogLevel.Warning, _testLogger.LogLevel); + } + + [Fact] + public void ParseAuthorityIfNecessary_CiamPreservedAuthorityWithInstance_LogsWarning() + { + // Arrange - CIAM with PreserveAuthority flag + var mergedOptions = new MergedOptions + { + Authority = "https://custom.contoso.com/contoso.onmicrosoft.com", + Instance = "https://custom.contoso.com/", + PreserveAuthority = true, + Logger = _testLogger + }; + + // Act + MergedOptions.ParseAuthorityIfNecessary(mergedOptions, _testLogger); + + // Assert - Warning should still be logged even with PreserveAuthority + Assert.Single(_testLogger.LogMessages); + Assert.Contains("Authority", _testLogger.LogMessages[0], StringComparison.OrdinalIgnoreCase); + Assert.Contains("ignored", _testLogger.LogMessages[0], StringComparison.OrdinalIgnoreCase); + Assert.Equal(LogLevel.Warning, _testLogger.LogLevel); + } + + [Fact] + public void ParseAuthorityIfNecessary_NoLogger_NoException() + { + // Arrange + var mergedOptions = new MergedOptions + { + Authority = "https://login.microsoftonline.com/common", + Instance = "https://login.microsoftonline.com/", + }; + + // Act & Assert - Should not throw when logger is null + MergedOptions.ParseAuthorityIfNecessary(mergedOptions, logger: null); + } + + // Test helper class to capture log messages + private class TestLogger : ILogger + { + public System.Collections.Generic.List LogMessages { get; } = new System.Collections.Generic.List(); + public LogLevel LogLevel { get; private set; } + + public IDisposable? BeginScope(TState state) where TState : notnull => null; + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + LogLevel = logLevel; + LogMessages.Add(formatter(state, exception)); + } + } + } +} diff --git a/tests/Microsoft.Identity.Web.Test/MergedOptionsAuthorityParsingTests.cs b/tests/Microsoft.Identity.Web.Test/MergedOptionsAuthorityParsingTests.cs new file mode 100644 index 000000000..c24f6e808 --- /dev/null +++ b/tests/Microsoft.Identity.Web.Test/MergedOptionsAuthorityParsingTests.cs @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Identity.Web; +using Xunit; + +namespace Microsoft.Identity.Web.Test +{ + public class MergedOptionsAuthorityParsingTests + { + [Fact] + public void ParseAuthority_AadAuthorityOnly_SetsInstanceAndTenant() + { + // Arrange + var mergedOptions = new MergedOptions + { + Authority = "https://login.microsoftonline.com/id4slab1.onmicrosoft.com/v2.0" + }; + + // Act + MergedOptions.ParseAuthorityIfNecessary(mergedOptions); + + // Assert + Assert.Equal("https://login.microsoftonline.com", mergedOptions.Instance); + Assert.Equal("id4slab1.onmicrosoft.com", mergedOptions.TenantId); + } + + [Fact] + public void PrepareAuthorityInstance_ForAadAuthorityOnly_ComputesPreparedInstance() + { + // Arrange + var mergedOptions = new MergedOptions + { + Authority = "https://login.microsoftonline.com/id4slab1.onmicrosoft.com/v2.0" + }; + + // Act + mergedOptions.PrepareAuthorityInstanceForMsal(); + + // Assert + Assert.Equal("https://login.microsoftonline.com/", mergedOptions.PreparedInstance); + } + + [Fact] + public void PrepareAuthorityInstance_ForB2C_RemovesTfpSegment() + { + // Arrange + var mergedOptions = new MergedOptions + { + Authority = "https://fabrikamb2c.b2clogin.com/tfp/fabrikamb2c.onmicrosoft.com/B2C_1_susi/v2.0", + SignUpSignInPolicyId = "B2C_1_susi" + }; + + // Act - Parse the authority first + MergedOptions.ParseAuthorityIfNecessary(mergedOptions); + mergedOptions.PrepareAuthorityInstanceForMsal(); + + // Assert - Instance should be parsed to the domain part + Assert.Equal("https://fabrikamb2c.b2clogin.com", mergedOptions.Instance); + // PreparedInstance should have /tfp/ removed if present in Instance + Assert.Equal("https://fabrikamb2c.b2clogin.com/", mergedOptions.PreparedInstance); + } + + [Fact] + public void PreserveAuthority_CiamAuthority_DoesNotSetTenant() + { + // Arrange - CIAM authority with a path to parse + var mergedOptions = new MergedOptions + { + Authority = "https://MSIDLABCIAM6.ciamlogin.com/tenant.onmicrosoft.com", + PreserveAuthority = true + }; + + // Act + MergedOptions.ParseAuthorityIfNecessary(mergedOptions); + + // Assert - When PreserveAuthority is true, Instance should be the full Authority + Assert.Equal("https://MSIDLABCIAM6.ciamlogin.com/tenant.onmicrosoft.com", mergedOptions.Instance); + // TenantId should remain null when PreserveAuthority is true + Assert.Null(mergedOptions.TenantId); + } + + [Fact] + public void ParseAuthority_WithoutV2Suffix_SetsInstanceAndTenant() + { + // Arrange + var mergedOptions = new MergedOptions + { + Authority = "https://login.microsoftonline.com/common" + }; + + // Act + MergedOptions.ParseAuthorityIfNecessary(mergedOptions); + + // Assert + Assert.Equal("https://login.microsoftonline.com", mergedOptions.Instance); + Assert.Equal("common", mergedOptions.TenantId); + } + + [Fact] + public void ParseAuthority_InstanceAndTenantAlreadySet_DoesNotParse() + { + // Arrange + var mergedOptions = new MergedOptions + { + Authority = "https://login.microsoftonline.com/tenant1/v2.0", + Instance = "https://login.microsoftonline.com/", + TenantId = "tenant2" + }; + + // Act + MergedOptions.ParseAuthorityIfNecessary(mergedOptions); + + // Assert - Should not modify existing Instance and TenantId + Assert.Equal("https://login.microsoftonline.com/", mergedOptions.Instance); + Assert.Equal("tenant2", mergedOptions.TenantId); + } + + [Fact] + public void ParseAuthority_B2CAuthorityWithPolicy_SetsInstanceAndTenant() + { + // Arrange + var mergedOptions = new MergedOptions + { + Authority = "https://fabrikamb2c.b2clogin.com/fabrikamb2c.onmicrosoft.com/B2C_1_susi/v2.0", + SignUpSignInPolicyId = "B2C_1_susi" + }; + + // Act + MergedOptions.ParseAuthorityIfNecessary(mergedOptions); + + // Assert + Assert.Equal("https://fabrikamb2c.b2clogin.com", mergedOptions.Instance); + Assert.Equal("fabrikamb2c.onmicrosoft.com", mergedOptions.TenantId); + } + } +} diff --git a/tests/Microsoft.Identity.Web.Test/MergedOptionsExtendedAuthorityTests.cs b/tests/Microsoft.Identity.Web.Test/MergedOptionsExtendedAuthorityTests.cs new file mode 100644 index 000000000..afdf8fb9f --- /dev/null +++ b/tests/Microsoft.Identity.Web.Test/MergedOptionsExtendedAuthorityTests.cs @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Identity.Web; +using Microsoft.Identity.Web.Test.Common; +using Xunit; + +namespace Microsoft.Identity.Web.Test +{ + /// + /// Additional edge case tests for MergedOptions authority parsing logic. + /// Issue #3610: Tests for edge cases not covered by MergedOptionsAuthorityParsingTests. + /// + public class MergedOptionsExtendedAuthorityTests + { + [Theory] + [InlineData("common")] + [InlineData("organizations")] + [InlineData("consumers")] + public void ParseAuthority_SpecialTenantValues_ParsesCorrectly(string tenantValue) + { + // Issue #3610: Special AAD tenant values (common, organizations, consumers) with Theory + // Arrange + var mergedOptions = new MergedOptions + { + Authority = $"https://login.microsoftonline.com/{tenantValue}/v2.0" + }; + + // Act + MergedOptions.ParseAuthorityIfNecessary(mergedOptions); + + // Assert + Assert.Equal("https://login.microsoftonline.com", mergedOptions.Instance); + Assert.Equal(tenantValue, mergedOptions.TenantId); + } + + [Fact] + public void ParseAuthority_SchemeLessAuthority_ParsesBaseAndTenant() + { + // Issue #3610: Authority without scheme prefix should parse correctly + // Arrange - authority without https:// prefix + var mergedOptions = new MergedOptions + { + Authority = "login.microsoftonline.com/common/v2.0" + }; + + // Act + MergedOptions.ParseAuthorityIfNecessary(mergedOptions); + + // Assert - Should still parse the tenant correctly + Assert.Equal("login.microsoftonline.com", mergedOptions.Instance); + Assert.Equal("common", mergedOptions.TenantId); + } + + [Fact] + public void ParseAuthority_WithTrailingSlash_Normalizes() + { + // Issue #3610: Trailing slashes should be handled correctly + // Arrange + var mergedOptions = new MergedOptions + { + Authority = "https://login.microsoftonline.com/common/v2.0/" + }; + + // Act + MergedOptions.ParseAuthorityIfNecessary(mergedOptions); + + // Assert - Trailing slash should be trimmed during parsing + Assert.Equal("https://login.microsoftonline.com", mergedOptions.Instance); + Assert.Equal("common", mergedOptions.TenantId); + } + + [Fact] + public void ParseAuthority_WithMultipleSlashes_IgnoresExtra() + { + // Issue #3610: Multiple consecutive slashes should not break parsing + // Arrange + var mergedOptions = new MergedOptions + { + Authority = "https://login.microsoftonline.com//common//v2.0" + }; + + // Act + MergedOptions.ParseAuthorityIfNecessary(mergedOptions); + + // Assert - Should handle multiple slashes gracefully + Assert.Equal("https://login.microsoftonline.com", mergedOptions.Instance); + // TenantId might include the slash or might be empty, depending on parsing logic + // The implementation handles this by finding the first slash after the scheme + Assert.NotNull(mergedOptions.Instance); + } + + [Fact] + public void ParseAuthority_AuthorityWithQueryParams_DoesNotBreak() + { + // Issue #3610: Authority with query parameters should not break parsing + // Arrange + var mergedOptions = new MergedOptions + { + Authority = "https://login.microsoftonline.com/common/v2.0?dc=ESTS-PUB-WUS2-AZ1-FD000-TEST1" + }; + + // Act + MergedOptions.ParseAuthorityIfNecessary(mergedOptions); + + // Assert - Should parse the base authority and tenant correctly + // Query parameters may be included or excluded from TenantId depending on implementation + Assert.Equal("https://login.microsoftonline.com", mergedOptions.Instance); + Assert.NotNull(mergedOptions.TenantId); + // The tenant should at least start with "common" + Assert.StartsWith("common", mergedOptions.TenantId, System.StringComparison.Ordinal); + } + } +} diff --git a/tests/Microsoft.Identity.Web.Test/MergedOptionsTests.cs b/tests/Microsoft.Identity.Web.Test/MergedOptionsTests.cs index d207c8138..732908df0 100644 --- a/tests/Microsoft.Identity.Web.Test/MergedOptionsTests.cs +++ b/tests/Microsoft.Identity.Web.Test/MergedOptionsTests.cs @@ -292,5 +292,26 @@ public void PrepareAuthorityInstanceForMsal_DoesNotParseAuthority_WhenInstanceAn Assert.Equal("organizations", options.TenantId); // TenantId remains unchanged Assert.Equal("https://login.microsoftonline.us/", options.PreparedInstance); // PreparedInstance based on original Instance } + + [Fact] + public void PrepareAuthorityInstanceForMsal_LeavesNullPreparedInstance_WhenNoConfigurationProvided() + { + // This test verifies the scenario from issue #2921 where a misconfigured key + // (e.g., "ManagedIdentity " with trailing space instead of "ManagedIdentity") + // results in null Instance and null Authority, which should leave PreparedInstance as null + + // Arrange + var options = new MergedOptions(); + // Simulating the scenario where configuration keys have typos and don't bind correctly + + // Act + options.PrepareAuthorityInstanceForMsal(); + + // Assert + Assert.Null(options.Instance); + Assert.Null(options.TenantId); + Assert.Null(options.Authority); + Assert.Null(options.PreparedInstance); + } } } diff --git a/tests/Microsoft.Identity.Web.Test/MicrosoftIdentityHttpClientBuilderExtensionsTests.cs b/tests/Microsoft.Identity.Web.Test/MicrosoftIdentityHttpClientBuilderExtensionsTests.cs new file mode 100644 index 000000000..66e71db2e --- /dev/null +++ b/tests/Microsoft.Identity.Web.Test/MicrosoftIdentityHttpClientBuilderExtensionsTests.cs @@ -0,0 +1,348 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Identity.Abstractions; +using NSubstitute; +using Xunit; + +namespace Microsoft.Identity.Web.Test +{ + public class MicrosoftIdentityHttpClientBuilderExtensionsTests + { + private readonly IAuthorizationHeaderProvider _mockHeaderProvider; + + public MicrosoftIdentityHttpClientBuilderExtensionsTests() + { + _mockHeaderProvider = Substitute.For(); + } + + [Fact] + public void AddMicrosoftIdentityMessageHandler_Parameterless_WithNullBuilder_ThrowsArgumentNullException() + { + // Arrange + IHttpClientBuilder? builder = null; + + // Act & Assert + Assert.Throws(() => builder!.AddMicrosoftIdentityMessageHandler()); + } + + [Fact] + public void AddMicrosoftIdentityMessageHandler_Parameterless_RegistersHandler() + { + // Arrange + var services = new ServiceCollection(); + services.AddSingleton(_mockHeaderProvider); + var builder = services.AddHttpClient("TestClient"); + + // Act + builder.AddMicrosoftIdentityMessageHandler(); + + // Assert + var serviceProvider = services.BuildServiceProvider(); + var httpClientFactory = serviceProvider.GetRequiredService(); + var client = httpClientFactory.CreateClient("TestClient"); + + Assert.NotNull(client); + } + + [Fact] + public void AddMicrosoftIdentityMessageHandler_WithOptions_WithNullBuilder_ThrowsArgumentNullException() + { + // Arrange + IHttpClientBuilder? builder = null; + var options = new MicrosoftIdentityMessageHandlerOptions + { + Scopes = { "test.scope" } + }; + + // Act & Assert + Assert.Throws(() => builder!.AddMicrosoftIdentityMessageHandler(options)); + } + + [Fact] + public void AddMicrosoftIdentityMessageHandler_WithOptions_WithNullOptions_ThrowsArgumentNullException() + { + // Arrange + var services = new ServiceCollection(); + services.AddSingleton(_mockHeaderProvider); + var builder = services.AddHttpClient("TestClient"); + MicrosoftIdentityMessageHandlerOptions? options = null; + + // Act & Assert + Assert.Throws(() => builder.AddMicrosoftIdentityMessageHandler(options!)); + } + + [Fact] + public void AddMicrosoftIdentityMessageHandler_WithOptions_RegistersHandlerWithOptions() + { + // Arrange + var services = new ServiceCollection(); + services.AddSingleton(_mockHeaderProvider); + var builder = services.AddHttpClient("TestClient"); + var options = new MicrosoftIdentityMessageHandlerOptions + { + Scopes = { "https://graph.microsoft.com/.default" } + }; + + // Act + builder.AddMicrosoftIdentityMessageHandler(options); + + // Assert + var serviceProvider = services.BuildServiceProvider(); + var httpClientFactory = serviceProvider.GetRequiredService(); + var client = httpClientFactory.CreateClient("TestClient"); + + Assert.NotNull(client); + } + + [Fact] + public void AddMicrosoftIdentityMessageHandler_WithDelegate_WithNullBuilder_ThrowsArgumentNullException() + { + // Arrange + IHttpClientBuilder? builder = null; + Action configureOptions = options => + { + options.Scopes.Add("test.scope"); + }; + + // Act & Assert + Assert.Throws(() => builder!.AddMicrosoftIdentityMessageHandler(configureOptions)); + } + + [Fact] + public void AddMicrosoftIdentityMessageHandler_WithDelegate_WithNullDelegate_ThrowsArgumentNullException() + { + // Arrange + var services = new ServiceCollection(); + services.AddSingleton(_mockHeaderProvider); + var builder = services.AddHttpClient("TestClient"); + Action? configureOptions = null; + + // Act & Assert + Assert.Throws(() => builder.AddMicrosoftIdentityMessageHandler(configureOptions!)); + } + + [Fact] + public void AddMicrosoftIdentityMessageHandler_WithDelegate_RegistersHandlerWithConfiguredOptions() + { + // Arrange + var services = new ServiceCollection(); + services.AddSingleton(_mockHeaderProvider); + var builder = services.AddHttpClient("TestClient"); + + // Act + builder.AddMicrosoftIdentityMessageHandler(options => + { + options.Scopes.Add("https://api.example.com/.default"); + options.RequestAppToken = true; + }); + + // Assert + var serviceProvider = services.BuildServiceProvider(); + var httpClientFactory = serviceProvider.GetRequiredService(); + var client = httpClientFactory.CreateClient("TestClient"); + + Assert.NotNull(client); + } + + [Fact] + public void AddMicrosoftIdentityMessageHandler_WithConfiguration_WithNullBuilder_ThrowsArgumentNullException() + { + // Arrange + IHttpClientBuilder? builder = null; + var configuration = new ConfigurationBuilder().Build(); + + // Act & Assert + Assert.Throws(() => + builder!.AddMicrosoftIdentityMessageHandler(configuration, "TestSection")); + } + + [Fact] + public void AddMicrosoftIdentityMessageHandler_WithConfiguration_WithNullConfiguration_ThrowsArgumentNullException() + { + // Arrange + var services = new ServiceCollection(); + services.AddSingleton(_mockHeaderProvider); + var builder = services.AddHttpClient("TestClient"); + IConfiguration? configuration = null; + + // Act & Assert + Assert.Throws(() => + builder.AddMicrosoftIdentityMessageHandler(configuration!, "TestSection")); + } + + [Fact] + public void AddMicrosoftIdentityMessageHandler_WithConfiguration_WithNullSectionName_ThrowsArgumentNullException() + { + // Arrange + var services = new ServiceCollection(); + services.AddSingleton(_mockHeaderProvider); + var builder = services.AddHttpClient("TestClient"); + var configuration = new ConfigurationBuilder().Build(); + string? sectionName = null; + + // Act & Assert + Assert.Throws(() => + builder.AddMicrosoftIdentityMessageHandler(configuration, sectionName!)); + } + + [Fact] + public void AddMicrosoftIdentityMessageHandler_WithConfiguration_RegistersHandlerWithBoundOptions() + { + // Arrange + var configurationData = new Dictionary + { + { "DownstreamApi:Scopes:0", "https://graph.microsoft.com/.default" }, + { "DownstreamApi:Scopes:1", "User.Read" } + }; + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(configurationData) + .Build(); + + var services = new ServiceCollection(); + services.AddSingleton(_mockHeaderProvider); + var builder = services.AddHttpClient("TestClient"); + + // Act + builder.AddMicrosoftIdentityMessageHandler( + configuration.GetSection("DownstreamApi"), + "DownstreamApi"); + + // Assert + var serviceProvider = services.BuildServiceProvider(); + var httpClientFactory = serviceProvider.GetRequiredService(); + var client = httpClientFactory.CreateClient("TestClient"); + + Assert.NotNull(client); + } + + [Fact] + public void AddMicrosoftIdentityMessageHandler_ServiceResolution_ResolvesAuthorizationHeaderProvider() + { + // Arrange + var services = new ServiceCollection(); + services.AddSingleton(_mockHeaderProvider); + var builder = services.AddHttpClient("TestClient"); + + // Act + builder.AddMicrosoftIdentityMessageHandler(options => + { + options.Scopes.Add("test.scope"); + }); + + // Assert + var serviceProvider = services.BuildServiceProvider(); + + // Verify that IAuthorizationHeaderProvider can be resolved + var headerProvider = serviceProvider.GetRequiredService(); + Assert.NotNull(headerProvider); + Assert.Same(_mockHeaderProvider, headerProvider); + } + + [Fact] + public void AddMicrosoftIdentityMessageHandler_MultipleHandlers_CanBeChained() + { + // Arrange + var services = new ServiceCollection(); + services.AddSingleton(_mockHeaderProvider); + var builder = services.AddHttpClient("TestClient"); + + // Act - Add multiple handlers to the pipeline + builder + .AddMicrosoftIdentityMessageHandler(options => + { + options.Scopes.Add("test.scope"); + }) + .AddHttpMessageHandler(() => new TestDelegatingHandler()); + + // Assert + var serviceProvider = services.BuildServiceProvider(); + var httpClientFactory = serviceProvider.GetRequiredService(); + var client = httpClientFactory.CreateClient("TestClient"); + + Assert.NotNull(client); + } + + [Fact] + public void AddMicrosoftIdentityMessageHandler_ReturnsBuilder_AllowsChaining() + { + // Arrange + var services = new ServiceCollection(); + services.AddSingleton(_mockHeaderProvider); + var builder = services.AddHttpClient("TestClient"); + + // Act + var result = builder.AddMicrosoftIdentityMessageHandler(); + + // Assert + Assert.NotNull(result); + Assert.Same(builder, result); + } + + [Fact] + public void AddMicrosoftIdentityMessageHandler_WithConfiguration_EmptyConfiguration_RegistersHandlerWithEmptyOptions() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var services = new ServiceCollection(); + services.AddSingleton(_mockHeaderProvider); + var builder = services.AddHttpClient("TestClient"); + + // Act + builder.AddMicrosoftIdentityMessageHandler( + configuration.GetSection("NonExistent"), + "NonExistent"); + + // Assert + var serviceProvider = services.BuildServiceProvider(); + var httpClientFactory = serviceProvider.GetRequiredService(); + var client = httpClientFactory.CreateClient("TestClient"); + + Assert.NotNull(client); + } + + [Fact] + public void AddMicrosoftIdentityMessageHandler_MultipleClients_CanHaveDifferentConfigurations() + { + // Arrange + var services = new ServiceCollection(); + services.AddSingleton(_mockHeaderProvider); + + // Act - Configure multiple clients with different options + services.AddHttpClient("Client1") + .AddMicrosoftIdentityMessageHandler(options => + { + options.Scopes.Add("scope1"); + }); + + services.AddHttpClient("Client2") + .AddMicrosoftIdentityMessageHandler(options => + { + options.Scopes.Add("scope2"); + options.RequestAppToken = true; + }); + + // Assert + var serviceProvider = services.BuildServiceProvider(); + var httpClientFactory = serviceProvider.GetRequiredService(); + + var client1 = httpClientFactory.CreateClient("Client1"); + var client2 = httpClientFactory.CreateClient("Client2"); + + Assert.NotNull(client1); + Assert.NotNull(client2); + } + + // Test helper class + private class TestDelegatingHandler : DelegatingHandler + { + } + } +} diff --git a/tests/Microsoft.Identity.Web.Test/MsalMtlsHttpClientFactoryTests.cs b/tests/Microsoft.Identity.Web.Test/MsalMtlsHttpClientFactoryTests.cs new file mode 100644 index 000000000..aa138aa2d --- /dev/null +++ b/tests/Microsoft.Identity.Web.Test/MsalMtlsHttpClientFactoryTests.cs @@ -0,0 +1,195 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Net.Http; +using System.Security.Cryptography.X509Certificates; +using Microsoft.Identity.Web.Test.Common; +using Xunit; + +namespace Microsoft.Identity.Web.Test +{ + public class MsalMtlsHttpClientFactoryTests : IDisposable + { + private readonly TestHttpClientFactory _httpClientFactory; + private readonly MsalMtlsHttpClientFactory _factory; + private bool _disposed = false; + + public MsalMtlsHttpClientFactoryTests() + { + _httpClientFactory = new TestHttpClientFactory(); + _factory = new MsalMtlsHttpClientFactory(_httpClientFactory); + } + +#if NET462 + [Fact] + public void GetHttpClient_WithCertificateOnUnsupportedPlatform_ShouldThrowNotSupportedException() + { + // Arrange + using var certificate = CreateTestCertificate(); + + // Act & Assert + Assert.Throws(() => _factory.GetHttpClient(certificate)); + } +#else // NET462 + [Fact] + public void Constructor_WithValidHttpClientFactory_ShouldNotThrow() + { + // Arrange & Act + var factory = new MsalMtlsHttpClientFactory(_httpClientFactory); + + // Assert + Assert.NotNull(factory); + } + + [Fact] + public void Constructor_WithNullHttpClientFactory_ShouldAcceptNull() + { + // Arrange & Act + var factory = new MsalMtlsHttpClientFactory(null!); + + // Assert + Assert.NotNull(factory); + } + + [Fact] + public void GetHttpClient_WithoutCertificate_ShouldReturnConfiguredHttpClient() + { + // Arrange & Act + HttpClient actualHttpClient = _factory.GetHttpClient(); + + // Assert + Assert.NotNull(actualHttpClient); + + // Verify telemetry header is present + Assert.True(actualHttpClient.DefaultRequestHeaders.Contains(Constants.TelemetryHeaderKey)); + + var telemetryHeaderValues = actualHttpClient.DefaultRequestHeaders.GetValues(Constants.TelemetryHeaderKey); + Assert.Single(telemetryHeaderValues); + } + + [Fact] + public void GetHttpClient_WithNullCertificate_ShouldReturnConfiguredHttpClient() + { + // Arrange & Act + HttpClient actualHttpClient = _factory.GetHttpClient(null!); + + // Assert + Assert.NotNull(actualHttpClient); + Assert.True(actualHttpClient.DefaultRequestHeaders.Contains(Constants.TelemetryHeaderKey)); + } + + [Fact] + public void GetHttpClient_WithSameCertificate_ShouldReturnCachedClient() + { + // Arrange + using var certificate = CreateTestCertificate(); + + // Act + HttpClient firstClient = _factory.GetHttpClient(certificate); + HttpClient secondClient = _factory.GetHttpClient(certificate); + + // Assert + Assert.Same(firstClient, secondClient); + } + + [Fact] + public void GetHttpClient_WithCertificate_ShouldConfigureProperHeaders() + { + // Arrange + using var certificate = CreateTestCertificate(); + + // Act + HttpClient httpClient = _factory.GetHttpClient(certificate); + + // Assert + // Verify telemetry header + Assert.True(httpClient.DefaultRequestHeaders.Contains(Constants.TelemetryHeaderKey)); + + // Verify max response buffer size + Assert.Equal(1024 * 1024, httpClient.MaxResponseContentBufferSize); + } + + [Fact] + public void GetHttpClient_CreatesClientFromFactory() + { + // Arrange & Act + _factory.GetHttpClient(); + + // Assert + Assert.True(_httpClientFactory.CreateClientCalled); + } + + [Fact] + public void GetHttpClient_MultipleCalls_CallsFactoryEachTime() + { + // Arrange & Act + _factory.GetHttpClient(); + _factory.GetHttpClient(); + + // Assert + Assert.Equal(2, _httpClientFactory.CreateClientCallCount); + } + + private static X509Certificate2 CreateTestCertificate() + { + // Create a simple test certificate for mocking purposes + // We don't need a real certificate with private key for HTTP client factory testing + var bytes = Convert.FromBase64String(TestConstants.CertificateX5c); + +#if NET9_0_OR_GREATER + // Use the new X509CertificateLoader for .NET 9.0+ + return X509CertificateLoader.LoadCertificate(bytes); +#else // NET9_0_OR_GREATER + // Use the legacy constructor for older frameworks +#pragma warning disable SYSLIB0057 // Type or member is obsolete + return new X509Certificate2(bytes); +#pragma warning restore SYSLIB0057 // Type or member is obsolete +#endif // NET9_0_OR_GREATER + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _httpClientFactory?.Dispose(); + } + _disposed = true; + } + } + + /// + /// Simple test HttpClientFactory implementation for testing purposes. + /// + private sealed class TestHttpClientFactory : IHttpClientFactory, IDisposable + { + public bool CreateClientCalled { get; private set; } + public int CreateClientCallCount { get; private set; } + private bool _disposed = false; + + public HttpClient CreateClient(string name) + { + CreateClientCalled = true; + CreateClientCallCount++; + return new HttpClient(); + } + + public void Dispose() + { + if (!_disposed) + { + _disposed = true; + } + } + } +#endif // NET462 + } +} diff --git a/tests/Microsoft.Identity.Web.Test/OidcIdpSignedAssertionProviderTests.cs b/tests/Microsoft.Identity.Web.Test/OidcIdpSignedAssertionProviderTests.cs new file mode 100644 index 000000000..3f708e4ec --- /dev/null +++ b/tests/Microsoft.Identity.Web.Test/OidcIdpSignedAssertionProviderTests.cs @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Identity.Web.OidcFic; +using Xunit; + +namespace Microsoft.Identity.Web.Test +{ + public class OidcIdpSignedAssertionProviderTests + { + [Theory] + [InlineData("https://login.microsoftonline.com/my-tenant-id/oauth2/v2.0/token", "https://login.microsoftonline.com/", "my-tenant-id")] + [InlineData("https://login.microsoftonline.com/contoso.onmicrosoft.com/oauth2/v2.0/token", "https://login.microsoftonline.com", "contoso.onmicrosoft.com")] + [InlineData("https://login.microsoftonline.com/12345678-1234-1234-1234-123456789abc/oauth2/v2.0/token", "https://login.microsoftonline.com/", "12345678-1234-1234-1234-123456789abc")] + public void ExtractTenantFromTokenEndpointIfSameInstance_SameInstance_ReturnsTenant( + string tokenEndpoint, + string configuredInstance, + string expectedTenant) + { + // Act + var result = OidcIdpSignedAssertionProvider.ExtractTenantFromTokenEndpointIfSameInstance( + tokenEndpoint, + configuredInstance); + + // Assert + Assert.Equal(expectedTenant, result); + } + + [Theory] + [InlineData("https://login.microsoftonline.us/my-tenant-id/oauth2/v2.0/token", "https://login.microsoftonline.com/", null)] + [InlineData("https://login.microsoftonline.com/my-tenant-id/oauth2/v2.0/token", "https://login.microsoftonline.us/", null)] + [InlineData("https://login.chinacloudapi.cn/my-tenant-id/oauth2/v2.0/token", "https://login.microsoftonline.com/", null)] + public void ExtractTenantFromTokenEndpointIfSameInstance_DifferentInstance_ReturnsNull( + string tokenEndpoint, + string configuredInstance, + string? expectedResult) + { + // Act + var result = OidcIdpSignedAssertionProvider.ExtractTenantFromTokenEndpointIfSameInstance( + tokenEndpoint, + configuredInstance); + + // Assert + Assert.Equal(expectedResult, result); + } + + [Theory] + [InlineData(null, "https://login.microsoftonline.com/")] + [InlineData("", "https://login.microsoftonline.com/")] + [InlineData("https://login.microsoftonline.com/tenant/oauth2/v2.0/token", null)] + [InlineData("https://login.microsoftonline.com/tenant/oauth2/v2.0/token", "")] + [InlineData(null, null)] + [InlineData("", "")] + public void ExtractTenantFromTokenEndpointIfSameInstance_NullOrEmptyInputs_ReturnsNull( + string? tokenEndpoint, + string? configuredInstance) + { + // Act + var result = OidcIdpSignedAssertionProvider.ExtractTenantFromTokenEndpointIfSameInstance( + tokenEndpoint, + configuredInstance); + + // Assert + Assert.Null(result); + } + + [Theory] + [InlineData("not-a-valid-uri", "https://login.microsoftonline.com/")] + [InlineData("https://login.microsoftonline.com/tenant/oauth2/v2.0/token", "not-a-valid-uri")] + public void ExtractTenantFromTokenEndpointIfSameInstance_InvalidUri_ReturnsNull( + string tokenEndpoint, + string configuredInstance) + { + // Act + var result = OidcIdpSignedAssertionProvider.ExtractTenantFromTokenEndpointIfSameInstance( + tokenEndpoint, + configuredInstance); + + // Assert + Assert.Null(result); + } + + [Theory] + [InlineData("https://login.microsoftonline.com/oauth2/v2.0/token", "https://login.microsoftonline.com/")] + [InlineData("https://login.microsoftonline.com/", "https://login.microsoftonline.com/")] + public void ExtractTenantFromTokenEndpointIfSameInstance_NoTenantInPath_ReturnsNull( + string tokenEndpoint, + string configuredInstance) + { + // Act + var result = OidcIdpSignedAssertionProvider.ExtractTenantFromTokenEndpointIfSameInstance( + tokenEndpoint, + configuredInstance); + + // Assert + Assert.Null(result); + } + + [Fact] + public void ExtractTenantFromTokenEndpointIfSameInstance_ValidatesOAuth2Pattern() + { + // Arrange + // This endpoint has a tenant but no oauth2 segment - should return null + var tokenEndpoint = "https://login.microsoftonline.com/my-tenant/some-other-path"; + var configuredInstance = "https://login.microsoftonline.com/"; + + // Act + var result = OidcIdpSignedAssertionProvider.ExtractTenantFromTokenEndpointIfSameInstance( + tokenEndpoint, + configuredInstance); + + // Assert + Assert.Null(result); + } + } +} diff --git a/tests/Microsoft.Identity.Web.Test/ServiceCollectionExtensionsTests.cs b/tests/Microsoft.Identity.Web.Test/ServiceCollectionExtensionsTests.cs index 5815eb5c7..570f5debe 100644 --- a/tests/Microsoft.Identity.Web.Test/ServiceCollectionExtensionsTests.cs +++ b/tests/Microsoft.Identity.Web.Test/ServiceCollectionExtensionsTests.cs @@ -6,6 +6,8 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.Metrics.Configuration; +using Microsoft.Extensions.Http; using Microsoft.Extensions.Options; using Microsoft.Identity.Abstractions; using Microsoft.Identity.Client; @@ -34,7 +36,8 @@ public void AddTokenAcquisition_Sdk_AddsWithCorrectLifetime() Assert.Equal(typeof(MicrosoftIdentityApplicationOptionsMerger), actual.ImplementationType); Assert.Null(actual.ImplementationInstance); Assert.Null(actual.ImplementationFactory); - }, actual => + }, + actual => { Assert.Equal(ServiceLifetime.Singleton, actual.Lifetime); Assert.Equal(typeof(IPostConfigureOptions), actual.ServiceType); @@ -90,13 +93,13 @@ public void AddTokenAcquisition_Sdk_AddsWithCorrectLifetime() Assert.Null(actual.ImplementationFactory); }, actual => - { - Assert.Equal(ServiceLifetime.Scoped, actual.Lifetime); - Assert.Equal(typeof(ITokenAcquisition), actual.ServiceType); - Assert.Equal(typeof(TokenAcquisitionAspNetCore), actual.ImplementationType); - Assert.Null(actual.ImplementationInstance); - Assert.Null(actual.ImplementationFactory); - }, + { + Assert.Equal(ServiceLifetime.Scoped, actual.Lifetime); + Assert.Equal(typeof(ITokenAcquisition), actual.ServiceType); + Assert.Equal(typeof(TokenAcquisitionAspNetCore), actual.ImplementationType); + Assert.Null(actual.ImplementationInstance); + Assert.Null(actual.ImplementationFactory); + }, actual => { Assert.Equal(ServiceLifetime.Scoped, actual.Lifetime); diff --git a/tests/Microsoft.Identity.Web.Test/TokenAcquirerTests.cs b/tests/Microsoft.Identity.Web.Test/TokenAcquirerTests.cs new file mode 100644 index 000000000..d9a1c776c --- /dev/null +++ b/tests/Microsoft.Identity.Web.Test/TokenAcquirerTests.cs @@ -0,0 +1,291 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Client; +using NSubstitute; +using Xunit; + +namespace Microsoft.Identity.Web.Test +{ + public class TokenAcquirerTests + { + private readonly ITokenAcquisition _tokenAcquisition; + private readonly string _scope = "https://graph.microsoft.com/.default"; + private readonly string _accessToken = "test_access_token"; + private readonly string _tenantId = "test_tenant_id"; + private readonly string _idToken = "test_id_token"; + private readonly DateTimeOffset _expiresOn = DateTimeOffset.UtcNow.AddHours(1); + private readonly Guid _correlationId = Guid.NewGuid(); + private readonly string _tokenType = "Bearer"; + private readonly string _authenticationScheme = "TestScheme"; + private readonly X509Certificate2 _bindingCertificate; + + public TokenAcquirerTests() + { + _tokenAcquisition = Substitute.For(); + + // Create a test certificate for BindingCertificate scenarios + using var rsa = RSA.Create(); + var request = new CertificateRequest(new X500DistinguishedName("CN=Test"), rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + _bindingCertificate = request.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddYears(1)); + } + + [Fact] + public async Task GetTokenForAppAsync_WithoutBindingCertificate_ReturnsCorrectAcquireTokenResult() + { + // Arrange + var authResult = CreateMockAuthenticationResult(); + _tokenAcquisition.GetAuthenticationResultForAppAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(authResult); + + var tokenAcquirer = new TokenAcquirer(_tokenAcquisition, _authenticationScheme); + + // Act + var result = await ((ITokenAcquirer)tokenAcquirer).GetTokenForAppAsync( + _scope, + null, + CancellationToken.None); + + // Assert + Assert.NotNull(result); + Assert.Equal(_accessToken, result.AccessToken); + Assert.Equal(_expiresOn, result.ExpiresOn); + Assert.Equal(_tenantId, result.TenantId); + Assert.Equal(_idToken, result.IdToken); + Assert.Equal(new[] { _scope }, result.Scopes); + Assert.Equal(_correlationId, result.CorrelationId); + Assert.Equal(_tokenType, result.TokenType); + Assert.Null(result.BindingCertificate); + } + + [Fact] + public async Task GetTokenForAppAsync_WithBindingCertificate_ReturnsAcquireTokenResultWithBindingCertificate() + { + // Arrange + var authResult = CreateMockAuthenticationResult(bindingCertificate: _bindingCertificate); + _tokenAcquisition.GetAuthenticationResultForAppAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(authResult); + + var tokenAcquirer = new TokenAcquirer(_tokenAcquisition, _authenticationScheme); + + // Act + var result = await ((ITokenAcquirer)tokenAcquirer).GetTokenForAppAsync( + _scope, + null, + CancellationToken.None); + + // Assert + Assert.NotNull(result); + Assert.Equal(_accessToken, result.AccessToken); + Assert.Equal(_expiresOn, result.ExpiresOn); + Assert.Equal(_tenantId, result.TenantId); + Assert.Equal(_idToken, result.IdToken); + Assert.Equal(new[] { _scope }, result.Scopes); + Assert.Equal(_correlationId, result.CorrelationId); + Assert.Equal(_tokenType, result.TokenType); + Assert.NotNull(result.BindingCertificate); + Assert.Equal(_bindingCertificate.Thumbprint, result.BindingCertificate.Thumbprint); + } + + [Fact] + public async Task GetTokenForUserAsync_WithAutoSessionKey_PropagatesGeneratedKeyBackToCaller() + { + // Arrange + const string autoGeneratedKey = "test-auto-generated-key"; + var authResult = CreateMockAuthenticationResult(); + var callerOptions = new AcquireTokenOptions + { + LongRunningWebApiSessionKey = AcquireTokenOptions.LongRunningWebApiSessionKeyAuto, + }; + + _tokenAcquisition.GetAuthenticationResultForUserAsync( + Arg.Any>(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(callInfo => + { + // simulate TokenAcquisition writing the auto-generated key back to the options + var options = callInfo.ArgAt(5); + if (options is not null) + { + options.LongRunningWebApiSessionKey = autoGeneratedKey; + } + + return Task.FromResult(authResult); + }); + + var tokenAcquirer = new TokenAcquirer(_tokenAcquisition, _authenticationScheme); + + // Act + await ((ITokenAcquirer)tokenAcquirer).GetTokenForUserAsync( + new[] { _scope }, + callerOptions, + user: null, + CancellationToken.None); + + // Assert + Assert.Equal(autoGeneratedKey, callerOptions.LongRunningWebApiSessionKey); + } + + [Fact] + public async Task GetTokenForUserAsync_WithExplicitSessionKey_PropagatesKeyBackToCaller() + { + // Arrange + const string explicitKey = "test-key"; + var authResult = CreateMockAuthenticationResult(); + var callerOptions = new AcquireTokenOptions + { + LongRunningWebApiSessionKey = explicitKey, + }; + + _tokenAcquisition.GetAuthenticationResultForUserAsync( + Arg.Any>(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(callInfo => + { + // simulate that TokenAcquisition does not modify the key when an explicit key is provided + return Task.FromResult(authResult); + }); + + var tokenAcquirer = new TokenAcquirer(_tokenAcquisition, _authenticationScheme); + + // Act + await ((ITokenAcquirer)tokenAcquirer).GetTokenForUserAsync( + new[] { _scope }, + callerOptions, + user: null, + CancellationToken.None); + + // Assert + Assert.Equal(explicitKey, callerOptions.LongRunningWebApiSessionKey); + } + + [Fact] + public async Task GetTokenForUserAsync_WithNoSessionKey_SessionKeyRemainsNull() + { + // Arrange + var authResult = CreateMockAuthenticationResult(); + var callerOptions = new AcquireTokenOptions + { + // LongRunningWebApiSessionKey is not set + }; + + _tokenAcquisition.GetAuthenticationResultForUserAsync( + Arg.Any>(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(authResult); + + var tokenAcquirer = new TokenAcquirer(_tokenAcquisition, _authenticationScheme); + + // Act + await ((ITokenAcquirer)tokenAcquirer).GetTokenForUserAsync( + new[] { _scope }, + callerOptions, + user: null, + CancellationToken.None); + + // Assert + Assert.Null(callerOptions.LongRunningWebApiSessionKey); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public async Task GetTokenForUserAsync_WhenEffectiveKeyIsNullOrEmpty_DoesNotOverwriteCallerKey(string? effectiveKeyValue) + { + // Arrange + const string originalKey = "test-key"; + var authResult = CreateMockAuthenticationResult(); + var callerOptions = new AcquireTokenOptions + { + LongRunningWebApiSessionKey = originalKey, + }; + + _tokenAcquisition.GetAuthenticationResultForUserAsync( + Arg.Any>(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(callInfo => + { + // simulate TokenAcquisition setting the key to a null or empty value + var options = callInfo.ArgAt(5); + if (options is not null) + { + options.LongRunningWebApiSessionKey = effectiveKeyValue; + } + + return Task.FromResult(authResult); + }); + + var tokenAcquirer = new TokenAcquirer(_tokenAcquisition, _authenticationScheme); + + // Act + await ((ITokenAcquirer)tokenAcquirer).GetTokenForUserAsync( + new[] { _scope }, + callerOptions, + user: null, + CancellationToken.None); + + // Assert + Assert.Equal(originalKey, callerOptions.LongRunningWebApiSessionKey); + } + + private AuthenticationResult CreateMockAuthenticationResult(X509Certificate2? bindingCertificate = null) + { + var authResult = new AuthenticationResult( + _accessToken, + false, + null, + _expiresOn, + _expiresOn, + _tenantId, + null, + _idToken, + new[] { _scope }, + _correlationId); + + // Unfortunately, MSAL's AuthenticationResult.BindingCertificate is not settable, + // and we can't mock it, so we'll use a custom AuthenticationResult wrapper + // or test the functionality through integration tests + if (bindingCertificate != null) + { + // Use reflection to set the BindingCertificate property since it's not exposed in the constructor + var bindingCertificateProperty = typeof(AuthenticationResult).GetProperty("BindingCertificate"); + bindingCertificateProperty?.SetValue(authResult, bindingCertificate); + } + + return authResult; + } + } +} diff --git a/tests/Microsoft.Identity.Web.Test/TokenAcquisitionAddInTests.cs b/tests/Microsoft.Identity.Web.Test/TokenAcquisitionAddInTests.cs index 625d83ac8..b714ee94a 100644 --- a/tests/Microsoft.Identity.Web.Test/TokenAcquisitionAddInTests.cs +++ b/tests/Microsoft.Identity.Web.Test/TokenAcquisitionAddInTests.cs @@ -12,6 +12,9 @@ using System.Collections.Generic; using Microsoft.IdentityModel.Tokens; using System.Security.Claims; +using Microsoft.Identity.Client.AuthScheme; +using System.Threading; +using System; namespace Microsoft.Identity.Web.Tests { @@ -121,5 +124,166 @@ public async Task InvokeOnBeforeTokenAcquisitionForUsernamePassword_InvokesEvent Assert.NotNull(result); Assert.Equal(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); } + + [Fact] + public async Task InvokeOnBeforeTokenAcquisitionForOnBehalfOf_InvokesEvent() + { + // Arrange + var options = new TokenAcquisitionExtensionOptions(); + var acquireTokenOptions = new AcquireTokenOptions(); + acquireTokenOptions.ForceRefresh = true; + + //Configure mocks + using MockHttpClientFactory mockHttpClient = new(); + mockHttpClient.AddMockHandler(MockHttpCreator.CreateClientCredentialTokenHandler()); + + var confidentialApp = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithAuthority(TestConstants.AuthorityCommonTenant) + .WithHttpClientFactory(mockHttpClient) + .WithInstanceDiscovery(false) + .WithClientSecret(TestConstants.ClientSecret) + .WithExperimentalFeatures(true) + .Build(); + + var userAssertion = new UserAssertion("user-assertion-token"); + AcquireTokenOnBehalfOfParameterBuilder builder = confidentialApp + .AcquireTokenOnBehalfOf(new string[] { "scope" }, userAssertion); + + bool eventInvoked = false; + bool formatResultInvoked = false; + + MsalAuthenticationExtension extension = new MsalAuthenticationExtension(); + options.OnBeforeTokenAcquisitionForOnBehalfOf += (builder, options, user) => + { + MsalAuthenticationExtension extension = new MsalAuthenticationExtension(); + + // Create a test authentication operation implementing IAuthenticationOperation2 + var authOperation = new TestAuthenticationOperation2 + { + OnFormatResult = (result) => + { + formatResultInvoked = true; + return Task.FromResult(result); + } + }; + extension.AuthenticationOperation = authOperation; + extension.OnBeforeTokenRequestHandler = (request) => + { + eventInvoked = true; + request.BodyParameters.Add("x-ms-user", user?.User?.FindFirst("user")?.Value); + return Task.CompletedTask; + }; + + builder.WithAuthenticationExtension(extension); + }; + + var user = new ClaimsPrincipal( + new CaseSensitiveClaimsIdentity(new[] + { + new Claim(ClaimConstants.Sub, "user-id"), + new Claim(ClaimConstants.Name, "Test User"), + })); + + // Act + var eventArgs = new OnBehalfOfEventArgs() { User = user }; + await options.InvokeOnBeforeTokenAcquisitionForOnBehalfOfAsync(builder, acquireTokenOptions, eventArgs); + + var result = await builder.ExecuteAsync(); + + // Assert + Assert.True(eventInvoked); + Assert.True(formatResultInvoked); + Assert.NotNull(result); + Assert.Equal(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource); + } + + [Fact] + public async Task InvokeOnBeforeOnBehalfOfInitializedAsync_SyncHandler_CanChangeUserAssertionToken() + { + // Arrange + var options = new TokenAcquisitionExtensionOptions(); + string originalAssertion = "original-assertion"; + string modifiedAssertion = "modified-assertion"; + + bool eventInvoked = false; + options.OnBeforeOnBehalfOfInitialized += (eventArgs) => + { + eventInvoked = true; + Assert.Equal(originalAssertion, eventArgs.UserAssertionToken); + eventArgs.UserAssertionToken = modifiedAssertion; + }; + + var eventArgsObj = new OnBehalfOfEventArgs + { + UserAssertionToken = originalAssertion + }; + + // Act + await options.InvokeOnBeforeOnBehalfOfInitializedAsync(eventArgsObj); + + // Assert + Assert.True(eventInvoked); + Assert.Equal(modifiedAssertion, eventArgsObj.UserAssertionToken); + } + + [Fact] + public async Task InvokeOnBeforeOnBehalfOfInitializedAsync_AsyncHandler_CanChangeUserAssertionToken() + { + // Arrange + var options = new TokenAcquisitionExtensionOptions(); + string originalAssertion = "original-assertion"; + string modifiedAssertion = "modified-assertion-async"; + + bool eventInvoked = false; + options.OnBeforeOnBehalfOfInitializedAsync += (eventArgs) => + { + eventInvoked = true; + Assert.Equal(originalAssertion, eventArgs.UserAssertionToken); + eventArgs.UserAssertionToken = modifiedAssertion; + return Task.CompletedTask; + }; + + var eventArgsObj = new OnBehalfOfEventArgs + { + UserAssertionToken = originalAssertion + }; + + // Act + await options.InvokeOnBeforeOnBehalfOfInitializedAsync(eventArgsObj); + + // Assert + Assert.True(eventInvoked); + Assert.Equal(modifiedAssertion, eventArgsObj.UserAssertionToken); + } + + // Helper class for testing IAuthenticationOperation2 + private class TestAuthenticationOperation2 : IAuthenticationOperation2 + { + public Func>? OnFormatResult { get; set; } + + public int TelemetryTokenType => 0; + + public string AuthorizationHeaderPrefix => "Bearer"; + + public string KeyId => string.Empty; + + public string AccessTokenType => "Bearer"; + + public void FormatResult(AuthenticationResult authenticationResult) { } + + public Task FormatResultAsync(AuthenticationResult authenticationResult, CancellationToken cancellationToken = default) + { + if (OnFormatResult != null) + { + return OnFormatResult(authenticationResult); + } + return Task.FromResult(authenticationResult); + } + + public IReadOnlyDictionary GetTokenRequestParams() => new Dictionary(); + + public Task ValidateCachedTokenAsync(MsalCacheValidationData cachedTokenData) => Task.FromResult(false); + } } } diff --git a/tests/Microsoft.Identity.Web.Test/TokenAcquisitionAuthorityTests.cs b/tests/Microsoft.Identity.Web.Test/TokenAcquisitionAuthorityTests.cs index b998d57a2..c93a0b8ae 100644 --- a/tests/Microsoft.Identity.Web.Test/TokenAcquisitionAuthorityTests.cs +++ b/tests/Microsoft.Identity.Web.Test/TokenAcquisitionAuthorityTests.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Globalization; using System.Net.Http; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication.JwtBearer; @@ -129,7 +130,7 @@ public async Task VerifyCorrectAuthorityUsedInTokenAcquisition_B2CAuthorityTests InitializeTokenAcquisitionObjects(); - IConfidentialClientApplication app = await _tokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(mergedOptions); + IConfidentialClientApplication app = await _tokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(mergedOptions, isTokenBinding: false); string expectedAuthority = string.Format( CultureInfo.InvariantCulture, @@ -168,7 +169,7 @@ public async Task VerifyCorrectRedirectUriAsync( InitializeTokenAcquisitionObjects(); - IConfidentialClientApplication app = await _tokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(mergedOptions); + IConfidentialClientApplication app = await _tokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(mergedOptions, isTokenBinding: false); if (!string.IsNullOrEmpty(redirectUri)) { @@ -206,11 +207,11 @@ public async Task VerifyDifferentRegionsDifferentAppAsync() mergedOptions.AzureRegion = "UKEast"; - IConfidentialClientApplication appEast = await _tokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(mergedOptions); + IConfidentialClientApplication appEast = await _tokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(mergedOptions, isTokenBinding: false); mergedOptions.AzureRegion = "UKWest"; - IConfidentialClientApplication appWest = await _tokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(mergedOptions); + IConfidentialClientApplication appWest = await _tokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(mergedOptions, isTokenBinding: false); Assert.NotSame(appEast, appWest); } @@ -478,9 +479,9 @@ public async Task GetOrBuildManagedIdentity_TestAsync(string? clientId) InitializeTokenAcquisitionObjects(); // Act - var app1 = + var app1 = await _tokenAcquisition.GetOrBuildManagedIdentityApplicationAsync(mergedOptions, managedIdentityOptions); - var app2 = + var app2 = await _tokenAcquisition.GetOrBuildManagedIdentityApplicationAsync(mergedOptions, managedIdentityOptions); // Assert @@ -539,5 +540,362 @@ public async Task GetOrBuildManagedIdentity_TestConcurrencyAsync(string? clientI Assert.Same(testApp, app); } } + + [Fact] + public async Task BuildConfidentialClient_ClientClaimsAppearInClientAssertionAsync() + { + // Arrange + var tenantId = Guid.NewGuid().ToString(); + var clientId = Guid.NewGuid().ToString(); + var instance = "https://login.microsoftonline.com/"; + + using var rsa = System.Security.Cryptography.RSA.Create(2048); + var req = new System.Security.Cryptography.X509Certificates.CertificateRequest( + $"CN=TestClaimsCert", rsa, System.Security.Cryptography.HashAlgorithmName.SHA256, System.Security.Cryptography.RSASignaturePadding.Pkcs1); + var cert = req.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-5), DateTimeOffset.UtcNow.AddHours(1)); + + var credential = CertificateDescription.FromCertificate(cert); + _microsoftIdentityOptionsMonitor = new TestOptionsMonitor(new MicrosoftIdentityOptions + { + Instance = instance, + TenantId = tenantId, + ClientId = clientId, + ClientCredentials = new[] { credential } + }); + _applicationOptionsMonitor = new TestOptionsMonitor(new ConfidentialClientApplicationOptions + { + Instance = instance, + ClientId = clientId, + ClientSecret = "ignored" + }); + + var customClaims = new Dictionary + { + { "custom_claim_one", "value_one" }, + { "custom_claim_two", "value_two" } + }; + + var tokenAcquisitionOptions = new TokenAcquisitionOptions + { + ExtraParameters = new Dictionary + { + { "IDWEB_CLIENT_ASSERTION_CLAIMS", JsonSerializer.Serialize(customClaims) } + } + }; + + var capturingHandler = new CapturingHandler(instance.TrimEnd('/') + "/" + tenantId); + var httpClientFactory = new CapturingMsalHttpClientFactory(new HttpClient(capturingHandler)); + + // Build service collection + var services = new ServiceCollection(); + services.AddTransient(provider => _microsoftIdentityOptionsMonitor); + services.AddTransient(provider => _applicationOptionsMonitor); + services.Configure(o => { }); + services.AddTokenAcquisition(); + services.AddLogging(); + services.AddAuthentication(); + services.AddMemoryCache(); + services.AddHttpClient(); + services.AddSingleton(httpClientFactory); + _provider = services.BuildServiceProvider(); + + InitializeTokenAcquisitionObjects(); + + var mergedOptions = _provider.GetRequiredService().Get(OpenIdConnectDefaults.AuthenticationScheme); + MergedOptions.UpdateMergedOptionsFromMicrosoftIdentityOptions(_microsoftIdentityOptionsMonitor.Get(OpenIdConnectDefaults.AuthenticationScheme), mergedOptions); + MergedOptions.UpdateMergedOptionsFromConfidentialClientApplicationOptions(_applicationOptionsMonitor.Get(OpenIdConnectDefaults.AuthenticationScheme), mergedOptions); + + // Act first token acquisition (network call expected) + await _tokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(mergedOptions, false); + var result = await _tokenAcquisition.GetAuthenticationResultForAppAsync( + scope: "https://graph.microsoft.com/.default", + authenticationScheme: OpenIdConnectDefaults.AuthenticationScheme, + tenant: tenantId, + tokenAcquisitionOptions: tokenAcquisitionOptions); + + // Assert first network call produced client assertion with claims + Assert.NotNull(result.AccessToken); + Assert.False(string.IsNullOrEmpty(capturingHandler.CapturedClientAssertion)); + var payloadJson = DecodeJwtPayload(capturingHandler.CapturedClientAssertion!); + Assert.Contains("value_one", payloadJson, StringComparison.Ordinal); + Assert.Contains("value_two", payloadJson, StringComparison.Ordinal); + + // Second call should be served from cache: no new network request, no new assertion captured + capturingHandler.ResetCapture(); + var result2 = await _tokenAcquisition.GetAuthenticationResultForAppAsync( + scope: "https://graph.microsoft.com/.default", + authenticationScheme: OpenIdConnectDefaults.AuthenticationScheme, + tenant: tenantId, + tokenAcquisitionOptions: tokenAcquisitionOptions); + Assert.NotNull(result2.AccessToken); + Assert.True(string.IsNullOrEmpty(capturingHandler.CapturedClientAssertion)); + } + + [Fact] + public async Task ClientClaims_Cached_NoSecondNetworkCallAsync() + { + // Arrange: initial build with claims + var tenantId = Guid.NewGuid().ToString(); + var clientId = Guid.NewGuid().ToString(); + var instance = "https://login.microsoftonline.com/"; + using var rsa = System.Security.Cryptography.RSA.Create(2048); + var req = new System.Security.Cryptography.X509Certificates.CertificateRequest( + "CN=OptionACacheCert", rsa, System.Security.Cryptography.HashAlgorithmName.SHA256, System.Security.Cryptography.RSASignaturePadding.Pkcs1); + var cert = req.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-5), DateTimeOffset.UtcNow.AddHours(1)); + var credential = CertificateDescription.FromCertificate(cert); + _microsoftIdentityOptionsMonitor = new TestOptionsMonitor(new MicrosoftIdentityOptions + { + Instance = instance, + TenantId = tenantId, + ClientId = clientId, + ClientCredentials = new[] { credential } + }); + _applicationOptionsMonitor = new TestOptionsMonitor(new ConfidentialClientApplicationOptions + { + Instance = instance, + ClientId = clientId, + ClientSecret = "ignored" + }); + var customClaims = new Dictionary + { + { "custom_claim_one", "value_one" }, + { "custom_claim_two", "value_two" } + }; + var tokenAcquisitionOptions = new TokenAcquisitionOptions + { + ExtraParameters = new Dictionary { { "IDWEB_CLIENT_ASSERTION_CLAIMS", JsonSerializer.Serialize(customClaims) } } + }; + var capturingHandler = new CapturingHandler(instance.TrimEnd('/') + "/" + tenantId); + var httpClientFactory = new CapturingMsalHttpClientFactory(new HttpClient(capturingHandler)); + var services = new ServiceCollection(); + services.AddTransient(provider => _microsoftIdentityOptionsMonitor); + services.AddTransient(provider => _applicationOptionsMonitor); + services.Configure(o => { }); + services.AddTokenAcquisition(); + services.AddLogging(); + services.AddAuthentication(); + services.AddMemoryCache(); + services.AddHttpClient(); + services.AddSingleton(httpClientFactory); + _provider = services.BuildServiceProvider(); + InitializeTokenAcquisitionObjects(); + var mergedOptions = _provider.GetRequiredService().Get(OpenIdConnectDefaults.AuthenticationScheme); + MergedOptions.UpdateMergedOptionsFromMicrosoftIdentityOptions(_microsoftIdentityOptionsMonitor.Get(OpenIdConnectDefaults.AuthenticationScheme), mergedOptions); + MergedOptions.UpdateMergedOptionsFromConfidentialClientApplicationOptions(_applicationOptionsMonitor.Get(OpenIdConnectDefaults.AuthenticationScheme), mergedOptions); + await _tokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(mergedOptions, false); + var first = await _tokenAcquisition.GetAuthenticationResultForAppAsync("https://graph.microsoft.com/.default", OpenIdConnectDefaults.AuthenticationScheme, tenantId, tokenAcquisitionOptions); + Assert.NotNull(first.AccessToken); + Assert.False(string.IsNullOrEmpty(capturingHandler.CapturedClientAssertion)); + var payloadJson = DecodeJwtPayload(capturingHandler.CapturedClientAssertion!); + Assert.Contains("value_one", payloadJson, StringComparison.Ordinal); + capturingHandler.ResetCapture(); + var second = await _tokenAcquisition.GetAuthenticationResultForAppAsync("https://graph.microsoft.com/.default", OpenIdConnectDefaults.AuthenticationScheme, tenantId, tokenAcquisitionOptions); + Assert.NotNull(second.AccessToken); + // Option A expectation: cached token => no new client_assertion sent + Assert.True(string.IsNullOrEmpty(capturingHandler.CapturedClientAssertion)); + Assert.Equal(first.AccessToken, second.AccessToken); // token from cache + } + + [Fact] + public async Task ClientClaims_ForceRefresh_NewAssertionAsync() + { + // Arrange similar to Option A + var tenantId = Guid.NewGuid().ToString(); + var clientId = Guid.NewGuid().ToString(); + var instance = "https://login.microsoftonline.com/"; + using var rsa = System.Security.Cryptography.RSA.Create(2048); + var req = new System.Security.Cryptography.X509Certificates.CertificateRequest( + "CN=OptionBForceRefreshCert", rsa, System.Security.Cryptography.HashAlgorithmName.SHA256, System.Security.Cryptography.RSASignaturePadding.Pkcs1); + var cert = req.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-5), DateTimeOffset.UtcNow.AddHours(1)); + var credential = CertificateDescription.FromCertificate(cert); + _microsoftIdentityOptionsMonitor = new TestOptionsMonitor(new MicrosoftIdentityOptions + { + Instance = instance, + TenantId = tenantId, + ClientId = clientId, + ClientCredentials = new[] { credential } + }); + _applicationOptionsMonitor = new TestOptionsMonitor(new ConfidentialClientApplicationOptions + { + Instance = instance, + ClientId = clientId, + ClientSecret = "ignored" + }); + var customClaims = new Dictionary { { "claimX", "claimXValue" } }; + var tokenAcquisitionOptions = new TokenAcquisitionOptions + { + ExtraParameters = new Dictionary { { "IDWEB_CLIENT_ASSERTION_CLAIMS", JsonSerializer.Serialize(customClaims) } } + }; + var forceOptions = new TokenAcquisitionOptions + { + ForceRefresh = true, + ExtraParameters = new Dictionary { { "IDWEB_CLIENT_ASSERTION_CLAIMS", JsonSerializer.Serialize(customClaims) } } + }; + var capturingHandler = new CapturingHandler(instance.TrimEnd('/') + "/" + tenantId); + var httpClientFactory = new CapturingMsalHttpClientFactory(new HttpClient(capturingHandler)); + var services = new ServiceCollection(); + services.AddTransient(provider => _microsoftIdentityOptionsMonitor); + services.AddTransient(provider => _applicationOptionsMonitor); + services.Configure(o => { }); + services.AddTokenAcquisition(); + services.AddLogging(); + services.AddAuthentication(); + services.AddMemoryCache(); + services.AddHttpClient(); + services.AddSingleton(httpClientFactory); + _provider = services.BuildServiceProvider(); + InitializeTokenAcquisitionObjects(); + var mergedOptions = _provider.GetRequiredService().Get(OpenIdConnectDefaults.AuthenticationScheme); + MergedOptions.UpdateMergedOptionsFromMicrosoftIdentityOptions(_microsoftIdentityOptionsMonitor.Get(OpenIdConnectDefaults.AuthenticationScheme), mergedOptions); + MergedOptions.UpdateMergedOptionsFromConfidentialClientApplicationOptions(_applicationOptionsMonitor.Get(OpenIdConnectDefaults.AuthenticationScheme), mergedOptions); + await _tokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(mergedOptions, false); + var first = await _tokenAcquisition.GetAuthenticationResultForAppAsync("https://graph.microsoft.com/.default", OpenIdConnectDefaults.AuthenticationScheme, tenantId, tokenAcquisitionOptions); + Assert.NotNull(first.AccessToken); + Assert.False(string.IsNullOrEmpty(capturingHandler.CapturedClientAssertion)); + var firstAssertion = capturingHandler.CapturedClientAssertion; + capturingHandler.ResetCapture(); + // Option B + var second = await _tokenAcquisition.GetAuthenticationResultForAppAsync("https://graph.microsoft.com/.default", OpenIdConnectDefaults.AuthenticationScheme, tenantId, forceOptions); + Assert.NotNull(second.AccessToken); + // New network call expected (assertion recaptured) + Assert.False(string.IsNullOrEmpty(capturingHandler.CapturedClientAssertion)); + var payload2 = DecodeJwtPayload(capturingHandler.CapturedClientAssertion!); + Assert.Contains("claimXValue", payload2, StringComparison.Ordinal); + // But assertions should differ (signed each time by MSAL with new exp etc.) + Assert.NotEqual(firstAssertion, capturingHandler.CapturedClientAssertion); + } + + [Fact] + public async Task ClientClaims_ChangedClaimsNotAppliedWithoutRebuildAsync() + { + // Arrange initial app with initial claims + var tenantId = Guid.NewGuid().ToString(); + var clientId = Guid.NewGuid().ToString(); + var instance = "https://login.microsoftonline.com/"; + using var rsa = System.Security.Cryptography.RSA.Create(2048); + var req = new System.Security.Cryptography.X509Certificates.CertificateRequest( + "CN=OptionCChangedClaimsCert", rsa, System.Security.Cryptography.HashAlgorithmName.SHA256, System.Security.Cryptography.RSASignaturePadding.Pkcs1); + var cert = req.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-5), DateTimeOffset.UtcNow.AddHours(1)); + var credential = CertificateDescription.FromCertificate(cert); + _microsoftIdentityOptionsMonitor = new TestOptionsMonitor(new MicrosoftIdentityOptions + { + Instance = instance, + TenantId = tenantId, + ClientId = clientId, + ClientCredentials = new[] { credential } + }); + _applicationOptionsMonitor = new TestOptionsMonitor(new ConfidentialClientApplicationOptions + { + Instance = instance, + ClientId = clientId, + ClientSecret = "ignored" + }); + var initialClaims = new Dictionary { { "c1", "v1" } }; + var initialOptions = new TokenAcquisitionOptions + { + ExtraParameters = new Dictionary { { "IDWEB_CLIENT_ASSERTION_CLAIMS", JsonSerializer.Serialize(initialClaims) } } + }; + var capturingHandler = new CapturingHandler(instance.TrimEnd('/') + "/" + tenantId); + var httpClientFactory = new CapturingMsalHttpClientFactory(new HttpClient(capturingHandler)); + var services = new ServiceCollection(); + services.AddTransient(provider => _microsoftIdentityOptionsMonitor); + services.AddTransient(provider => _applicationOptionsMonitor); + services.Configure(o => { }); + services.AddTokenAcquisition(); + services.AddLogging(); + services.AddAuthentication(); + services.AddMemoryCache(); + services.AddHttpClient(); + services.AddSingleton(httpClientFactory); + _provider = services.BuildServiceProvider(); + InitializeTokenAcquisitionObjects(); + var mergedOptions = _provider.GetRequiredService().Get(OpenIdConnectDefaults.AuthenticationScheme); + MergedOptions.UpdateMergedOptionsFromMicrosoftIdentityOptions(_microsoftIdentityOptionsMonitor.Get(OpenIdConnectDefaults.AuthenticationScheme), mergedOptions); + MergedOptions.UpdateMergedOptionsFromConfidentialClientApplicationOptions(_applicationOptionsMonitor.Get(OpenIdConnectDefaults.AuthenticationScheme), mergedOptions); + await _tokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(mergedOptions, false); + var first = await _tokenAcquisition.GetAuthenticationResultForAppAsync("https://graph.microsoft.com/.default", OpenIdConnectDefaults.AuthenticationScheme, tenantId, initialOptions); + Assert.False(string.IsNullOrEmpty(capturingHandler.CapturedClientAssertion)); + var firstPayload = DecodeJwtPayload(capturingHandler.CapturedClientAssertion!); + Assert.Contains("v1", firstPayload, StringComparison.Ordinal); + // Attempt to change claims (should not affect cached app) + var newClaims = new Dictionary { { "c1", "v2" }, { "c2", "vNew" } }; + var newOptions = new TokenAcquisitionOptions + { + ForceRefresh = true, + ExtraParameters = new Dictionary { { "IDWEB_CLIENT_ASSERTION_CLAIMS", JsonSerializer.Serialize(newClaims) } } + }; + // Call GetOrBuild again with new claims + var app2 = await _tokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(mergedOptions, false); + // Same instance expected + Assert.Same(app2, await _tokenAcquisition.GetOrBuildConfidentialClientApplicationAsync(mergedOptions, false)); + capturingHandler.ResetCapture(); + var second = await _tokenAcquisition.GetAuthenticationResultForAppAsync("https://graph.microsoft.com/.default", OpenIdConnectDefaults.AuthenticationScheme, tenantId, newOptions); + Assert.False(string.IsNullOrEmpty(capturingHandler.CapturedClientAssertion)); + var secondPayload = DecodeJwtPayload(capturingHandler.CapturedClientAssertion!); + // Validate old value still present and new ones absent + Assert.Contains("v2", secondPayload, StringComparison.Ordinal); + Assert.DoesNotContain("v1", secondPayload, StringComparison.Ordinal); + } + + private static string DecodeJwtPayload(string jwt) + { + var parts = jwt.Split('.'); + Assert.True(parts.Length >= 2, "JWT format invalid"); + string payload = parts[1]; + // Base64Url decode + string padded = payload.Replace('-', '+').Replace('_', '/'); + switch (padded.Length % 4) + { + case 2: padded += "=="; break; + case 3: padded += "="; break; + } + var bytes = Convert.FromBase64String(padded); + return System.Text.Encoding.UTF8.GetString(bytes); + } + + private class CapturingMsalHttpClientFactory : IMsalHttpClientFactory + { + private readonly HttpClient _httpClient; + public CapturingMsalHttpClientFactory(HttpClient httpClient) => _httpClient = httpClient; + public HttpClient GetHttpClient() => _httpClient; + } + + private class CapturingHandler : HttpMessageHandler + { + private readonly string _authorityBase; + public string? CapturedClientAssertion {get; private set; } + public CapturingHandler(string authorityBase) => _authorityBase = authorityBase.TrimEnd('/'); + public void ResetCapture() => CapturedClientAssertion = null; + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var uri = request.RequestUri!.ToString(); + if (uri.EndsWith("/.well-known/openid-configuration", StringComparison.OrdinalIgnoreCase)) + { + var json = $"{{ \"token_endpoint\": \"{_authorityBase}/oauth2/v2.0/token\", \"issuer\": \"{_authorityBase}/\" }}"; + return new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json") + }; + } + if (uri.EndsWith("/oauth2/v2.0/token", StringComparison.OrdinalIgnoreCase)) + { + var body = await request.Content!.ReadAsStringAsync(); + foreach (var kv in body.Split('&')) + { + var pair = kv.Split('='); + if (pair.Length == 2 && pair[0] == "client_assertion") + { + CapturedClientAssertion = Uri.UnescapeDataString(pair[1]); + } + } + var tokenResponse = "{ \"access_token\": \"at\", \"expires_in\": 3600, \"token_type\": \"Bearer\" }"; + return new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(tokenResponse, System.Text.Encoding.UTF8, "application/json") + }; + } + return new HttpResponseMessage(System.Net.HttpStatusCode.NotFound); + } + } } } diff --git a/tests/Microsoft.Identity.Web.Test/TokenAcquisitionTests.cs b/tests/Microsoft.Identity.Web.Test/TokenAcquisitionTests.cs index bc8ef2fd7..65e4ee2d1 100644 --- a/tests/Microsoft.Identity.Web.Test/TokenAcquisitionTests.cs +++ b/tests/Microsoft.Identity.Web.Test/TokenAcquisitionTests.cs @@ -133,7 +133,7 @@ private TokenAcquirerFactory InitTokenAcquirerFactory() tokenAcquirerFactory.Services.Configure(options => { options.Instance = "https://login.microsoftonline.com/"; - options.TenantId = "f645ad92-e38d-4d1a-b510-d1b09a74a8ca"; + options.TenantId = "10c419d4-4a50-45b2-aa4e-919fb84df24f"; options.ClientId = "idu773ld-e38d-jud3-45lk-d1b09a74a8ca"; options.ExtraQueryParameters = new Dictionary { @@ -150,5 +150,238 @@ private TokenAcquirerFactory InitTokenAcquirerFactory() return tokenAcquirerFactory; } + + /// + /// Tests that when identity configuration is missing (simulating a misconfigured key like "ManagedIdentity " with trailing space), + /// a meaningful ArgumentException is thrown instead of a NullReferenceException. + /// This addresses issue #2921. + /// + [Fact] + public async Task GetAuthenticationResultForAppAsync_ThrowsMeaningfulError_WhenConfigurationIsMissing() + { + // Arrange - Create a factory with missing identity configuration + TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest(); + TokenAcquirerFactory tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance(); + tokenAcquirerFactory.Services.Configure(options => + { + // Intentionally NOT setting Instance, TenantId, or Authority + // This simulates the scenario where configuration keys have typos + // (e.g., "ManagedIdentity " instead of "ManagedIdentity") + options.ClientId = "test-client-id"; + options.ClientCredentials = [new CredentialDescription() + { + SourceType = CredentialSource.ClientSecret, + ClientSecret = "someSecret" + }]; + }); + + tokenAcquirerFactory.Services.AddSingleton(); + + IServiceProvider serviceProvider = tokenAcquirerFactory.Build(); + IAuthorizationHeaderProvider authorizationHeaderProvider = serviceProvider.GetRequiredService(); + + // Act & Assert - Should throw ArgumentException with meaningful message + var exception = await Assert.ThrowsAsync(async () => + await authorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync( + "https://graph.microsoft.com/.default", + new AuthorizationHeaderProviderOptions())); + + Assert.StartsWith(IDWebErrorMessage.MissingIdentityConfiguration, exception.Message, System.StringComparison.Ordinal); + } + + /// + /// Tests that SendX5C=true results in x5c claim being included in the client assertion. + /// This test examines the actual HTTP request to verify x5c presence in the JWT header. + /// + [Fact] + public async Task RopcFlow_WithSendX5CTrue_IncludesX5CInClientAssertion() + { + // Arrange + var factory = InitTokenAcquirerFactoryForRopcWithCertificate(sendX5C: true); + IServiceProvider serviceProvider = factory.Build(); + + var mockHttpClient = serviceProvider.GetRequiredService() as MockHttpClientFactory; + + var mockHandler = MockHttpCreator.CreateClientCredentialTokenHandler(); + mockHttpClient!.AddMockHandler(mockHandler); + + IAuthorizationHeaderProvider authorizationHeaderProvider = serviceProvider.GetRequiredService(); + + // Create claims principal with username and password for pure ROPC flow + var claims = new List + { + new System.Security.Claims.Claim(ClaimConstants.Username, "testuser@contoso.com"), + new System.Security.Claims.Claim(ClaimConstants.Password, "testpassword123") + }; + var claimsPrincipal = new System.Security.Claims.ClaimsPrincipal( + new Microsoft.IdentityModel.Tokens.CaseSensitiveClaimsIdentity(claims)); + + // Act + string result = await authorizationHeaderProvider.CreateAuthorizationHeaderForUserAsync( + new[] { "https://graph.microsoft.com/.default" }, + authorizationHeaderProviderOptions: null, + claimsPrincipal: claimsPrincipal); + + // Assert + Assert.NotNull(result); + Assert.Equal("Bearer header.payload.signature", result); + + // Verify the request was made + Assert.NotNull(mockHandler.ActualRequestMessage); + Assert.NotNull(mockHandler.ActualRequestPostData); + + // Verify it's ROPC flow + Assert.True(mockHandler.ActualRequestPostData.ContainsKey("grant_type")); + Assert.Equal("password", mockHandler.ActualRequestPostData["grant_type"]); + + // Verify x5c is present in client_assertion JWT header + string? clientAssertion = GetClientAssertionFromPostData(mockHandler.ActualRequestPostData); + if (clientAssertion != null) + { + string jwtHeader = DecodeJwtHeader(clientAssertion); + // With SendX5C=true, the header should contain "x5c" claim + Assert.Contains("\"x5c\"", jwtHeader, StringComparison.Ordinal); + } + } + + /// + /// Tests that SendX5C=false results in NO x5c claim in the client assertion. + /// This verifies that the x5c certificate chain is excluded when SendX5C=false. + /// + [Fact] + public async Task RopcFlow_WithSendX5CFalse_DoesNotIncludeX5CInClientAssertion() + { + // Arrange + var factory = InitTokenAcquirerFactoryForRopcWithCertificate(sendX5C: false); + IServiceProvider serviceProvider = factory.Build(); + + var mockHttpClient = serviceProvider.GetRequiredService() as MockHttpClientFactory; + + var mockHandler = MockHttpCreator.CreateClientCredentialTokenHandler(); + mockHttpClient!.AddMockHandler(mockHandler); + + IAuthorizationHeaderProvider authorizationHeaderProvider = serviceProvider.GetRequiredService(); + + // Create claims principal with username and password + var claims = new List + { + new System.Security.Claims.Claim(ClaimConstants.Username, "user@contoso.com"), + new System.Security.Claims.Claim(ClaimConstants.Password, "password123") + }; + var claimsPrincipal = new System.Security.Claims.ClaimsPrincipal( + new Microsoft.IdentityModel.Tokens.CaseSensitiveClaimsIdentity(claims)); + + // Act + string result = await authorizationHeaderProvider.CreateAuthorizationHeaderForUserAsync( + new[] { "https://graph.microsoft.com/.default" }, + authorizationHeaderProviderOptions: null, + claimsPrincipal: claimsPrincipal); + + // Assert + Assert.NotNull(result); + Assert.NotNull(mockHandler.ActualRequestMessage); + Assert.NotNull(mockHandler.ActualRequestPostData); + + // Verify it's ROPC flow + Assert.True(mockHandler.ActualRequestPostData.ContainsKey("grant_type")); + Assert.Equal("password", mockHandler.ActualRequestPostData["grant_type"]); + + // Verify x5c is NOT present in client_assertion JWT header + string? clientAssertion = GetClientAssertionFromPostData(mockHandler.ActualRequestPostData); + if (clientAssertion != null) + { + string jwtHeader = DecodeJwtHeader(clientAssertion); + // With SendX5C=false, the header should NOT contain "x5c" claim + Assert.DoesNotContain("\"x5c\"", jwtHeader, StringComparison.Ordinal); + } + } + + /// + /// Extracts the client_assertion parameter from HTTP POST data. + /// + /// The HTTP POST data dictionary. + /// The client_assertion JWT string, or null if not present. + private static string? GetClientAssertionFromPostData(Dictionary postData) + { + return postData.ContainsKey("client_assertion") ? postData["client_assertion"] : null; + } + + /// + /// Decodes the header portion of a JWT (JSON Web Token). + /// Converts base64url encoding to standard base64, then decodes to UTF-8 string. + /// + /// The complete JWT string in format: header.payload.signature + /// The decoded JWT header as a JSON string. + private static string DecodeJwtHeader(string jwt) + { + // Split JWT into parts (header.payload.signature) + var parts = jwt.Split('.'); + if (parts.Length < 2) + { + return string.Empty; + } + + // Convert base64url to base64 + string base64 = parts[0].Replace('-', '+').Replace('_', '/'); + + // Add padding if needed + switch (base64.Length % 4) + { + case 2: base64 += "=="; break; + case 3: base64 += "="; break; + } + + // Decode base64 to bytes, then to UTF-8 string + return System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(base64)); + } + + private TokenAcquirerFactory InitTokenAcquirerFactoryForRopcWithCertificate(bool sendX5C) + { + TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest(); + TokenAcquirerFactory tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance(); + + var mockHttpFactory = new MockHttpClientFactory(); + + tokenAcquirerFactory.Services.Configure(options => + { + options.Instance = "https://login.microsoftonline.com/"; + options.TenantId = "10c419d4-4a50-45b2-aa4e-919fb84df24f"; + options.ClientId = "idu773ld-e38d-jud3-45lk-d1b09a74a8ca"; + options.SendX5C = sendX5C; // Set the SendX5C flag + + // SendX5C is only meaningful with certificate credentials + // Certificate is used for CLIENT authentication, username/password for USER authentication (ROPC) + options.ClientCredentials = [ + CertificateDescription.FromCertificate(CreateTestCertificate()) + ]; + }); + + // Add MockedHttpClientFactory + tokenAcquirerFactory.Services.AddSingleton(mockHttpFactory); + + return tokenAcquirerFactory; + } + + /// + /// Creates a minimal self-signed certificate for testing purposes. + /// In unit tests, the mock HTTP handlers don't actually validate the certificate. + /// + private static System.Security.Cryptography.X509Certificates.X509Certificate2 CreateTestCertificate() + { + // Create a minimal self-signed certificate for testing + // The certificate details don't matter for unit tests as HTTP calls are mocked + using var rsa = System.Security.Cryptography.RSA.Create(2048); + var request = new System.Security.Cryptography.X509Certificates.CertificateRequest( + "CN=TestCertificate", + rsa, + System.Security.Cryptography.HashAlgorithmName.SHA256, + System.Security.Cryptography.RSASignaturePadding.Pkcs1); + + var certificate = request.CreateSelfSigned( + DateTimeOffset.UtcNow.AddDays(-1), + DateTimeOffset.UtcNow.AddDays(365)); + + return certificate; + } } } diff --git a/tests/Microsoft.Identity.Web.Test/WebApiExtensionsAotTests.cs b/tests/Microsoft.Identity.Web.Test/WebApiExtensionsAotTests.cs new file mode 100644 index 000000000..274aa1600 --- /dev/null +++ b/tests/Microsoft.Identity.Web.Test/WebApiExtensionsAotTests.cs @@ -0,0 +1,503 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#if NET10_0_OR_GREATER + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.Identity.Abstractions; +using Microsoft.Identity.Web.PostConfigureOptions; +using Microsoft.Identity.Web.Resource; +using Microsoft.Identity.Web.Test.Common; +using Microsoft.Identity.Web.Test.Common.TestHelpers; +using NSubstitute; +using Xunit; + +namespace Microsoft.Identity.Web.Test +{ + public class WebApiExtensionsAotTests + { + private const string ConfigSectionName = "AzureAd"; + private const string JwtBearerScheme = JwtBearerDefaults.AuthenticationScheme; + + [Fact] + public void AddMicrosoftIdentityWebApiAot_WithConfigSection_RegistersServices() + { + // Arrange + var services = new ServiceCollection().AddLogging(); + + // Act + services.AddAuthentication() + .AddMicrosoftIdentityWebApiAot( + options => + { + options.Instance = TestConstants.AadInstance; + options.TenantId = TestConstants.TenantIdAsGuid; + options.ClientId = TestConstants.ClientId; + }, + JwtBearerScheme, + null); + + // Assert + var provider = services.BuildServiceProvider(); + + // Verify core services are registered + Assert.Contains(services, s => s.ServiceType == typeof(IHttpContextAccessor)); + Assert.Contains(services, s => s.ServiceType == typeof(IMergedOptionsStore)); + Assert.Contains(services, s => s.ServiceType == typeof(MicrosoftIdentityIssuerValidatorFactory)); + + // Verify MicrosoftIdentityApplicationOptions can be retrieved + var appOptions = provider.GetRequiredService>().Get(JwtBearerScheme); + Assert.NotNull(appOptions); + + // Verify JWT bearer options can be retrieved + var jwtOptions = provider.GetRequiredService>().Get(JwtBearerScheme); + Assert.NotNull(jwtOptions); + + // Verify post-configurator is registered + var postConfigurators = services.Where(s => + s.ServiceType == typeof(IPostConfigureOptions) && + (s.ImplementationFactory != null || s.ImplementationType != null)).ToList(); + Assert.NotEmpty(postConfigurators); + } + + [Fact] + public void AddMicrosoftIdentityWebApiAot_WithAction_RegistersServices() + { + // Arrange + var services = new ServiceCollection().AddLogging(); + + // Act + services.AddAuthentication() + .AddMicrosoftIdentityWebApiAot(options => + { + options.Instance = TestConstants.AadInstance; + options.TenantId = TestConstants.TenantIdAsGuid; + options.ClientId = TestConstants.ClientId; + }, JwtBearerScheme, null); + + // Assert + var provider = services.BuildServiceProvider(); + + // Verify MicrosoftIdentityApplicationOptions are configured + var appOptions = provider.GetRequiredService>().Get(JwtBearerScheme); + Assert.Equal(TestConstants.AadInstance, appOptions.Instance); + Assert.Equal(TestConstants.TenantIdAsGuid, appOptions.TenantId); + Assert.Equal(TestConstants.ClientId, appOptions.ClientId); + + // Verify core services are registered + Assert.Contains(services, s => s.ServiceType == typeof(IHttpContextAccessor)); + Assert.Contains(services, s => s.ServiceType == typeof(IMergedOptionsStore)); + Assert.Contains(services, s => s.ServiceType == typeof(MicrosoftIdentityIssuerValidatorFactory)); + } + + [Fact] + public void AddMicrosoftIdentityWebApiAot_WithCustomJwtBearerOptions_AppliesConfiguration() + { + // Arrange + var services = new ServiceCollection().AddLogging(); + bool customOptionsApplied = false; + + // Act + services.AddAuthentication() + .AddMicrosoftIdentityWebApiAot( + options => + { + options.Instance = TestConstants.AadInstance; + options.TenantId = TestConstants.TenantIdAsGuid; + options.ClientId = TestConstants.ClientId; + }, + JwtBearerScheme, + jwtOptions => + { + customOptionsApplied = true; + jwtOptions.TokenValidationParameters.ClockSkew = TimeSpan.FromMinutes(5); + }); + + // Assert + var provider = services.BuildServiceProvider(); + var jwtOptions = provider.GetRequiredService>().Get(JwtBearerScheme); + + Assert.True(customOptionsApplied); + Assert.Equal(TimeSpan.FromMinutes(5), jwtOptions.TokenValidationParameters.ClockSkew); + } + + [Fact] + public void PostConfigurator_ConfiguresAuthority() + { + // Arrange + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication() + .AddMicrosoftIdentityWebApiAot(options => + { + options.Instance = TestConstants.AadInstance; + options.TenantId = TestConstants.TenantIdAsGuid; + options.ClientId = TestConstants.ClientId; + }, JwtBearerScheme, null); + + var provider = services.BuildServiceProvider(); + + // Act + var jwtOptions = provider.GetRequiredService>().Get(JwtBearerScheme); + + // Assert + Assert.NotNull(jwtOptions.Authority); + Assert.Contains(TestConstants.TenantIdAsGuid, jwtOptions.Authority, StringComparison.Ordinal); + Assert.EndsWith("/v2.0", jwtOptions.Authority, StringComparison.Ordinal); + } + + [Fact] + public void PostConfigurator_ConfiguresAudienceValidation() + { + // Arrange + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication() + .AddMicrosoftIdentityWebApiAot(options => + { + options.Instance = TestConstants.AadInstance; + options.TenantId = TestConstants.TenantIdAsGuid; + options.ClientId = TestConstants.ClientId; + }, JwtBearerScheme, null); + + var provider = services.BuildServiceProvider(); + + // Act + var jwtOptions = provider.GetRequiredService>().Get(JwtBearerScheme); + + // Assert + Assert.NotNull(jwtOptions.TokenValidationParameters); + Assert.NotNull(jwtOptions.TokenValidationParameters.AudienceValidator); + } + + [Fact] + public void PostConfigurator_ChainsOnTokenValidated_ForOboSupport() + { + // Arrange + var services = new ServiceCollection().AddLogging(); + services.AddAuthentication() + .AddMicrosoftIdentityWebApiAot(options => + { + options.Instance = TestConstants.AadInstance; + options.TenantId = TestConstants.TenantIdAsGuid; + options.ClientId = TestConstants.ClientId; + }, JwtBearerScheme, null); + + var provider = services.BuildServiceProvider(); + + // Act + var jwtOptions = provider.GetRequiredService>().Get(JwtBearerScheme); + + // Assert + Assert.NotNull(jwtOptions.Events); + Assert.NotNull(jwtOptions.Events.OnTokenValidated); + } + + [Fact] + public void PostConfigurator_RespectsCustomerAuthority() + { + // Arrange + var customAuthority = "https://custom.authority.com/tenant-id/v2.0"; + var services = new ServiceCollection().AddLogging(); + + services.AddAuthentication() + .AddMicrosoftIdentityWebApiAot(options => + { + options.Instance = TestConstants.AadInstance; + options.TenantId = TestConstants.TenantIdAsGuid; + options.ClientId = TestConstants.ClientId; + }, JwtBearerScheme, null); + + // Customer configures their own authority AFTER our call + services.Configure(JwtBearerScheme, options => + { + options.Authority = customAuthority; + }); + + var provider = services.BuildServiceProvider(); + + // Act + var jwtOptions = provider.GetRequiredService>().Get(JwtBearerScheme); + + // Assert - our PostConfigure should respect the customer's authority + Assert.Equal(customAuthority, jwtOptions.Authority); + } + + [Fact] + public void PostConfigurator_SkipsWhenNotConfigured() + { + // Arrange + var services = new ServiceCollection().AddLogging(); + + // Add JWT bearer without using our AOT method + services.AddAuthentication() + .AddJwtBearer(JwtBearerScheme); + + // Manually register the post-configurator + services.AddSingleton>( + sp => new TestOptionsMonitor( + new MicrosoftIdentityApplicationOptions())); + + services.AddSingleton>( + sp => new MicrosoftIdentityJwtBearerOptionsPostConfigurator( + sp.GetRequiredService>(), + sp)); + + var provider = services.BuildServiceProvider(); + + // Act - PostConfigure should skip because ClientId is not set + var jwtOptions = provider.GetRequiredService>().Get(JwtBearerScheme); + + // Assert - Should not throw, and authority should remain null + Assert.Null(jwtOptions.Authority); + } + + [Fact] + public void ValidateRequiredOptions_ThrowsForMissingClientId() + { + // Arrange + var options = new MicrosoftIdentityApplicationOptions + { + Instance = TestConstants.AadInstance, + TenantId = TestConstants.TenantIdAsGuid, + }; + + // Act & Assert + var exception = Assert.Throws(() => + MicrosoftIdentityJwtBearerOptionsPostConfigurator.ValidateRequiredOptions(options)); + + Assert.Contains("ClientId", exception.Message, StringComparison.Ordinal); + } + + [Fact] + public void ValidateRequiredOptions_ThrowsForMissingInstance() + { + // Arrange + var options = new MicrosoftIdentityApplicationOptions + { + ClientId = TestConstants.ClientId, + TenantId = TestConstants.TenantIdAsGuid, + Authority = "", + }; + + // Act & Assert + var exception = Assert.Throws(() => + MicrosoftIdentityJwtBearerOptionsPostConfigurator.ValidateRequiredOptions(options)); + + Assert.Contains("Instance", exception.Message, StringComparison.Ordinal); + } + + [Fact] + public void ValidateRequiredOptions_ThrowsForMissingTenantId() + { + // Arrange + var options = new MicrosoftIdentityApplicationOptions + { + ClientId = TestConstants.ClientId, + Instance = TestConstants.AadInstance, + Authority = "", + }; + + // Act & Assert + var exception = Assert.Throws(() => + MicrosoftIdentityJwtBearerOptionsPostConfigurator.ValidateRequiredOptions(options)); + + Assert.Contains("TenantId", exception.Message, StringComparison.Ordinal); + } + + [Fact] + public void ValidateRequiredOptions_ThrowsForMissingDomain_B2C() + { + // Arrange + var options = new MicrosoftIdentityApplicationOptions + { + ClientId = TestConstants.ClientId, + Instance = TestConstants.B2CInstance, + SignUpSignInPolicyId = TestConstants.B2CSignUpSignInUserFlow, + Authority = "", + }; + + // Act & Assert + var exception = Assert.Throws(() => + MicrosoftIdentityJwtBearerOptionsPostConfigurator.ValidateRequiredOptions(options)); + + Assert.Contains("Domain", exception.Message, StringComparison.Ordinal); + } + + [Fact] + public void ValidateRequiredOptions_PassesWithValidOptions() + { + // Arrange + var options = new MicrosoftIdentityApplicationOptions + { + ClientId = TestConstants.ClientId, + Instance = TestConstants.AadInstance, + TenantId = TestConstants.TenantIdAsGuid, + }; + + // Act & Assert - should not throw + MicrosoftIdentityJwtBearerOptionsPostConfigurator.ValidateRequiredOptions(options); + } + + [Fact] + public void BuildAuthority_AAD_BuildsCorrectAuthority() + { + // Arrange + var options = new MicrosoftIdentityApplicationOptions + { + Instance = TestConstants.AadInstance, + TenantId = TestConstants.TenantIdAsGuid, + }; + + // Act + var authority = Internal.IdentityOptionsHelpers.BuildAuthority(options); + + // Assert + Assert.Contains(TestConstants.AadInstance.TrimEnd('/'), authority, StringComparison.Ordinal); + Assert.Contains(TestConstants.TenantIdAsGuid, authority, StringComparison.Ordinal); + Assert.EndsWith("/v2.0", authority, StringComparison.Ordinal); + } + + [Fact] + public void BuildAuthority_B2C_BuildsCorrectAuthority() + { + // Arrange + var options = new MicrosoftIdentityApplicationOptions + { + Instance = TestConstants.B2CInstance, + Domain = TestConstants.B2CTenant, + SignUpSignInPolicyId = TestConstants.B2CSignUpSignInUserFlow, + }; + + // Act + var authority = Internal.IdentityOptionsHelpers.BuildAuthority(options); + + // Assert + Assert.Contains(TestConstants.B2CTenant, authority, StringComparison.Ordinal); + Assert.Contains(TestConstants.B2CSignUpSignInUserFlow, authority, StringComparison.Ordinal); + Assert.EndsWith("/v2.0", authority, StringComparison.Ordinal); + } + + [Fact] + public async Task ChainTokenStorageHandler_ChainsExistingHandler() + { + // Arrange + bool existingHandlerCalled = false; + Func existingHandler = context => + { + existingHandlerCalled = true; + return Task.CompletedTask; + }; + + // Act + var chainedHandler = Internal.IdentityOptionsHelpers.ChainTokenStorageHandler(existingHandler); + + // Assert + Assert.NotNull(chainedHandler); + + // Create a mock context + var httpContext = new DefaultHttpContext(); + var tokenValidatedContext = new TokenValidatedContext( + httpContext, + new AuthenticationScheme(JwtBearerScheme, null, typeof(JwtBearerHandler)), + new JwtBearerOptions()) + { + SecurityToken = new System.IdentityModel.Tokens.Jwt.JwtSecurityToken() + }; + + // Execute the chained handler + await chainedHandler(tokenValidatedContext); + + // Verify existing handler was called + Assert.True(existingHandlerCalled); + } + + [Fact] + public async Task ChainTokenStorageHandler_WorksWithNullExistingHandler() + { + // Arrange & Act + var chainedHandler = Internal.IdentityOptionsHelpers.ChainTokenStorageHandler(null); + + // Assert + Assert.NotNull(chainedHandler); + + // Create a mock context + var httpContext = new DefaultHttpContext(); + var tokenValidatedContext = new TokenValidatedContext( + httpContext, + new AuthenticationScheme(JwtBearerScheme, null, typeof(JwtBearerHandler)), + new JwtBearerOptions()) + { + SecurityToken = new System.IdentityModel.Tokens.Jwt.JwtSecurityToken() + }; + + // Execute the handler - should not throw + await chainedHandler(tokenValidatedContext); + } + + [Fact] + public void ConfigureAudienceValidation_SetsAudienceValidator() + { + // Arrange + var options = new MicrosoftIdentityApplicationOptions + { + ClientId = TestConstants.ClientId, + Instance = TestConstants.AadInstance, + TenantId = TestConstants.TenantIdAsGuid, + }; + var tokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters(); + + // Act + Internal.IdentityOptionsHelpers.ConfigureAudienceValidation( + tokenValidationParameters, + options.ClientId, + !string.IsNullOrWhiteSpace(options.SignUpSignInPolicyId)); + + // Assert + Assert.NotNull(tokenValidationParameters.AudienceValidator); + } + + [Fact] + public void AddMicrosoftIdentityWebApiAot_WithTokenAcquisition_EnablesObo() + { + // Arrange + var services = new ServiceCollection().AddLogging(); + + // Act - The key test: OBO should work without calling EnableTokenAcquisitionToCallDownstreamApi + services.AddAuthentication() + .AddMicrosoftIdentityWebApiAot(options => + { + options.Instance = TestConstants.AadInstance; + options.TenantId = TestConstants.TenantIdAsGuid; + options.ClientId = TestConstants.ClientId; + }, JwtBearerScheme, null); + + services.AddTokenAcquisition(); + + var provider = services.BuildServiceProvider(); + + // Assert + var jwtOptions = provider.GetRequiredService>().Get(JwtBearerScheme); + + // Verify OnTokenValidated is set up for OBO + Assert.NotNull(jwtOptions.Events); + Assert.NotNull(jwtOptions.Events.OnTokenValidated); + + // Verify MergedOptions are populated via the merger + var mergedOptionsStore = provider.GetRequiredService(); + var mergedOptions = mergedOptionsStore.Get(JwtBearerScheme); + Assert.NotNull(mergedOptions); + Assert.Equal(TestConstants.ClientId, mergedOptions.ClientId); + } + } +} + +#endif diff --git a/tests/PerformanceTests/Microsoft.Identity.Web.Perf.Benchmark/TokenAcquisitionTests.cs b/tests/PerformanceTests/Microsoft.Identity.Web.Perf.Benchmark/TokenAcquisitionTests.cs index 7f06ac360..26a3ef489 100644 --- a/tests/PerformanceTests/Microsoft.Identity.Web.Perf.Benchmark/TokenAcquisitionTests.cs +++ b/tests/PerformanceTests/Microsoft.Identity.Web.Perf.Benchmark/TokenAcquisitionTests.cs @@ -12,7 +12,7 @@ using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Identity.Client; using Microsoft.Identity.Web.Test.Common; -using Microsoft.Identity.Lab.Api; +using Microsoft.Identity.Test.LabInfrastructure; namespace Microsoft.Identity.Web.Perf.Benchmark { @@ -21,7 +21,7 @@ public class TokenAcquisitionTests private readonly WebApplicationFactory _factory; private HttpClient _client; -#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. // The GlobalSetup ensures that the _client is not null. public TokenAcquisitionTests() { @@ -59,7 +59,7 @@ public void GlobalCleanup() public async Task GetAccessTokenForUserAsync() { HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, TestConstants.SecurePageGetTokenForUserAsync); - HttpResponseMessage response = await _client.SendAsync(httpRequestMessage); + HttpResponseMessage response = await _client.SendAsync(httpRequestMessage); if (!response.IsSuccessStatusCode) { throw new HttpRequestException($"GetAccessTokenForUserAsync failed. Status code: {response.StatusCode}. Reason phrase: {response.ReasonPhrase}."); @@ -79,18 +79,18 @@ public async Task GetAccessTokenForAppAsync() private static async Task AcquireTokenForLabUserAsync() { - var labResponse = await LabUserHelper.GetSpecificUserAsync(TestConstants.OBOUser).ConfigureAwait(false); + var userConfig = await LabResponseHelper.GetUserConfigAsync("MSAL-User-Default-JSON"); var msalPublicClient = PublicClientApplicationBuilder .Create(TestConstants.OBOClientSideClientId) - .WithAuthority(labResponse.Lab.Authority, TestConstants.Organizations) + .WithAuthority($"https://login.microsoftonline.com/{TestConstants.TenantIdAsGuid}", TestConstants.Organizations) .Build(); #pragma warning disable CS0618 // Obsolete AuthenticationResult authResult = await msalPublicClient .AcquireTokenByUsernamePassword( TestConstants.s_oBOApiScope, - TestConstants.OBOUser, - labResponse.User.GetOrFetchPassword()) + userConfig.Upn, + LabResponseHelper.FetchUserPassword(userConfig.LabName)) .ExecuteAsync(CancellationToken.None) .ConfigureAwait(false); 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 diff --git a/tools/CrossPlatformValidator/CrossPlatformValidation/Benchmark/Benchmark.csproj b/tools/CrossPlatformValidator/CrossPlatformValidation/Benchmark/Benchmark.csproj index 01f7ecaf0..aff8f3b2d 100644 --- a/tools/CrossPlatformValidator/CrossPlatformValidation/Benchmark/Benchmark.csproj +++ b/tools/CrossPlatformValidator/CrossPlatformValidation/Benchmark/Benchmark.csproj @@ -10,7 +10,7 @@ - + diff --git a/tools/CrossPlatformValidator/CrossPlatformValidation/Benchmark/Program.cs b/tools/CrossPlatformValidator/CrossPlatformValidation/Benchmark/Program.cs index 941e30856..1a0aed17f 100644 --- a/tools/CrossPlatformValidator/CrossPlatformValidation/Benchmark/Program.cs +++ b/tools/CrossPlatformValidator/CrossPlatformValidation/Benchmark/Program.cs @@ -8,7 +8,7 @@ using BenchmarkDotNet.Running; using BenchmarkDotNet.Toolchains.InProcess.NoEmit; using Microsoft.Identity.Client; -using Microsoft.Identity.Lab.Api; +using Microsoft.Identity.Test.LabInfrastructure; namespace Brenchmark { @@ -25,24 +25,24 @@ static void Main(string[] args) public class ValidateBenchmark { private static IPublicClientApplication msalPublicClient; - private static LabResponse labResponse; + private static LabUserConfig userConfig; private static string authorizationHeader; static ValidateBenchmark() { - Initialize("https://login.microsoftonline.com/organizations", "f4aa5217-e87c-42b2-82af-5624dd14ee72"); - labResponse = LabUserHelper.GetSpecificUserAsync(OBOUser).GetAwaiter().GetResult(); + Initialize("https://login.microsoftonline.com/organizations", "8837cde9-4029-4bfc-9259-e9e70ce670f7"); + userConfig = LabResponseHelper.GetUserConfigAsync("MSAL-User-Default-JSON").GetAwaiter().GetResult(); msalPublicClient = PublicClientApplicationBuilder .Create(OBOClientSideClientId) - .WithAuthority(labResponse.Lab.Authority, Organizations) + .WithAuthority($"{userConfig.Authority}{userConfig.TenantId}", Organizations) .Build(); authorizationHeader = AcquireTokenForLabUserAsync().Result.CreateAuthorizationHeader(); } public const string Organizations = "organizations"; - public const string OBOUser = "idlab1@msidlab4.onmicrosoft.com"; - public const string OBOClientSideClientId = "c0485386-1e9a-4663-bc96-7ab30656de7f"; - public static string[] s_oBOApiScope = new string[] { "api://f4aa5217-e87c-42b2-82af-5624dd14ee72/.default" }; + public const string OBOUser = "MSAL-User-Default@id4slab1.onmicrosoft.com"; + public const string OBOClientSideClientId = "9c0e534b-879c-4dce-b0e2-0e1be873ba14"; + public static string[] s_oBOApiScope = new string[] { "api://8837cde9-4029-4bfc-9259-e9e70ce670f7/.default" }; public int numberValidations = 1000000; [DllImport("CrossPlatformValidation.dll")] @@ -80,7 +80,7 @@ private static async Task AcquireTokenForLabUserAsync() .AcquireTokenByUsernamePassword( s_oBOApiScope, OBOUser, - labResponse.User.GetOrFetchPassword()) + LabResponseHelper.FetchUserPassword(userConfig.LabName)) .ExecuteAsync(CancellationToken.None) .ConfigureAwait(false); } diff --git a/tools/CrossPlatformValidator/CrossPlatformValidation/BenchmarkCSharp/BenchmarkCSharp.csproj b/tools/CrossPlatformValidator/CrossPlatformValidation/BenchmarkCSharp/BenchmarkCSharp.csproj index d5aeee273..a29103f11 100644 --- a/tools/CrossPlatformValidator/CrossPlatformValidation/BenchmarkCSharp/BenchmarkCSharp.csproj +++ b/tools/CrossPlatformValidator/CrossPlatformValidation/BenchmarkCSharp/BenchmarkCSharp.csproj @@ -17,7 +17,7 @@ - + @@ -34,5 +34,5 @@ - + diff --git a/tools/CrossPlatformValidator/CrossPlatformValidation/BenchmarkCSharp/Program.cs b/tools/CrossPlatformValidator/CrossPlatformValidation/BenchmarkCSharp/Program.cs index ff7ccdbc1..ecd3d8235 100644 --- a/tools/CrossPlatformValidator/CrossPlatformValidation/BenchmarkCSharp/Program.cs +++ b/tools/CrossPlatformValidator/CrossPlatformValidation/BenchmarkCSharp/Program.cs @@ -9,7 +9,7 @@ using BenchmarkDotNet.Toolchains.InProcess.NoEmit; using CrossPlatformValidation; using Microsoft.Identity.Client; -using Microsoft.Identity.Lab.Api; +using Microsoft.Identity.Test.LabInfrastructure; namespace BenchmarkCSharp { @@ -37,7 +37,7 @@ private static void DisplayTestSubject() public class ValidateBenchmark { private static IPublicClientApplication msalPublicClient; - private static LabResponse labResponse; + private static LabUserConfig userConfig; private static string authorizationHeader; private static RequestValidator _requestValidator; @@ -45,19 +45,19 @@ public class ValidateBenchmark static ValidateBenchmark() { _requestValidator = new RequestValidator(); - _requestValidator.Initialize("https://login.microsoftonline.com/organizations", "f4aa5217-e87c-42b2-82af-5624dd14ee72"); - labResponse = LabUserHelper.GetSpecificUserAsync(OBOUser).GetAwaiter().GetResult(); + _requestValidator.Initialize("https://login.microsoftonline.com/organizations", "8837cde9-4029-4bfc-9259-e9e70ce670f7"); + userConfig = LabResponseHelper.GetUserConfigAsync("MSAL-User-Default-JSON").GetAwaiter().GetResult(); msalPublicClient = PublicClientApplicationBuilder .Create(OBOClientSideClientId) - .WithAuthority(labResponse.Lab.Authority, Organizations) + .WithAuthority($"{userConfig.Authority}{userConfig.TenantId}", Organizations) .Build(); authorizationHeader = AcquireTokenForLabUserAsync().Result.CreateAuthorizationHeader(); } public const string Organizations = "organizations"; - public const string OBOUser = "idlab1@msidlab4.onmicrosoft.com"; - public const string OBOClientSideClientId = "c0485386-1e9a-4663-bc96-7ab30656de7f"; - public static string[] s_oBOApiScope = new string[] { "api://f4aa5217-e87c-42b2-82af-5624dd14ee72/.default" }; + public const string OBOUser = "MSAL-User-Default@id4slab1.onmicrosoft.com"; + public const string OBOClientSideClientId = "9c0e534b-879c-4dce-b0e2-0e1be873ba14"; + public static string[] s_oBOApiScope = new string[] { "api://8837cde9-4029-4bfc-9259-e9e70ce670f7/.default" }; public int numberValidations = 1000000; [Benchmark] @@ -89,7 +89,7 @@ private static async Task AcquireTokenForLabUserAsync() .AcquireTokenByUsernamePassword( s_oBOApiScope, OBOUser, - labResponse.User.GetOrFetchPassword()) + LabResponseHelper.FetchUserPassword(userConfig.LabName)) .ExecuteAsync(CancellationToken.None) .ConfigureAwait(false); } diff --git a/tools/CrossPlatformValidator/CrossPlatformValidation/CSharpConsoleApp/CSharpConsoleApp.csproj b/tools/CrossPlatformValidator/CrossPlatformValidation/CSharpConsoleApp/CSharpConsoleApp.csproj index 0b0e0b207..a51fb4323 100644 --- a/tools/CrossPlatformValidator/CrossPlatformValidation/CSharpConsoleApp/CSharpConsoleApp.csproj +++ b/tools/CrossPlatformValidator/CrossPlatformValidation/CSharpConsoleApp/CSharpConsoleApp.csproj @@ -10,7 +10,7 @@ - + diff --git a/tools/CrossPlatformValidator/CrossPlatformValidation/CSharpConsoleApp/Program.cs b/tools/CrossPlatformValidator/CrossPlatformValidation/CSharpConsoleApp/Program.cs index 5ca0b9865..dba6296cb 100644 --- a/tools/CrossPlatformValidator/CrossPlatformValidation/CSharpConsoleApp/Program.cs +++ b/tools/CrossPlatformValidator/CrossPlatformValidation/CSharpConsoleApp/Program.cs @@ -3,13 +3,13 @@ using CrossPlatformValidation; using Microsoft.Identity.Client; -using Microsoft.Identity.Lab.Api; +using Microsoft.Identity.Test.LabInfrastructure; Console.WriteLine("Hello, World!"); RequestValidator requestValidator = new(); -requestValidator.Initialize("https://login.microsoftonline.com/organizations", "f4aa5217-e87c-42b2-82af-5624dd14ee72"); +requestValidator.Initialize("https://login.microsoftonline.com/organizations", "8837cde9-4029-4bfc-9259-e9e70ce670f7"); string authorizationHeader = AcquireTokenForLabUserAsync().Result.CreateAuthorizationHeader(); var result = requestValidator.Validate(authorizationHeader); //string token = "Bearer "; @@ -23,22 +23,22 @@ static async Task AcquireTokenForLabUserAsync() { - string Organizations = "organizations"; - string OBOUser = "idlab1@msidlab4.onmicrosoft.com"; - string OBOClientSideClientId = "c0485386-1e9a-4663-bc96-7ab30656de7f"; - string[] s_oBOApiScope = new string[] { "api://f4aa5217-e87c-42b2-82af-5624dd14ee72/.default" }; + string Organizations = "organizations"; + string OBOUser = "MSAL-User-Default@id4slab1.onmicrosoft.com"; + string OBOClientSideClientId = "9c0e534b-879c-4dce-b0e2-0e1be873ba14"; + string[] s_oBOApiScope = new string[] { "api://8837cde9-4029-4bfc-9259-e9e70ce670f7/.default" }; -var labResponse = await LabUserHelper.GetSpecificUserAsync(OBOUser).ConfigureAwait(false); + var userConfig = await LabResponseHelper.GetUserConfigAsync("MSAL-User-Default-JSON"); var msalPublicClient = PublicClientApplicationBuilder .Create(OBOClientSideClientId) - .WithAuthority(labResponse.Lab.Authority, Organizations) + .WithAuthority($"{userConfig.Authority}{userConfig.TenantId}", Organizations) .Build(); AuthenticationResult authResult = await msalPublicClient .AcquireTokenByUsernamePassword( s_oBOApiScope, OBOUser, - labResponse.User.GetOrFetchPassword()) + LabResponseHelper.FetchUserPassword(userConfig.LabName)) .ExecuteAsync(CancellationToken.None) .ConfigureAwait(false); diff --git a/tools/CrossPlatformValidator/CrossPlatformValidation/CrossPlatformValidatorTests/CrossPlatformValidatorTests.csproj b/tools/CrossPlatformValidator/CrossPlatformValidation/CrossPlatformValidatorTests/CrossPlatformValidatorTests.csproj index 578266d53..5e6caeb4c 100644 --- a/tools/CrossPlatformValidator/CrossPlatformValidation/CrossPlatformValidatorTests/CrossPlatformValidatorTests.csproj +++ b/tools/CrossPlatformValidator/CrossPlatformValidation/CrossPlatformValidatorTests/CrossPlatformValidatorTests.csproj @@ -10,7 +10,7 @@ - + diff --git a/tools/CrossPlatformValidator/CrossPlatformValidation/CrossPlatformValidatorTests/InitializeAndValidateTests.cs b/tools/CrossPlatformValidator/CrossPlatformValidation/CrossPlatformValidatorTests/InitializeAndValidateTests.cs index ea407e283..cba8d2a9c 100644 --- a/tools/CrossPlatformValidator/CrossPlatformValidation/CrossPlatformValidatorTests/InitializeAndValidateTests.cs +++ b/tools/CrossPlatformValidator/CrossPlatformValidation/CrossPlatformValidatorTests/InitializeAndValidateTests.cs @@ -3,16 +3,16 @@ using System.Runtime.InteropServices; using Microsoft.Identity.Client; -using Microsoft.Identity.Lab.Api; +using Microsoft.Identity.Test.LabInfrastructure; namespace CrossPlatformValidatorTests { public class InitializeAndValidateTests { public const string Organizations = "organizations"; - public const string OBOUser = "idlab1@msidlab4.onmicrosoft.com"; - public const string OBOClientSideClientId = "c0485386-1e9a-4663-bc96-7ab30656de7f"; - public static string[] s_oBOApiScope = new string[] { "api://f4aa5217-e87c-42b2-82af-5624dd14ee72/.default" }; + public const string OBOUser = "MSAL-User-Default@id4slab1.onmicrosoft.com"; + public const string OBOClientSideClientId = "9c0e534b-879c-4dce-b0e2-0e1be873ba14"; + public static string[] s_oBOApiScope = new string[] { "api://8837cde9-4029-4bfc-9259-e9e70ce670f7/.default" }; public int numberValidations = 1000000; [DllImport("CrossPlatformValidation.dll")] @@ -31,29 +31,29 @@ public void InitializeTestSucceeds() [Fact] public void ValidateTestSucceeds() { - Initialize("https://login.microsoftonline.com/organizations", "f4aa5217-e87c-42b2-82af-5624dd14ee72"); + Initialize("https://login.microsoftonline.com/organizations", "8837cde9-4029-4bfc-9259-e9e70ce670f7"); string authorizationHeader = AcquireTokenForLabUserAsync().Result.CreateAuthorizationHeader(); for (int i = 0; i < numberValidations; i++) { var result = Validate(authorizationHeader); Assert.NotNull(result); } - + } private static async Task AcquireTokenForLabUserAsync() { - var labResponse = await LabUserHelper.GetSpecificUserAsync(OBOUser).ConfigureAwait(false); + var userConfig = await LabResponseHelper.GetUserConfigAsync("MSAL-User-Default-JSON"); var msalPublicClient = PublicClientApplicationBuilder .Create(OBOClientSideClientId) - .WithAuthority(labResponse.Lab.Authority, Organizations) + .WithAuthority($"{userConfig.Authority}{userConfig.TenantId}", Organizations) .Build(); AuthenticationResult authResult = await msalPublicClient .AcquireTokenByUsernamePassword( s_oBOApiScope, OBOUser, - labResponse.User.GetOrFetchPassword()) + LabResponseHelper.FetchUserPassword(userConfig.LabName)) .ExecuteAsync(CancellationToken.None) .ConfigureAwait(false); diff --git a/tools/README.md b/tools/README.md new file mode 100644 index 000000000..bde675a52 --- /dev/null +++ b/tools/README.md @@ -0,0 +1,229 @@ +# Microsoft Identity Web - Tools Directory + +This directory contains various tools and utilities used for development, testing, and maintenance of the Microsoft Identity Web library. Each tool serves a specific purpose in the development workflow. + +## Table of Contents + +- [PowerShell Scripts](#powershell-scripts) + - [Check-BrokenLinks.ps1](#check-brokenlinksps1) + - [mark-shipped.ps1](#mark-shippedps1) +- [.NET Tools](#net-tools) + - [ConfigureGeneratedApplications](#configuregeneratedapplications) + - [GenerateMergeOptionsMethods](#generatemergeoptionsmethods) + - [CrossPlatformValidator](#crossplatformvalidator) + - [app-provisioning-tool](#app-provisioning-tool) + +--- + +## PowerShell Scripts + +### Check-BrokenLinks.ps1 + +**Purpose:** Validates markdown documentation integrity by detecting broken internal links across the repository. + +**Functionality:** +- Recursively scans all `.md` files in the repository +- Extracts markdown links in the format `[text](target)` +- Verifies that target files exist (skips external URLs, anchors, and mailto links) +- Optionally checks external HTTP/HTTPS links for validity + +**Usage:** +```powershell +.\Check-BrokenLinks.ps1 [-Path ] [-IncludeExternal] [-OutputFormat {Table|List|Json|Csv}] +``` + +**Parameters:** +- `-Path`: Root path to start scanning (defaults to current directory) +- `-IncludeExternal`: Include validation of external HTTP/HTTPS links +- `-OutputFormat`: Format of the output report (Table, List, Json, or Csv) + +**Output:** Reports broken links grouped by pattern (scenarios/, deployment/, etc.) + +--- + +### mark-shipped.ps1 + +**Purpose:** Manages the .NET API surface by moving unshipped APIs to shipped APIs and cleaning up the unshipped API files. + +**Functionality:** +- Processes both Public API and Internal API tracking files +- Reads unshipped APIs from `PublicAPI.Unshipped.txt` and `InternalAPI.Unshipped.txt` +- Moves non-removed API items to the corresponding `Shipped.txt` files +- Filters out items marked with the `*REMOVED*` prefix +- Clears unshipped files and reinitializes them with `#nullable enable` + +**Key Files:** +- `PublicAPI.Shipped.txt` / `PublicAPI.Unshipped.txt` +- `InternalAPI.Shipped.txt` / `InternalAPI.Unshipped.txt` + +**Usage:** +```powershell +.\mark-shipped.ps1 +``` + +Runs without arguments and processes all API files recursively throughout the repository. + +--- + +## .NET Tools + +### ConfigureGeneratedApplications + +**Purpose:** A C# .NET 8.0 console application that configures project template applications by replacing placeholders with parameter values from JSON configuration files. + +**Key Files:** +- `Program.cs` - Main application logic (JSON parsing, file processing, replacements) +- `Configuration.json` - Configuration file containing parameters and project definitions +- Model classes: `Configuration`, `Project`, `File`, `PropertyMapping`, `Replacement` + +**Workflow:** +1. Reads `Configuration.json` containing project template definitions +2. For each project, processes specified JSON files +3. Locates properties using JSON path notation (e.g., `database:connectionString`) +4. Replaces placeholder values with actual parameter values +5. Generates an `issue.md` file with a testing checklist for the configured applications + +**Technology:** .NET 8.0 Console Application + +**Output:** Updated project files with configured values and a testing checklist + +--- + +### GenerateMergeOptionsMethods + +**Purpose:** A code generator tool that creates merge/update methods for synchronizing properties between different option classes. + +**Functionality:** +- Uses reflection to inspect two types and their properties +- Generates C# code for property synchronization +- Handles different property types appropriately: + - String properties: Uses `string.IsNullOrWhiteSpace()` checks + - Value types: Direct assignment + - Reference types: Null checks before assignment +- Currently generates `UpdateMicrosoftIdentityApplicationOptionsFromMergedOptions()` method + +**Technology:** .NET 8.0 Console Application + +**Output:** Generated C# code for updating one option type from another (printed to console, intended to be copied into source files) + +**Use Case:** Reduces manual coding errors when creating merge methods between option classes in the library. + +--- + +### CrossPlatformValidator + +**Purpose:** A cross-platform JWT token validation library suite designed for testing authentication across different .NET frameworks and platforms. + +**Structure:** + +#### Core Library (CrossPlatformValidation) +The main validation library that provides JWT token validation capabilities. + +**Key Components:** +- `RequestValidator` - Validates JWT tokens against Azure AD/Microsoft Entra ID +- `EntryPoint` - Exposes validation functionality via P/Invoke for use from unmanaged code (C/C++) +- Supports both `JwtSecurityTokenHandler` and `JsonWebTokenHandler` + +**Functionality:** +- Initializes with authority URL and audience +- Validates bearer tokens using Azure AD OIDC configuration +- Returns `TokenValidationResult` containing claims and issuer information + +#### Supporting Projects +- **CSharpConsoleApp** - Example console application demonstrating validator usage +- **BenchmarkCSharp** - Performance benchmarking tool for measuring validation performance +- **CrossPlatformValidatorTests** - Comprehensive test suite for the validator + +**Technology:** Multi-targeting .NET library with C# examples and tests + +**Use Case:** Testing authentication scenarios across different platforms and .NET framework versions. + +--- + +### app-provisioning-tool + +**Purpose:** A dotnet CLI tool (`msidentity-app-sync`) that creates and updates Azure AD and Azure AD B2C app registrations, and automatically configures ASP.NET Core application code. + +**Package Name:** `msidentity-app-sync` + +**Structure:** +- `app-provisioning-tool/` - CLI executable project +- `app-provisioning-lib/` - Core provisioning library +- `tests/` - Test suite +- `images/`, `README.md`, `vs2019-16.9-how-to-use.md` - Documentation and resources + +**Capabilities:** +- Auto-detects application type (webapp, mvc, webapi, blazor) +- Creates Azure AD and Azure AD B2C app registrations +- Updates configuration files (`appsettings.json`, `Program.cs`, etc.) +- Handles redirect URIs from launch settings +- Supports configuring existing applications via `--client-id` parameter +- Manages app secrets and authentication configuration + +**Installation:** + +Global installation from NuGet: +```bash +dotnet tool install --global msidentity-app-sync +``` + +Build and install from repository: +```bash +cd tools/app-provisioning-tool +dotnet build +dotnet pack +dotnet tool install --global --add-source ./app-provisioning-tool/bin/Debug msidentity-app-sync +``` + +**Usage:** + +Run in your ASP.NET Core project folder: +```bash +msidentity-app-sync +``` + +The tool will: +- Detect your application configuration automatically +- Prompt for tenant and identity provider information if needed +- Create or update Azure AD app registration +- Update your application's configuration files + +**Parameters:** +- `--client-id ` - Use an existing app registration +- `--tenant-id ` - Specify the tenant +- Other parameters for customizing the provisioning process + +**Documentation:** See `app-provisioning-tool/README.md` for detailed usage instructions and `vs2019-16.9-how-to-use.md` for Visual Studio integration. + +**Use Case:** Streamlines the process of setting up Azure AD authentication in ASP.NET Core applications, particularly useful for developers and during project setup. + +--- + +## Summary + +| Tool | Type | Primary Use Case | +|------|------|------------------| +| **Check-BrokenLinks.ps1** | PowerShell Script | Documentation quality assurance - validates markdown links | +| **mark-shipped.ps1** | PowerShell Script | API management - tracks shipped vs unshipped API changes | +| **ConfigureGeneratedApplications** | .NET Console App | Project template configuration - sets up test applications | +| **GenerateMergeOptionsMethods** | .NET Console App | Code generation - creates property merge methods | +| **CrossPlatformValidator** | .NET Library | Authentication testing - validates JWT tokens cross-platform | +| **app-provisioning-tool** | .NET CLI Tool | Azure AD provisioning - automates app registration and config | + +--- + +## Contributing + +When adding new tools to this directory: +1. Ensure the tool has a clear, single purpose +2. Include documentation (README or inline comments) +3. Add an entry to this README describing the tool's purpose and usage +4. Consider whether the tool should be a standalone utility or integrated into the build process + +--- + +## Additional Resources + +- [Microsoft Identity Web Documentation](../README.md) +- [Contributing Guidelines](../CONTRIBUTING.md) +- [Testing Guide](../TESTING.md)