From 3e11a662e753a5380c44507f310cc46718d41a40 Mon Sep 17 00:00:00 2001 From: Jean-Marc Prieur Date: Thu, 29 Jan 2026 21:11:03 -0800 Subject: [PATCH 1/6] Adding an article for Aspire --- docs/README.md | 4 + docs/frameworks/aspire.md | 983 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 987 insertions(+) create mode 100644 docs/frameworks/aspire.md diff --git a/docs/README.md b/docs/README.md index 6c6a81e88..b21f2f925 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,6 +2,7 @@ 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 @@ -262,6 +263,9 @@ Microsoft.Identity.Web supports multiple ways to authenticate your application: - [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 diff --git a/docs/frameworks/aspire.md b/docs/frameworks/aspire.md new file mode 100644 index 000000000..95bc42987 --- /dev/null +++ b/docs/frameworks/aspire.md @@ -0,0 +1,983 @@ +# 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 +``` + +--- + +## 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.AddHttpClient(client => + client.BaseAddress = new("https+http://apiservice")) + .AddMicrosoftIdentityMessageHandler(builder.Configuration.GetSection("WeatherApi")); +// ... +app.UseAuthentication(); +app.UseAuthorization(); +``` + +**That's it!** The `MicrosoftIdentityMessageHandler` automatically acquires and attaches tokens. + +
+ +--- + +## 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, message handler | +| | `appsettings.json` | Azure AD config, downstream API scopes | +| | `.csproj` | Add `Microsoft.Identity.Web` | +| | `LoginLogoutEndpointRouteBuilderExtensions.cs` | Login/logout endpoints *(new file)* | +| | `Components/Layout/LogInOrOut.razor` | Login/logout UI *(new file)* | + +--- + +## 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 + C -->|4 id token + cookie + Access token| B + B -->|5 HTTP + Bearer token| D[ASP.NET API
MyService.ApiService
Microsoft.Identity.Web] + D -->|6 Validate JWT| C + D -->|7 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) + +
+ +--- + +## Prerequisites + +- **.NET 9 SDK** or later +- **Azure AD tenant** with two app registrations + +
+πŸ“‹ App Registration Details + +- **Web app** (Blazor): supports redirect URIs (configured to `{URL of the blazorapp}/signin-oidc`). For details see: + - [How to add a redirect URI to your application](https://learn.microsoft.com/entra/identity-platform/how-to-add-redirect-uri) +- **API app** (ApiService): exposes scopes (e.g., App ID URI is `api://`). For details, see: + - [Configure an application to expose a web API](https://learn.microsoft.com/entra/identity-platform/quickstart-configure-app-expose-web-apis) +- **Client credentials** (certificate or secret) for the web app registration. For details see: + - [Add and manage application credentials in Microsoft Entra ID](https://learn.microsoft.com/entra/identity-platform/how-to-add-credentials?tabs=certificate) + +
+ +> πŸ“š **New to Aspire?** See [.NET Aspire Overview](https://learn.microsoft.com/dotnet/aspire/get-started/aspire-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 + +**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 + +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 + consent handler +builder.Services.AddRazorComponents().AddInteractiveServerComponents(); +builder.Services.AddServerSideBlazor().AddMicrosoftIdentityConsentHandler(); + +builder.Services.AddOutputCache(); + +// 3) 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(); + +// 4) Login/Logout endpoints +app.MapGroup("/authentication").MapLoginAndLogout(); + +app.MapDefaultEndpoints(); +app.Run(); +``` + +
+πŸ”‘ Key Points Explained + +- **`AddMicrosoftIdentityWebApp`**: Configures OIDC authentication +- **`EnableTokenAcquisitionToCallDownstreamApi`**: Enables token acquisition for downstream APIs +- **`AddMicrosoftIdentityMessageHandler`**: Attaches bearer tokens to HttpClient requests automatically. Reads scopes from the `WeatherApi` configuration section. +- **`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: Add Login/Logout Endpoints + +**Create `MyService.Web/LoginLogoutEndpointRouteBuilderExtensions.cs`:** + +```csharp +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.Mvc; + +namespace MyService.Web; + +internal static class LoginLogoutEndpointRouteBuilderExtensions +{ + internal static IEndpointConventionBuilder MapLoginAndLogout(this IEndpointRouteBuilder endpoints) + { + var group = endpoints.MapGroup(""); + + group.MapGet("/login", (string? returnUrl) => TypedResults.Challenge(GetAuthProperties(returnUrl))) + .AllowAnonymous(); + + group.MapPost("/logout", ([FromForm] string? returnUrl) => TypedResults.SignOut(GetAuthProperties(returnUrl), + [CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectDefaults.AuthenticationScheme])); + + return group; + } + + private static AuthenticationProperties GetAuthProperties(string? returnUrl) + { + const string pathBase = "/"; + if (string.IsNullOrEmpty(returnUrl)) 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 }; + } +} +``` + +**Features:** +- `GET /authentication/login`: Initiates OIDC challenge +- `POST /authentication/logout`: Signs out of both cookie and OIDC schemes +- Prevents open redirects with URL validation + +### 2.5: Add Blazor UI Components + +
+πŸ“„ Create LogInOrOut.razor component + +**Create `MyService.Web/Components/Layout/LogInOrOut.razor`:** + +```razor +@implements IDisposable +@inject NavigationManager Navigation + + + +@code { + private string? currentUrl; + + protected override void OnInitialized() + { + currentUrl = Navigation.Uri; + Navigation.LocationChanged += OnLocationChanged; + } + + private void OnLocationChanged(object? sender, LocationChangedEventArgs e) + { + currentUrl = Navigation.Uri; + StateHasChanged(); + } + + public void Dispose() => Navigation.LocationChanged -= OnLocationChanged; +} +``` + +**Key Features:** +- ``: Renders different UI based on auth state +- Tracks `currentUrl` for proper post-login/logout redirection +- Implements `IDisposable` to clean up event handlers + +
+ +**Add to Navigation:** Include `` in your `NavMenu.razor` or `MainLayout.razor`. + +--- + +## 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 | + +### 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 scenarios where you need to handle consent in the UI: + +```csharp +// In Weather.razor or API client +try +{ + forecasts = await WeatherApi.GetAsync(); +} +catch (MicrosoftIdentityWebChallengeUserException ex) +{ + // Re-challenge user with additional claims + ConsentHandler.HandleException(ex); +} +``` + +### 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) + +
+ +--- + +**Last Updated:** January 2025 +**Solution:** MyService (.NET 9, Aspire, Microsoft.Identity.Web) From 2b75d10f3249e12df8930e8640b5bff8a811713f Mon Sep 17 00:00:00 2001 From: Jean-Marc Prieur Date: Thu, 29 Jan 2026 21:33:15 -0800 Subject: [PATCH 2/6] Update docs/frameworks/aspire.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/frameworks/aspire.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/frameworks/aspire.md b/docs/frameworks/aspire.md index 95bc42987..1226aeb67 100644 --- a/docs/frameworks/aspire.md +++ b/docs/frameworks/aspire.md @@ -817,7 +817,7 @@ public class WeatherApiClient _httpClient = httpClient; } - public async Task GetSensitiveDataAsync() + public async Task GetSensitiveDataAsync() { // Override scopes for this specific request var request = new HttpRequestMessage(HttpMethod.Get, "/weatherforecast") From e4c502ab7e683b7df841600d1d34bf757a7e3efa Mon Sep 17 00:00:00 2001 From: Jean-Marc Prieur Date: Thu, 29 Jan 2026 21:34:09 -0800 Subject: [PATCH 3/6] Update docs/frameworks/aspire.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/frameworks/aspire.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/frameworks/aspire.md b/docs/frameworks/aspire.md index 1226aeb67..702ff7127 100644 --- a/docs/frameworks/aspire.md +++ b/docs/frameworks/aspire.md @@ -302,7 +302,7 @@ app.MapGet("/weatherforecast", () => app.MapDefaultEndpoints(); app.Run(); -record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) +record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) { public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); } From b4c706c4b5009ed93e1b65157449050c50bbcaee Mon Sep 17 00:00:00 2001 From: Jean-Marc Prieur Date: Thu, 29 Jan 2026 22:07:39 -0800 Subject: [PATCH 4/6] Addressed PR feedback --- docs/frameworks/aspire.md | 54 +++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/docs/frameworks/aspire.md b/docs/frameworks/aspire.md index 702ff7127..471b1074e 100644 --- a/docs/frameworks/aspire.md +++ b/docs/frameworks/aspire.md @@ -14,6 +14,30 @@ aspire new aspire-starter --name MyService --- +## Prerequisites + +- **.NET 10 SDK** or later +- **.NET Aspire CLI** - See [Install Aspire CLI](https://aspire.dev/get-started/install-cli/) +- **Azure AD tenant** with two app registrations + +
+πŸ“‹ App Registration Details + +- **Web app** (Blazor): supports redirect URIs (configured to `{URL of the blazorapp}/signin-oidc`). For details see: + - [How to add a redirect URI to your application](https://learn.microsoft.com/entra/identity-platform/how-to-add-redirect-uri) +- **API app** (ApiService): exposes scopes (e.g., App ID URI is `api://`). For details, see: + - [Configure an application to expose a web API](https://learn.microsoft.com/entra/identity-platform/quickstart-configure-app-expose-web-apis) +- **Client credentials** (certificate or secret) for the web app registration. For details see: + - [Add and manage application credentials in Microsoft Entra ID](https://learn.microsoft.com/entra/identity-platform/how-to-add-credentials?tabs=certificate) and [Client credentials](../authentication/credentials/credentials-README.md) + +
+ +> πŸ“š **New to Aspire?** See [.NET Aspire Overview](https://learn.microsoft.com/dotnet/aspire/get-started/aspire-overview) + +--- + +> **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)
@@ -59,7 +83,7 @@ dotnet add package Microsoft.Identity.Web ```json { "AzureAd": { - "Instance": "https://login.microsoftonline.com", + "Instance": "https://login.microsoftonline.com/", "TenantId": "", "ClientId": "", "CallbackPath": "/signin-oidc", @@ -156,27 +180,6 @@ client.BaseAddress = new("https+http://apiservice"); --- -## Prerequisites - -- **.NET 9 SDK** or later -- **Azure AD tenant** with two app registrations - -
-πŸ“‹ App Registration Details - -- **Web app** (Blazor): supports redirect URIs (configured to `{URL of the blazorapp}/signin-oidc`). For details see: - - [How to add a redirect URI to your application](https://learn.microsoft.com/entra/identity-platform/how-to-add-redirect-uri) -- **API app** (ApiService): exposes scopes (e.g., App ID URI is `api://`). For details, see: - - [Configure an application to expose a web API](https://learn.microsoft.com/entra/identity-platform/quickstart-configure-app-expose-web-apis) -- **Client credentials** (certificate or secret) for the web app registration. For details see: - - [Add and manage application credentials in Microsoft Entra ID](https://learn.microsoft.com/entra/identity-platform/how-to-add-credentials?tabs=certificate) - -
- -> πŸ“š **New to Aspire?** See [.NET Aspire Overview](https://learn.microsoft.com/dotnet/aspire/get-started/aspire-overview) - ---- - ## Solution Structure ``` @@ -372,7 +375,7 @@ Add Azure AD configuration and downstream API scopes to `MyService.Web/appsettin ```json { "AzureAd": { - "Instance": "https://login.microsoftonline.com", + "Instance": "https://login.microsoftonline.com/", "Domain": ".onmicrosoft.com", "TenantId": "", "ClientId": "", @@ -566,7 +569,8 @@ internal static class LoginLogoutEndpointRouteBuilderExtensions { const string pathBase = "/"; if (string.IsNullOrEmpty(returnUrl)) returnUrl = pathBase; - else if (! Uri.IsWellFormedUriString(returnUrl, UriKind.Relative)) returnUrl = new Uri(returnUrl, UriKind.Absolute).PathAndQuery; + 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 }; } @@ -841,7 +845,7 @@ For production deployments in Azure, use managed identity instead of client secr ```json { "AzureAd": { - "Instance": "https://login.microsoftonline.com", + "Instance": "https://login.microsoftonline.com/", "TenantId": "", "ClientId": "", "ClientCredentials": [ From b4b373743595c52df73b9de33bfeea5602c70294 Mon Sep 17 00:00:00 2001 From: Jean-Marc Prieur Date: Thu, 29 Jan 2026 22:37:43 -0800 Subject: [PATCH 5/6] Adding the skill for adding Entra ID to Aspire --- .github/skills/README.md | 119 ++++++++ .../entra-id-aspire-authentication/SKILL.md | 280 ++++++++++++++++++ 2 files changed, 399 insertions(+) create mode 100644 .github/skills/README.md create mode 100644 .github/skills/entra-id-aspire-authentication/SKILL.md diff --git a/.github/skills/README.md b/.github/skills/README.md new file mode 100644 index 000000000..761715ee8 --- /dev/null +++ b/.github/skills/README.md @@ -0,0 +1,119 @@ +# 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 | +|-------|-------------| +| [entra-id-aspire-authentication](./entra-id-aspire-authentication/SKILL.md) | Adding Microsoft Entra ID authentication to .NET Aspire applications | + +## 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/SKILL.md b/.github/skills/entra-id-aspire-authentication/SKILL.md new file mode 100644 index 000000000..88c6ab86e --- /dev/null +++ b/.github/skills/entra-id-aspire-authentication/SKILL.md @@ -0,0 +1,280 @@ +--- +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 + +--- + +## Step-by-Step Implementation + +### Prerequisites + +1. Azure AD tenant with two app registrations: + - **Web app** (Blazor): with redirect URI `{app-url}/signin-oidc` + - **API app**: exposing scopes (App ID URI like `api://`) +2. Client credentials for the web app (secret or certificate) + +### 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(); +builder.Services.AddServerSideBlazor().AddMicrosoftIdentityConsentHandler(); + +// HttpClient with automatic token attachment +builder.Services.AddHttpClient(client => +{ + client.BaseAddress = new("https+http://apiservice"); // Aspire service discovery +}) +.AddMicrosoftIdentityMessageHandler(builder.Configuration.GetSection("WeatherApi")); + +var app = builder.Build(); + +app.UseAuthentication(); +app.UseAuthorization(); +app.UseAntiforgery(); + +app.MapRazorComponents().AddInteractiveServerRenderMode(); +app.MapGroup("/authentication").MapLoginAndLogout(); + +app.Run(); +``` + +**2.4 Create Login/Logout Endpoints (`LoginLogoutEndpointRouteBuilderExtensions.cs`):** +```csharp +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.Mvc; + +namespace MyService.Web; + +internal static class LoginLogoutEndpointRouteBuilderExtensions +{ + internal static IEndpointConventionBuilder MapLoginAndLogout(this IEndpointRouteBuilder endpoints) + { + var group = endpoints.MapGroup(""); + + group.MapGet("/login", (string? returnUrl) => TypedResults.Challenge(GetAuthProperties(returnUrl))) + .AllowAnonymous(); + + group.MapPost("/logout", ([FromForm] string? returnUrl) => TypedResults.SignOut(GetAuthProperties(returnUrl), + [CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectDefaults.AuthenticationScheme])); + + 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 }; + } +} +``` + +--- + +## 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")); +``` + +--- + +## 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 | + +--- + +## 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 + message handler | +| Web | `appsettings.json` | AzureAd config + downstream API scopes | + +--- + +## Resources + +- [Microsoft.Identity.Web Documentation](https://github.com/AzureAD/microsoft-identity-web/tree/master/docs) +- [Aspire Integration Guide](../../docs/frameworks/aspire.md) +- [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) From b045d0780c71d376f2313e6e4a03a20f2a9f1d04 Mon Sep 17 00:00:00 2001 From: Jean-Marc Prieur Date: Thu, 29 Jan 2026 23:08:20 -0800 Subject: [PATCH 6/6] Adding more links between article / skills / skill's readme --- .github/skills/README.md | 8 +++++--- .../skills/entra-id-aspire-authentication/SKILL.md | 2 +- docs/frameworks/aspire.md | 12 +++++++++++- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/.github/skills/README.md b/.github/skills/README.md index 761715ee8..fbd07c285 100644 --- a/.github/skills/README.md +++ b/.github/skills/README.md @@ -16,9 +16,11 @@ Skills work with multiple AI coding assistants that support the open skills form ## Available Skills -| Skill | Description | -|-------|-------------| -| [entra-id-aspire-authentication](./entra-id-aspire-authentication/SKILL.md) | Adding Microsoft Entra ID authentication to .NET Aspire applications | +| 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) | + +> **πŸ’‘ Tip:** Skills are condensed versions optimized for AI assistants. For comprehensive documentation with detailed explanations, diagrams, and troubleshooting, see the linked full guides. ## How to Use Skills diff --git a/.github/skills/entra-id-aspire-authentication/SKILL.md b/.github/skills/entra-id-aspire-authentication/SKILL.md index 88c6ab86e..a087da26a 100644 --- a/.github/skills/entra-id-aspire-authentication/SKILL.md +++ b/.github/skills/entra-id-aspire-authentication/SKILL.md @@ -273,8 +273,8 @@ builder.Services.AddDownstreamApi("GraphApi", builder.Configuration.GetSection(" ## 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) -- [Aspire Integration Guide](../../docs/frameworks/aspire.md) - [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/docs/frameworks/aspire.md b/docs/frameworks/aspire.md index 471b1074e..74e93b30b 100644 --- a/docs/frameworks/aspire.md +++ b/docs/frameworks/aspire.md @@ -983,5 +983,15 @@ builder.Services.AddHttpClient(client => --- +## πŸ€– AI Coding Assistant Skill + +A condensed version of this guide is available as an **AI Skill** for GitHub Copilot, Claude, and other AI coding assistants. The skill helps AI assistants implement this authentication pattern in your Aspire projects. + +πŸ“ **Location:** [.github/skills/entra-id-aspire-authentication/SKILL.md](../../.github/skills/entra-id-aspire-authentication/SKILL.md) + +See the [Skills README](../../.github/skills/README.md) for installation instructions. + +--- + **Last Updated:** January 2025 -**Solution:** MyService (.NET 9, Aspire, Microsoft.Identity.Web) +**Solution:** MyService (.NET 10, Aspire, Microsoft.Identity.Web)