{"id":269,"date":"2026-02-05T10:05:00","date_gmt":"2026-02-05T18:05:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/aspire\/?p=269"},"modified":"2026-02-04T18:42:12","modified_gmt":"2026-02-05T02:42:12","slug":"securing-dotnet-aspire-apps-with-microsoft-entra-id","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/aspire\/securing-dotnet-aspire-apps-with-microsoft-entra-id\/","title":{"rendered":"Securing Aspire Apps with Microsoft Entra ID"},"content":{"rendered":"<p><div class=\"alert alert-info\"><p class=\"alert-divider\"><i class=\"fabric-icon fabric-icon--Info\"><\/i><strong>About the Author<\/strong><\/p>This is a guest post from Jean-Marc Prieur from Microsoft&#8217;s 1ES (One Engineering System) team. We help reduce developer toil across Microsoft by making it easy to build services that are secure and compliant from the start, for instance with Aspire and Entra ID.<\/div><\/p>\n<p><strong>TL;DR:<\/strong> When you create an Aspire app with <code>aspire new aspire-starter<\/code>, you get a Blazor frontend and API backend\u2014but no authentication. This post shows two ways to fix that: <strong>the quick way<\/strong> (using AI skills with GitHub Copilot) and <strong>the detailed way<\/strong> (understanding every line of code).<\/p>\n<h2>The problem<\/h2>\n<p>You&#8217;ve just created your first Aspire application:<\/p>\n<pre><code class=\"language-bash\">aspire new aspire-starter --name MyService<\/code><\/pre>\n<p>You&#8217;ve got a beautiful Blazor frontend talking to an API backend, all orchestrated by Aspire. But there&#8217;s a problem: <strong>anyone can access your API<\/strong>. No authentication. No authorization.<\/p>\n<p>The questions pile up:<\/p>\n<ul>\n<li>How do I sign users in?<\/li>\n<li>How do I protect my APIs?<\/li>\n<li>How do I pass tokens between services?<\/li>\n<li>How does this work with Aspire&#8217;s service discovery?<\/li>\n<\/ul>\n<h2>The quick way: AI skills (5 minutes)<\/h2>\n<p>If you&#8217;re using <strong>GitHub Copilot<\/strong>, <strong>Claude<\/strong>, or another AI coding assistant, you can add Entra ID authentication in minutes using <strong>AI skills<\/strong>\u2014an open standard for sharing domain-specific knowledge with AI assistants.<\/p>\n<h3>Overview<\/h3>\n<p>Watch the live demo of the Aspire+Entra ID agent skills from February 3rd Microsoft Reactor episode <strong>How Microsoft uses Agentic AI to accelerate software delivery<\/strong>.<\/p>\n<p><a href=\"https:\/\/youtu.be\/jvzPLZQQD3A?t=2262\"><img decoding=\"async\" src=\"https:\/\/img.youtube.com\/vi\/jvzPLZQQD3A\/hqdefault.jpg\" alt=\"How Microsoft uses Agentic AI to accelerate software delivery\" \/><\/a><\/p>\n<p>If you want to try yourself:<\/p>\n<h3>Step 1: Add the skills to your repo<\/h3>\n<pre><code class=\"language-bash\"># From your repo root\nmkdir -p .github\/skills # or .copilot\/skills or what your coding agent understand\ncd .github\/skills\n\n# Download the skills (or copy the  repo)\ncurl -O https:\/\/aka.ms\/msidweb\/aspire\/entra-id-code-skill\ncurl -O https:\/\/aka.ms\/msidweb\/aspire\/entra-id-provisioning-skill<\/code><\/pre>\n<p>Or simply copy the <code>entra-id-aspire-authentication<\/code> and <code>entra-id-aspire-provisioning<\/code> folders from the <a href=\"https:\/\/aka.ms\/EntraIdAgentSkills\">Entra ID aspire skills on github<\/a>.<\/p>\n<h3>Step 2: Ask your AI assistant<\/h3>\n<p>Open GitHub Copilot Chat and say:<\/p>\n<blockquote>\n<p><strong>You:<\/strong> &#8220;Add Entra ID authentication to my Aspire app&#8221;<\/p>\n<\/blockquote>\n<p><strong>Copilot<\/strong> will detect the skill and walk you through it:<\/p>\n<blockquote>\n<p><strong>Copilot:<\/strong> &#8220;I detected your Aspire solution with:<\/p>\n<ul>\n<li><strong>Web App<\/strong> (Blazor): <code>MyService.Web<\/code><\/li>\n<li><strong>API<\/strong>: <code>MyService.ApiService<\/code><\/li>\n<\/ul>\n<p>I&#8217;ll implement authentication in two phases:<\/p>\n<p><strong>Phase 1 (now):<\/strong> Add authentication code with placeholder values. The app will <strong>build<\/strong> but won&#8217;t <strong>run<\/strong> until app registrations are configured.<\/p>\n<p><strong>Phase 2 (after):<\/strong> Use the provisioning skill to create Entra ID app registrations.<\/p>\n<p>Ready to proceed?&#8221;<\/p>\n<p><strong>You:<\/strong> &#8220;Yes&#8221;<\/p>\n<\/blockquote>\n<p>Copilot then:<\/p>\n<ol>\n<li>Adds <code>Microsoft.Identity.Web<\/code> packages<\/li>\n<li>Updates <code>appsettings.json<\/code> with Azure AD config (placeholders)<\/li>\n<li>Updates <code>Program.cs<\/code> in both projects<\/li>\n<li>Copies helper files for login\/logout and Blazor auth handling<\/li>\n<li>Creates the login button UI component<\/li>\n<li>Adds proper exception handling for Conditional Access<\/li>\n<\/ol>\n<h3>Step 3: Provision app registrations<\/h3>\n<blockquote>\n<p><strong>Copilot:<\/strong> &#8220;I&#8217;m now ready to provision the Entra ID apps. Shall I proceed?&#8221;<\/p>\n<p><strong>You:<\/strong> &#8220;Yes please!&#8221;<\/p>\n<p><strong>Copilot:<\/strong> &#8220;I&#8217;ll use Microsoft Graph PowerShell to create the app registrations. First, let me check your tenant connection&#8230;&#8221;<\/p>\n<\/blockquote>\n<p>Copilot runs PowerShell commands to:<\/p>\n<ol>\n<li>Create the API app registration with <code>access_as_user<\/code> scope<\/li>\n<li>Create the Web app registration with redirect URIs<\/li>\n<li>Generate a client secret and store it in user-secrets<\/li>\n<li>Update your <code>appsettings.json<\/code> with real values<\/li>\n<\/ol>\n<h3>Step 4: Run and test<\/h3>\n<pre><code class=\"language-bash\">aspire run<\/code><\/pre>\n<p>Open the Aspire dashboard, navigate to your Blazor app, and click <strong>Login<\/strong>. That&#8217;s it! \ud83c\udf89<\/p>\n<hr \/>\n<h2>What just happened?<\/h2>\n<p>The skills automated what would take at least 30-60 minutes manually, or even days without the detailed instructions:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/aspire\/wp-content\/uploads\/sites\/90\/2026\/02\/aspire-app-architecture.webp\" alt=\"Aspire app architecture\" \/><\/p>\n<p><strong>The flow:<\/strong><\/p>\n<ol>\n<li>User visits Blazor app \u2192 redirected to Entra ID<\/li>\n<li>User signs in \u2192 app receives tokens + establishes session<\/li>\n<li>User requests data \u2192 <code>MicrosoftIdentityMessageHandler<\/code> automatically acquires access token<\/li>\n<li>Token attached to API call \u2192 API validates JWT \u2192 returns data<\/li>\n<\/ol>\n<hr \/>\n<h2>The detailed way: understanding the code<\/h2>\n<p>Want to know what the skills actually did? Or prefer to implement manually? Here&#8217;s the simplified breakdown. The full article is available from <a href=\"https:\/\/aka.ms\/ms-id-web\/aspire\">Manually add Entra ID authentication and authorization to an Aspire App<\/a><\/p>\n<h3>Files modified<\/h3>\n<table>\n<thead>\n<tr>\n<th>\n        Project\n      <\/th>\n<th>\n        File\n      <\/th>\n<th>\n        Changes\n      <\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>\n        <strong>ApiService<\/strong>\n      <\/td>\n<td>\n        <code>Program.cs<\/code>\n      <\/td>\n<td>\n        JWT Bearer auth, authorization middleware\n      <\/td>\n<\/tr>\n<tr>\n<td>\n      <\/td>\n<td>\n        <code>appsettings.json<\/code>\n      <\/td>\n<td>\n        Azure AD configuration\n      <\/td>\n<\/tr>\n<tr>\n<td>\n        <strong>Web<\/strong>\n      <\/td>\n<td>\n        <code>Program.cs<\/code>\n      <\/td>\n<td>\n        OIDC auth, token acquisition, message handler\n      <\/td>\n<\/tr>\n<tr>\n<td>\n      <\/td>\n<td>\n        <code>appsettings.json<\/code>\n      <\/td>\n<td>\n        Azure AD config, downstream API scopes\n      <\/td>\n<\/tr>\n<tr>\n<td>\n      <\/td>\n<td>\n        <code>LoginLogoutEndpointRouteBuilderExtensions.cs<\/code>\n      <\/td>\n<td>\n        Login\/logout with incremental consent <em>(copy from skill)<\/em>\n      <\/td>\n<\/tr>\n<tr>\n<td>\n      <\/td>\n<td>\n        <code>BlazorAuthenticationChallengeHandler.cs<\/code>\n      <\/td>\n<td>\n        Auth challenge handler <em>(copy from skill)<\/em>\n      <\/td>\n<\/tr>\n<tr>\n<td>\n      <\/td>\n<td>\n        <code>Components\/UserInfo.razor<\/code>\n      <\/td>\n<td>\n        <strong>Login button UI<\/strong> <em>(new)<\/em>\n      <\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2>Part 1: Protecting the API<\/h2>\n<p>Let&#8217;s start with the API since it&#8217;s simpler. We need to validate incoming JWT tokens.<\/p>\n<h3>Add Microsoft.Identity.Web to the API<\/h3>\n<pre><code class=\"language-bash\">cd MyService.ApiService\ndotnet add package Microsoft.Identity.Web<\/code><\/pre>\n<h3>Configure Azure AD settings<\/h3>\n<p>Add to <code>appsettings.json<\/code>:<\/p>\n<pre><code class=\"language-json\">{\n  \"AzureAd\": {\n    \"Instance\": \"https:\/\/login.microsoftonline.com\/\",\n    \"TenantId\": \"YOUR_TENANT_ID\",\n    \"ClientId\": \"YOUR_API_CLIENT_ID\",\n    \"Audiences\": [\"api:\/\/YOUR_API_CLIENT_ID\"]\n  }\n}<\/code><\/pre>\n<h3>Wire up authentication<\/h3>\n<p>Update <code>Program.cs<\/code>:<\/p>\n<pre><code class=\"language-csharp\">using Microsoft.AspNetCore.Authentication.JwtBearer;\nusing Microsoft.Identity.Web;\n\nvar builder = WebApplication.CreateBuilder(args);\nbuilder.AddServiceDefaults();\n\n\/\/ \ud83d\udc47 Add this: JWT Bearer authentication\nbuilder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)\n    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection(\"AzureAd\"));\n\nbuilder.Services.AddAuthorization();\n\nvar app = builder.Build();\n\n\/\/ \ud83d\udc47 Add middleware (order matters!)\napp.UseAuthentication();\napp.UseAuthorization();\n\n\/\/ \ud83d\udc47 Protect your endpoints\napp.MapGet(\"\/weatherforecast\", () =&gt;\n{\n    \/\/ ... your logic\n})\n.RequireAuthorization();\n\napp.Run();<\/code><\/pre>\n<p><strong>That&#8217;s it for the API.<\/strong> Without a valid token, callers get a 401.<\/p>\n<h2>Part 2: Blazor frontend authentication<\/h2>\n<p>This is where the magic happens. We need to:<\/p>\n<ol>\n<li>Sign users in with OIDC<\/li>\n<li>Acquire access tokens for the API<\/li>\n<li>Automatically attach tokens to HTTP requests<\/li>\n<\/ol>\n<h3>Add Microsoft.Identity.Web<\/h3>\n<pre><code class=\"language-bash\">cd MyService.Web\ndotnet add package Microsoft.Identity.Web<\/code><\/pre>\n<h3>Configure Azure AD + downstream API<\/h3>\n<p>Add to <code>appsettings.json<\/code>:<\/p>\n<pre><code class=\"language-json\">{\n  \"AzureAd\": {\n    \"Instance\": \"https:\/\/login.microsoftonline.com\",\n    \"Domain\": \"yourtenant.onmicrosoft.com\",\n    \"TenantId\": \"YOUR_TENANT_ID\",\n    \"ClientId\": \"YOUR_WEB_APP_CLIENT_ID\",\n    \"CallbackPath\": \"\/signin-oidc\",\n    \"ClientCredentials\": [\n      {\n        \"SourceType\": \"ClientSecret\",\n        \"ClientSecret\": \"YOUR_SECRET\"\n      }\n    ]\n  },\n  \"WeatherApi\": {\n    \"Scopes\": [\"api:\/\/YOUR_API_CLIENT_ID\/.default\"]\n  }\n}<\/code><\/pre>\n<blockquote>\n<p>\u26a0\ufe0f <strong>Production tip:<\/strong> Use managed identity or certificates instead of client secrets. See the <a href=\"https:\/\/github.com\/AzureAD\/microsoft-identity-web\/blob\/master\/docs\/authentication\/credentials\/certificateless.md\">certificateless authentication guide<\/a>.<\/p>\n<\/blockquote>\n<h3>Wire up authentication + token acquisition<\/h3>\n<p>Here&#8217;s the full <code>Program.cs<\/code>:<\/p>\n<pre><code class=\"language-csharp\">using Microsoft.AspNetCore.Authentication.OpenIdConnect;\nusing Microsoft.Identity.Web;\n\nvar builder = WebApplication.CreateBuilder(args);\nbuilder.AddServiceDefaults();\n\n\/\/ 1\ufe0f\u20e3 OIDC authentication + token acquisition\nbuilder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)\n    .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection(\"AzureAd\"))\n    .EnableTokenAcquisitionToCallDownstreamApi()\n    .AddInMemoryTokenCaches();\n\nbuilder.Services.AddCascadingAuthenticationState();\nbuilder.Services.AddRazorComponents().AddInteractiveServerComponents();\n\n\/\/ \ud83d\udc47 Handles incremental consent & Conditional Access in Blazor Server\nbuilder.Services.AddScoped&lt;BlazorAuthenticationChallengeHandler&gt;();\n\n\/\/ 2\ufe0f\u20e3 HttpClient with automatic token attachment\nbuilder.Services.AddHttpClient&lt;WeatherApiClient&gt;(client =&gt;\n{\n    \/\/ Aspire service discovery! \ud83c\udf89\n    client.BaseAddress = new(\"https+http:\/\/apiservice\");\n})\n.AddMicrosoftIdentityMessageHandler(builder.Configuration.GetSection(\"WeatherApi\"));\n\nvar app = builder.Build();\n\napp.UseAuthentication();\napp.UseAuthorization();\napp.UseAntiforgery();\n\napp.MapRazorComponents&lt;App&gt;().AddInteractiveServerRenderMode();\napp.MapGroup(\"\/authentication\").MapLoginAndLogout();\n\napp.Run();<\/code><\/pre>\n<h3>The magic: MicrosoftIdentityMessageHandler<\/h3>\n<p>The key insight is <code>.AddMicrosoftIdentityMessageHandler()<\/code>. This DelegatingHandler:<\/p>\n<ol>\n<li><strong>Intercepts outgoing HTTP requests<\/strong><\/li>\n<li><strong>Checks the token cache<\/strong> for a valid access token<\/li>\n<li><strong>Silently acquires a new token<\/strong> if needed (using refresh token)<\/li>\n<li><strong>Attaches the <code>Authorization: Bearer<\/code> header<\/strong><\/li>\n<li><strong>Handles Conditional Access challenges<\/strong> automatically<\/li>\n<\/ol>\n<p>You write normal HttpClient code. Tokens just appear. \u2728<\/p>\n<p>\ud83d\udcda <strong>Deep dive:<\/strong> <a href=\"https:\/\/github.com\/AzureAD\/microsoft-identity-web\/blob\/master\/docs\/calling-downstream-apis\/custom-apis.md#microsoftidentitymessagehandler---for-httpclient-integration\">MicrosoftIdentityMessageHandler documentation<\/a><\/p>\n<pre><code class=\"language-csharp\">public class WeatherApiClient(HttpClient httpClient)\n{\n    public async Task&lt;WeatherForecast[]?&gt; GetForecastAsync()\n    {\n        \/\/ No token handling code needed!\n        return await httpClient.GetFromJsonAsync&lt;WeatherForecast[]&gt;(\"\/weatherforecast\");\n    }\n}<\/code><\/pre>\n<h2>The Aspire advantage<\/h2>\n<p>Notice how we&#8217;re using Aspire&#8217;s service discovery:<\/p>\n<pre><code class=\"language-csharp\">client.BaseAddress = new(\"https+http:\/\/apiservice\");<\/code><\/pre>\n<p>At runtime, Aspire resolves <code>\"apiservice\"<\/code> to the actual endpoint. This works seamlessly whether you&#8217;re running:<\/p>\n<ul>\n<li>Locally with the Aspire dashboard<\/li>\n<li>In Docker containers<\/li>\n<li>In Kubernetes<\/li>\n<li>In Azure Container Apps<\/li>\n<\/ul>\n<p><strong>Zero hardcoded URLs. Zero environment-specific config.<\/strong><\/p>\n<h2>Adding login\/logout UI<\/h2>\n<p>The login\/logout endpoints need to support <strong>incremental consent<\/strong> and <strong>Conditional Access<\/strong>. Copy these helper files from the <a href=\"https:\/\/aka.ms\/ms-id-web\/aspire-skill\">skill folder<\/a> or use the code below:<\/p>\n<pre><code class=\"language-csharp\">\/\/ LoginLogoutEndpointRouteBuilderExtensions.cs\nusing Microsoft.AspNetCore.Authentication;\nusing Microsoft.AspNetCore.Authentication.Cookies;\nusing Microsoft.AspNetCore.Authentication.OpenIdConnect;\nusing Microsoft.IdentityModel.Protocols.OpenIdConnect;\n\nnamespace Microsoft.Identity.Web;\n\npublic static class LoginLogoutEndpointRouteBuilderExtensions\n{\n    public static IEndpointConventionBuilder MapLoginAndLogout(\n        this IEndpointRouteBuilder endpoints)\n    {\n        var group = endpoints.MapGroup(\"\");\n\n        \/\/ Enhanced login with incremental consent support\n        group.MapGet(\"\/login\", (\n            string? returnUrl,\n            string? scope,      \/\/ For incremental consent\n            string? loginHint,  \/\/ Pre-fill username\n            string? domainHint, \/\/ Skip home realm discovery\n            string? claims) =&gt;  \/\/ Conditional Access\n        {\n            var properties = GetAuthProperties(returnUrl);\n\n            if (!string.IsNullOrEmpty(scope))\n            {\n                var scopes = scope.Split(' ', StringSplitOptions.RemoveEmptyEntries);\n                properties.SetParameter(OpenIdConnectParameterNames.Scope, scopes);\n            }\n            if (!string.IsNullOrEmpty(loginHint))\n                properties.SetParameter(OpenIdConnectParameterNames.LoginHint, loginHint);\n            if (!string.IsNullOrEmpty(domainHint))\n                properties.SetParameter(OpenIdConnectParameterNames.DomainHint, domainHint);\n            if (!string.IsNullOrEmpty(claims))\n                properties.Items[\"claims\"] = claims;\n\n            return TypedResults.Challenge(properties, [OpenIdConnectDefaults.AuthenticationScheme]);\n        }).AllowAnonymous();\n\n        group.MapPost(\"\/logout\", async (HttpContext context) =&gt;\n        {\n            string? returnUrl = null;\n            if (context.Request.HasFormContentType)\n            {\n                var form = await context.Request.ReadFormAsync();\n                returnUrl = form[\"ReturnUrl\"];\n            }\n            return TypedResults.SignOut(GetAuthProperties(returnUrl),\n                [CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectDefaults.AuthenticationScheme]);\n        }).DisableAntiforgery();\n\n        return group;\n    }\n\n    private static AuthenticationProperties GetAuthProperties(string? returnUrl)\n    {\n        const string pathBase = \"\/\";\n        if (string.IsNullOrEmpty(returnUrl)) returnUrl = pathBase;\n        else if (returnUrl.StartsWith(\"\/\/\", StringComparison.Ordinal)) returnUrl = pathBase;\n        else if (!Uri.IsWellFormedUriString(returnUrl, UriKind.Relative)) returnUrl = new Uri(returnUrl, UriKind.Absolute).PathAndQuery;\n        else if (returnUrl[0] != '\/') returnUrl = $\"{pathBase}{returnUrl}\";\n        return new AuthenticationProperties { RedirectUri = returnUrl };\n    }\n}<\/code><\/pre>\n<p>Then add a Blazor component for the login button (<code>Components\/UserInfo.razor<\/code>):<\/p>\n<pre><code class=\"language-razor\">@using Microsoft.AspNetCore.Components.Authorization\n\n&lt;AuthorizeView&gt;\n    &lt;Authorized&gt;\n        &lt;span&gt;Hello, @context.User.Identity?.Name&lt;\/span&gt;\n        &lt;form action=\"\/authentication\/logout\" method=\"post\"&gt;\n            &lt;AntiforgeryToken \/&gt;\n            &lt;button type=\"submit\"&gt;Logout&lt;\/button&gt;\n        &lt;\/form&gt;\n    &lt;\/Authorized&gt;\n    &lt;NotAuthorized&gt;\n        &lt;a href=\"\/authentication\/login?returnUrl=\/\"&gt;Login&lt;\/a&gt;\n    &lt;\/NotAuthorized&gt;\n&lt;\/AuthorizeView&gt;<\/code><\/pre>\n<h2>Handling consent &amp; Conditional Access<\/h2>\n<blockquote>\n<p>\u26a0\ufe0f <strong>This is critical for Blazor Server!<\/strong> You must handle exceptions on pages that call APIs.<\/p>\n<\/blockquote>\n<p>The <code>BlazorAuthenticationChallengeHandler<\/code> (copy from skill folder) handles <code>MicrosoftIdentityWebChallengeUserException<\/code>. Use it on <strong>every page calling a protected API<\/strong>:<\/p>\n<pre><code class=\"language-razor\">@page \"\/weather\"\n@attribute [Authorize]\n@inject WeatherApiClient WeatherApi\n@inject BlazorAuthenticationChallengeHandler ChallengeHandler\n\n@code {\n    private WeatherForecast[]? forecasts;\n    private string? errorMessage;\n\n    protected override async Task OnInitializedAsync()\n    {\n        if (!await ChallengeHandler.IsAuthenticatedAsync())\n        {\n            await ChallengeHandler.ChallengeUserWithConfiguredScopesAsync(\"WeatherApi:Scopes\");\n            return;\n        }\n\n        try\n        {\n            forecasts = await WeatherApi.GetForecastAsync();\n        }\n        catch (Exception ex)\n        {\n            \/\/ Handles incremental consent \/ Conditional Access\n            if (!await ChallengeHandler.HandleExceptionAsync(ex))\n            {\n                errorMessage = $\"Error: {ex.Message}\";\n            }\n        }\n    }\n}<\/code><\/pre>\n<h2>Common scenarios<\/h2>\n<h3>Protecting Blazor pages<\/h3>\n<pre><code class=\"language-razor\">@page \"\/weather\"\n@attribute [Authorize]\n\n&lt;!-- Only authenticated users see this --&gt;<\/code><\/pre>\n<h3>Scope validation in the API<\/h3>\n<p>Ensure tokens have specific scopes:<\/p>\n<pre><code class=\"language-csharp\">app.MapGet(\"\/sensitive-data\", () =&gt; { \/* ... *\/ })\n    .RequireAuthorization()\n    .RequireScope(\"data.read\");<\/code><\/pre>\n<h3>Service-to-service (no user)<\/h3>\n<p>For daemon scenarios:<\/p>\n<pre><code class=\"language-csharp\">.AddMicrosoftIdentityMessageHandler(options =&gt;\n{\n    options.Scopes.Add(\"api:\/\/client-id\/.default\");\n    options.RequestAppToken = true; \/\/ App-only token\n});<\/code><\/pre>\n<h3>Per-request scope override<\/h3>\n<pre><code class=\"language-csharp\">var request = new HttpRequestMessage(HttpMethod.Get, \"\/admin-endpoint\")\n    .WithAuthenticationOptions(options =&gt;\n    {\n        options.Scopes.Clear();\n        options.Scopes.Add(\"api:\/\/client-id\/admin.write\");\n    });<\/code><\/pre>\n<hr \/>\n<h2>Troubleshooting tips<\/h2>\n<table>\n<thead>\n<tr>\n<th>Symptom<\/th>\n<th>Check This<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>401 Unauthorized<\/td>\n<td>Are scopes correct? Does the token audience match?<\/td>\n<\/tr>\n<tr>\n<td>OIDC redirect fails<\/td>\n<td>Is <code>\/signin-oidc<\/code> in your app registration&#8217;s redirect URIs?<\/td>\n<\/tr>\n<tr>\n<td>Token not attached<\/td>\n<td>Is <code>AddMicrosoftIdentityMessageHandler<\/code> on the HttpClient?<\/td>\n<\/tr>\n<tr>\n<td>AADSTS65001<\/td>\n<td>Admin consent needed\u2014grant it in Azure Portal<\/td>\n<\/tr>\n<tr>\n<td>No login button<\/td>\n<td>Is <code>UserInfo.razor<\/code> included in your layout?<\/td>\n<\/tr>\n<tr>\n<td>404 on <code>\/MicrosoftIdentity\/Account\/Challenge<\/code><\/td>\n<td>Use <code>BlazorAuthenticationChallengeHandler<\/code> instead of old <code>MicrosoftIdentityConsentHandler<\/code><\/td>\n<\/tr>\n<tr>\n<td>Consent loop<\/td>\n<td>Add try\/catch with <code>HandleExceptionAsync<\/code> on all API-calling pages<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><strong>Pro tip:<\/strong> Enable logging to see token acquisition details:<\/p>\n<pre><code class=\"language-csharp\">builder.Services.AddLogging(options =&gt;\n{\n    options.AddFilter(\"Microsoft.Identity\", LogLevel.Debug);\n});<\/code><\/pre>\n<h2>Production checklist<\/h2>\n<p>Before going live:<\/p>\n<ul>\n<li>[ ] Replace client secrets with managed identity or certificates<\/li>\n<li>[ ] Configure distributed token cache (Redis, SQL) instead of in-memory<\/li>\n<li>[ ] Set up proper redirect URIs for all deployment environments<\/li>\n<li>[ ] Grant admin consent for required scopes<\/li>\n<li>[ ] Enable Conditional Access policies as needed<\/li>\n<li>[ ] Ensure <code>BlazorAuthenticationChallengeHandler<\/code> is used.<\/li>\n<li>[ ] Add try\/catch with <code>HandleExceptionAsync<\/code> on all pages calling APIs<\/li>\n<\/ul>\n<h2>Conclusion<\/h2>\n<p>Securing Aspire apps with Entra ID has never been easier:<\/p>\n<p><strong>\ud83d\ude80 The quick way:<\/strong> Use the AI skills with GitHub Copilot or Claude. Add authentication in 5 minutes with a conversation.<\/p>\n<p><strong>\ud83d\udcd6 The detailed way:<\/strong> Understand every line by following the manual implementation guide.<\/p>\n<p>Either way, you get:<\/p>\n<ul>\n<li><strong>API protection<\/strong>: JWT Bearer authentication with 5 lines of code<\/li>\n<li><strong>User sign-in<\/strong>: Standard OIDC with automatic token acquisition<\/li>\n<li><strong>Service calls<\/strong>: Tokens attached automatically via <code>MicrosoftIdentityMessageHandler<\/code><\/li>\n<li><strong>Aspire integration<\/strong>: Works seamlessly with service discovery<\/li>\n<\/ul>\n<p>The combination of Aspire&#8217;s orchestration, Microsoft.Identity.Web&#8217;s auth handling, and AI skills means you can go from zero to authenticated in minutes.<\/p>\n<p><strong>\ud83d\udcd6 Full guide:<\/strong> <a href=\"https:\/\/aka.ms\/ms-id-web\/aspire\">Aspire Integration Guide<\/a> \u2014 Comprehensive documentation with all configuration options and advanced scenarios.<\/p>\n<hr \/>\n<h2>Resources<\/h2>\n<ul>\n<li>\ud83d\udcd6 <a href=\"https:\/\/aka.ms\/ms-id-web\/aspire\">Full Aspire Integration Guide<\/a><\/li>\n<li>\ud83e\udd16 <a href=\"https:\/\/aka.ms\/msidweb\/aspire\/entra-id-code-skill\">AI Skill: Authentication<\/a><\/li>\n<li>\ud83e\udd16 <a href=\"https:\/\/aka.ms\/msidweb\/aspire\/entra-id-provisioning-skill\">AI Skill: Provisioning<\/a><\/li>\n<li>\ud83d\udcd8 <a href=\"https:\/\/aka.ms\/ms-identity-web\">Microsoft.Identity.Web Documentation<\/a><\/li>\n<li>\ud83d\udcd8 <a href=\"https:\/\/learn.microsoft.com\/dotnet\/aspire\/get-started\/aspire-overview\">Aspire Overview<\/a><\/li>\n<li>\ud83d\udd10 <a href=\"https:\/\/aka.ms\/ms-id-web\/credentials\">Credentials Guide<\/a><\/li>\n<li>\ud83c\udfab <a href=\"https:\/\/learn.microsoft.com\/entra\/identity-platform\/\">Microsoft Identity Platform<\/a><\/li>\n<\/ul>\n<hr \/>\n<h2>Summary<\/h2>\n<p>You&#8217;ve learned two ways to secure your Aspire apps with Entra ID:<\/p>\n<ol>\n<li><strong>The quick way<\/strong> \u2014 AI skills automate the entire process<\/li>\n<li><strong>The detailed way<\/strong> \u2014 Understand exactly what&#8217;s happening under the hood<\/li>\n<\/ol>\n<p><em>Try the AI skills in your next project! Have questions? Found an issue? Let me know in the comments or open an issue on the <a href=\"https:\/\/github.com\/AzureAD\/microsoft-identity-web\">repo<\/a>!<\/em><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Learn to secure Aspire distributed applications with Microsoft Entra ID authentication. Use AI skills with GitHub Copilot for a 5-minute setup, or follow the detailed guide to understand JWT Bearer protection, OIDC sign-in, and automatic token handling.<\/p>\n","protected":false},"author":764,"featured_media":270,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1,17,19],"tags":[49,9,41,42,45,46,39,50,44,40,43,48,47],"class_list":["post-269","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-aspire-category","category-deep-dives","category-integrations","tag-ai-skills","tag-aspire","tag-authentication","tag-authorization","tag-blazor-server","tag-distributed-applications","tag-entra-id","tag-github-copilot","tag-jwt","tag-microsoft-identity-web","tag-oidc","tag-service-discovery","tag-token-acquisition"],"acf":[],"blog_post_summary":"<p>Learn to secure Aspire distributed applications with Microsoft Entra ID authentication. Use AI skills with GitHub Copilot for a 5-minute setup, or follow the detailed guide to understand JWT Bearer protection, OIDC sign-in, and automatic token handling.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/posts\/269","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/users\/764"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/comments?post=269"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/posts\/269\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/media\/270"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/media?parent=269"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/categories?post=269"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/aspire\/wp-json\/wp\/v2\/tags?post=269"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}