About the Author
This is a guest post from Jean-Marc Prieur from Microsoft’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.TL;DR: When you create an Aspire app with aspire new aspire-starter, you get a Blazor frontend and API backend—but no authentication. This post shows two ways to fix that: the quick way (using AI skills with GitHub Copilot) and the detailed way (understanding every line of code).
The problem
You’ve just created your first Aspire application:
aspire new aspire-starter --name MyService
You’ve got a beautiful Blazor frontend talking to an API backend, all orchestrated by Aspire. But there’s a problem: anyone can access your API. No authentication. No authorization.
The questions pile up:
- How do I sign users in?
- How do I protect my APIs?
- How do I pass tokens between services?
- How does this work with Aspire’s service discovery?
The quick way: AI skills (5 minutes)
If you’re using GitHub Copilot, Claude, or another AI coding assistant, you can add Entra ID authentication in minutes using AI skills—an open standard for sharing domain-specific knowledge with AI assistants.
Overview
Watch the live demo of the Aspire+Entra ID agent skills from February 3rd Microsoft Reactor episode How Microsoft uses Agentic AI to accelerate software delivery.
If you want to try yourself:
Step 1: Add the skills to your repo
# From your repo root
mkdir -p .github/skills # or .copilot/skills or what your coding agent understand
cd .github/skills
# Download the skills (or copy the repo)
curl -O https://aka.ms/msidweb/aspire/entra-id-code-skill
curl -O https://aka.ms/msidweb/aspire/entra-id-provisioning-skill
Or simply copy the entra-id-aspire-authentication and entra-id-aspire-provisioning folders from the Entra ID aspire skills on github.
Step 2: Ask your AI assistant
Open GitHub Copilot Chat and say:
You: “Add Entra ID authentication to my Aspire app”
Copilot will detect the skill and walk you through it:
Copilot: “I detected your Aspire solution with:
- Web App (Blazor):
MyService.Web- API:
MyService.ApiServiceI’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 provisioning skill to create Entra ID app registrations.
Ready to proceed?”
You: “Yes”
Copilot then:
- Adds
Microsoft.Identity.Webpackages - Updates
appsettings.jsonwith Azure AD config (placeholders) - Updates
Program.csin both projects - Copies helper files for login/logout and Blazor auth handling
- Creates the login button UI component
- Adds proper exception handling for Conditional Access
Step 3: Provision app registrations
Copilot: “I’m now ready to provision the Entra ID apps. Shall I proceed?”
You: “Yes please!”
Copilot: “I’ll use Microsoft Graph PowerShell to create the app registrations. First, let me check your tenant connection…”
Copilot runs PowerShell commands to:
- Create the API app registration with
access_as_userscope - Create the Web app registration with redirect URIs
- Generate a client secret and store it in user-secrets
- Update your
appsettings.jsonwith real values
Step 4: Run and test
aspire run
Open the Aspire dashboard, navigate to your Blazor app, and click Login. That’s it! 
What just happened?
The skills automated what would take at least 30-60 minutes manually, or even days without the detailed instructions:

The flow:
- User visits Blazor app → redirected to Entra ID
- User signs in → app receives tokens + establishes session
- User requests data →
MicrosoftIdentityMessageHandlerautomatically acquires access token - Token attached to API call → API validates JWT → returns data
The detailed way: understanding the code
Want to know what the skills actually did? Or prefer to implement manually? Here’s the simplified breakdown. The full article is available from Manually add Entra ID authentication and authorization to an Aspire App
Files modified
| Project | File | Changes |
|---|---|---|
| ApiService |
Program.cs
|
JWT Bearer auth, authorization middleware |
appsettings.json
|
Azure AD configuration | |
| Web |
Program.cs
|
OIDC auth, token acquisition, message handler |
appsettings.json
|
Azure AD config, downstream API scopes | |
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) |
Part 1: Protecting the API
Let’s start with the API since it’s simpler. We need to validate incoming JWT tokens.
Add Microsoft.Identity.Web to the API
cd MyService.ApiService
dotnet add package Microsoft.Identity.Web
Configure Azure AD settings
Add to appsettings.json:
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"TenantId": "YOUR_TENANT_ID",
"ClientId": "YOUR_API_CLIENT_ID",
"Audiences": ["api://YOUR_API_CLIENT_ID"]
}
}
Wire up authentication
Update Program.cs:
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Identity.Web;
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
// 👇 Add this: JWT Bearer authentication
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));
builder.Services.AddAuthorization();
var app = builder.Build();
// 👇 Add middleware (order matters!)
app.UseAuthentication();
app.UseAuthorization();
// 👇 Protect your endpoints
app.MapGet("/weatherforecast", () =>
{
// ... your logic
})
.RequireAuthorization();
app.Run();
That’s it for the API. Without a valid token, callers get a 401.
Part 2: Blazor frontend authentication
This is where the magic happens. We need to:
- Sign users in with OIDC
- Acquire access tokens for the API
- Automatically attach tokens to HTTP requests
Add Microsoft.Identity.Web
cd MyService.Web
dotnet add package Microsoft.Identity.Web
Configure Azure AD + downstream API
Add to appsettings.json:
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com",
"Domain": "yourtenant.onmicrosoft.com",
"TenantId": "YOUR_TENANT_ID",
"ClientId": "YOUR_WEB_APP_CLIENT_ID",
"CallbackPath": "/signin-oidc",
"ClientCredentials": [
{
"SourceType": "ClientSecret",
"ClientSecret": "YOUR_SECRET"
}
]
},
"WeatherApi": {
"Scopes": ["api://YOUR_API_CLIENT_ID/.default"]
}
}
Production tip: Use managed identity or certificates instead of client secrets. See the certificateless authentication guide.
Wire up authentication + token acquisition
Here’s the full Program.cs:
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Identity.Web;
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
// 1⃣ OIDC authentication + token acquisition
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi()
.AddInMemoryTokenCaches();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
// 👇 Handles incremental consent & Conditional Access in Blazor Server
builder.Services.AddScoped<BlazorAuthenticationChallengeHandler>();
// 2⃣ HttpClient with automatic token attachment
builder.Services.AddHttpClient<WeatherApiClient>(client =>
{
// Aspire service discovery! 🎉
client.BaseAddress = new("https+http://apiservice");
})
.AddMicrosoftIdentityMessageHandler(builder.Configuration.GetSection("WeatherApi"));
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.UseAntiforgery();
app.MapRazorComponents<App>().AddInteractiveServerRenderMode();
app.MapGroup("/authentication").MapLoginAndLogout();
app.Run();
The magic: MicrosoftIdentityMessageHandler
The key insight is .AddMicrosoftIdentityMessageHandler(). This DelegatingHandler:
- Intercepts outgoing HTTP requests
- Checks the token cache for a valid access token
- Silently acquires a new token if needed (using refresh token)
- Attaches the
Authorization: Bearerheader - Handles Conditional Access challenges automatically
You write normal HttpClient code. Tokens just appear. 
Deep dive: MicrosoftIdentityMessageHandler documentation
public class WeatherApiClient(HttpClient httpClient)
{
public async Task<WeatherForecast[]?> GetForecastAsync()
{
// No token handling code needed!
return await httpClient.GetFromJsonAsync<WeatherForecast[]>("/weatherforecast");
}
}
The Aspire advantage
Notice how we’re using Aspire’s service discovery:
client.BaseAddress = new("https+http://apiservice");
At runtime, Aspire resolves "apiservice" to the actual endpoint. This works seamlessly whether you’re running:
- Locally with the Aspire dashboard
- In Docker containers
- In Kubernetes
- In Azure Container Apps
Zero hardcoded URLs. Zero environment-specific config.
Adding login/logout UI
The login/logout endpoints need to support incremental consent and Conditional Access. Copy these helper files from the skill folder or use the code below:
// LoginLogoutEndpointRouteBuilderExtensions.cs
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
namespace Microsoft.Identity.Web;
public static class LoginLogoutEndpointRouteBuilderExtensions
{
public static IEndpointConventionBuilder MapLoginAndLogout(
this IEndpointRouteBuilder endpoints)
{
var group = endpoints.MapGroup("");
// Enhanced login with incremental consent support
group.MapGet("/login", (
string? returnUrl,
string? scope, // For incremental consent
string? loginHint, // Pre-fill username
string? domainHint, // Skip home realm discovery
string? claims) => // Conditional Access
{
var properties = GetAuthProperties(returnUrl);
if (!string.IsNullOrEmpty(scope))
{
var scopes = scope.Split(' ', StringSplitOptions.RemoveEmptyEntries);
properties.SetParameter(OpenIdConnectParameterNames.Scope, scopes);
}
if (!string.IsNullOrEmpty(loginHint))
properties.SetParameter(OpenIdConnectParameterNames.LoginHint, loginHint);
if (!string.IsNullOrEmpty(domainHint))
properties.SetParameter(OpenIdConnectParameterNames.DomainHint, domainHint);
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 };
}
}
Then add a Blazor component for the login button (Components/UserInfo.razor):
@using Microsoft.AspNetCore.Components.Authorization
<AuthorizeView>
<Authorized>
<span>Hello, @context.User.Identity?.Name</span>
<form action="/authentication/logout" method="post">
<AntiforgeryToken />
<button type="submit">Logout</button>
</form>
</Authorized>
<NotAuthorized>
<a href="/authentication/login?returnUrl=/">Login</a>
</NotAuthorized>
</AuthorizeView>
Handling consent & Conditional Access
This is critical for Blazor Server! You must handle exceptions on pages that call APIs.
The BlazorAuthenticationChallengeHandler (copy from skill folder) handles MicrosoftIdentityWebChallengeUserException. Use it on every page calling a protected API:
@page "/weather"
@attribute [Authorize]
@inject WeatherApiClient WeatherApi
@inject BlazorAuthenticationChallengeHandler ChallengeHandler
@code {
private WeatherForecast[]? forecasts;
private string? errorMessage;
protected override async Task OnInitializedAsync()
{
if (!await ChallengeHandler.IsAuthenticatedAsync())
{
await ChallengeHandler.ChallengeUserWithConfiguredScopesAsync("WeatherApi:Scopes");
return;
}
try
{
forecasts = await WeatherApi.GetForecastAsync();
}
catch (Exception ex)
{
// Handles incremental consent / Conditional Access
if (!await ChallengeHandler.HandleExceptionAsync(ex))
{
errorMessage = $"Error: {ex.Message}";
}
}
}
}
Common scenarios
Protecting Blazor pages
@page "/weather"
@attribute [Authorize]
<!-- Only authenticated users see this -->
Scope validation in the API
Ensure tokens have specific scopes:
app.MapGet("/sensitive-data", () => { /* ... */ })
.RequireAuthorization()
.RequireScope("data.read");
Service-to-service (no user)
For daemon scenarios:
.AddMicrosoftIdentityMessageHandler(options =>
{
options.Scopes.Add("api://client-id/.default");
options.RequestAppToken = true; // App-only token
});
Per-request scope override
var request = new HttpRequestMessage(HttpMethod.Get, "/admin-endpoint")
.WithAuthenticationOptions(options =>
{
options.Scopes.Clear();
options.Scopes.Add("api://client-id/admin.write");
});
Troubleshooting tips
| Symptom | Check This |
|---|---|
| 401 Unauthorized | Are scopes correct? Does the token audience match? |
| OIDC redirect fails | Is /signin-oidc in your app registration’s redirect URIs? |
| Token not attached | Is AddMicrosoftIdentityMessageHandler on the HttpClient? |
| AADSTS65001 | Admin consent needed—grant it in Azure Portal |
| No login button | Is UserInfo.razor included in your layout? |
404 on /MicrosoftIdentity/Account/Challenge |
Use BlazorAuthenticationChallengeHandler instead of old MicrosoftIdentityConsentHandler |
| Consent loop | Add try/catch with HandleExceptionAsync on all API-calling pages |
Pro tip: Enable logging to see token acquisition details:
builder.Services.AddLogging(options =>
{
options.AddFilter("Microsoft.Identity", LogLevel.Debug);
});
Production checklist
Before going live:
- [ ] Replace client secrets with managed identity or certificates
- [ ] Configure distributed token cache (Redis, SQL) instead of in-memory
- [ ] Set up proper redirect URIs for all deployment environments
- [ ] Grant admin consent for required scopes
- [ ] Enable Conditional Access policies as needed
- [ ] Ensure
BlazorAuthenticationChallengeHandleris used. - [ ] Add try/catch with
HandleExceptionAsyncon all pages calling APIs
Conclusion
Securing Aspire apps with Entra ID has never been easier:
The quick way: Use the AI skills with GitHub Copilot or Claude. Add authentication in 5 minutes with a conversation.
The detailed way: Understand every line by following the manual implementation guide.
Either way, you get:
- API protection: JWT Bearer authentication with 5 lines of code
- User sign-in: Standard OIDC with automatic token acquisition
- Service calls: Tokens attached automatically via
MicrosoftIdentityMessageHandler - Aspire integration: Works seamlessly with service discovery
The combination of Aspire’s orchestration, Microsoft.Identity.Web’s auth handling, and AI skills means you can go from zero to authenticated in minutes.
Full guide: Aspire Integration Guide — Comprehensive documentation with all configuration options and advanced scenarios.
Resources
Full Aspire Integration Guide
AI Skill: Authentication
AI Skill: Provisioning
Microsoft.Identity.Web Documentation
Aspire Overview
Credentials Guide
Microsoft Identity Platform
Summary
You’ve learned two ways to secure your Aspire apps with Entra ID:
- The quick way — AI skills automate the entire process
- The detailed way — Understand exactly what’s happening under the hood
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 repo!
The post Securing Aspire Apps with Microsoft Entra ID appeared first on Aspire Blog.

Production tip: Use managed identity or certificates instead of client secrets. See the 



