Web

A Secure API and a Blazor App in One ASP.NET Core Project with Microsoft Entra ID

Web · Published 2026-10-27

One ASP.NET Core project can serve both a Blazor web app and a web API protected by Microsoft Entra ID (the new name of Azure Active Directory). The trick is to register two authentication schemes: OpenID Connect with a cookie for people using the Blazor UI, and JWT bearer tokens for programs calling the API, then tell each endpoint which scheme it accepts. Microsoft Identity Web wires both with a few lines.

The code targets the Blazor Web App template of current .NET releases and the Microsoft.Identity.Web package. It follows Microsoft's documented APIs but was not compiled for this article; check names against the version you install.

The design in one picture

CallerHow it authenticatesScheme
A person in the browser using Blazor pagesSigns in with Entra ID; the server keeps a session cookieOpenID Connect + cookie
Blazor WebAssembly components calling /api/... on the same siteSame cookie, sent automatically (backend-for-frontend)Cookie
Another service, a script or a mobile appGets an access token from Entra ID and sends Authorization: Bearer ...JWT bearer

Keeping tokens on the server and letting the browser use only an HTTP-only cookie is the pattern Microsoft recommends for Blazor Web Apps (often called backend-for-frontend, or BFF). Bearer tokens are for non-browser clients.

1. Register the applications in Entra ID

  1. Web app registration. In the Entra admin center, register an application with a Web redirect URI such as https://localhost:5001/signin-oidc and a front-channel logout URL. Note the tenant ID and client ID.
  2. Expose an API. In the same registration (or a separate one if you prefer to separate concerns), set an Application ID URI such as api://<client-id> and add a scope, for example Forecast.Read. For daemon clients without a user, define an app role instead, since app-only tokens carry roles rather than scopes.
  3. Client registrations. Each external caller gets its own registration with permission to that scope or role, granted by an administrator where needed.

Store secrets or certificates for the web app in user secrets locally and in Azure Key Vault or managed identity in production, never in appsettings.json.

2. Configuration

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "<tenant-id>",
    "ClientId": "<client-id>",
    "CallbackPath": "/signin-oidc",
    "SignedOutCallbackPath": "/signout-callback-oidc"
  },
  "AzureAdApi": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "<tenant-id>",
    "ClientId": "<client-id>",
    "Audience": "api://<client-id>"
  }
}

Two sections keep the web sign-in and the API token validation independent, which helps when the API later moves to its own registration.

3. Program.cs: two schemes, one app

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.UI;

var builder = WebApplication.CreateBuilder(args);

// People: OpenID Connect sign-in, session kept in a cookie (default scheme)
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));

// Programs: JWT bearer tokens issued by Entra ID for our API
builder.Services.AddAuthentication()
    .AddMicrosoftIdentityWebApi(builder.Configuration, "AzureAdApi",
        JwtBearerDefaults.AuthenticationScheme);

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("ApiCaller", policy => policy
        .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
        .RequireAuthenticatedUser()
        .RequireScope("Forecast.Read"));
});

builder.Services.AddCascadingAuthenticationState();
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents()
    .AddMicrosoftIdentityConsentHandler();
builder.Services.AddControllersWithViews().AddMicrosoftIdentityUI();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.UseAntiforgery();
app.MapStaticAssets();

// External API: bearer tokens only, scope required
app.MapGet("/api/forecast", () => Forecast.Next(5))
   .RequireAuthorization("ApiCaller");

// Same-site API for Blazor WebAssembly components: the sign-in cookie
app.MapGet("/bff/forecast", () => Forecast.Next(5))
   .RequireAuthorization();

app.MapControllers();   // MicrosoftIdentity/Account/SignIn and SignOut
app.MapRazorComponents<App>()
   .AddInteractiveServerRenderMode();

app.Run();

What each part does:

For app-only callers, replace RequireScope with RequireRole("Forecast.Read.All"), or accept either with a custom requirement.

4. Protect Blazor pages

Add @attribute [Authorize] to a routable component to require sign-in, and use <AuthorizeView> to show content by state:

@page "/forecast"
@attribute [Authorize]

<AuthorizeView>
    <Authorized>Signed in as @context.User.Identity?.Name</Authorized>
</AuthorizeView>

Components that run on the server can call your own services directly; there is no need for them to go through HTTP to /api. WebAssembly components run in the browser and call /bff/... with an HttpClient whose base address is the site, so the cookie is sent automatically and no token ever reaches the browser.

5. Call the API from another program

A client acquires a token for api://<client-id>/.default (app-only) or for the delegated scope, then sends it in the Authorization header. Any HTTP library works; in Python, for example, the MSAL library obtains the token and urllib3 sends the request, as shown in sending HTTP requests with urllib3.

Common problems

Summary

Register OpenID Connect plus cookie for the Blazor UI with AddMicrosoftIdentityWebApp, add a JWT bearer scheme for external callers with AddMicrosoftIdentityWebApi, and use an authorization policy that names the bearer scheme and required scope on API endpoints. Let in-browser components use same-site cookie endpoints, keep tokens on the server, and keep secrets out of configuration files.

Sources

Read next

More in Web