Demo Application: BlazorAuthNZDemo (Blazor Web App with auto render mode) Technology: ASP.NET Core / .NET 10, Blazor Server + WebAssembly, Microsoft Entra ID Level: Intermediate to Advanced | Estimated duration: ~3h
Table of Contents
- Architecture and Blazor Render Modes
- Authentication with ASP.NET Core Identity
- Authentication State Propagation
- Integration with OpenID Connect (Entra ID)
- API Protection
- Authorization Policies
- Common Pitfalls and Best Practices
- Practical Exercises
- Glossary
1. Architecture and Blazor Render Modes
1.1 Overview of the .NET 10 SSR-First Model
In ASP.NET Core 10, the Blazor Web App template represents the reference model for building web applications. The fundamental change compared to previous versions is the adoption of an SSR-first paradigm (Server-Side Rendering first). By default, all components are rendered server-side and interactivity is deliberately added where needed.
This paradigm shift has profound implications for the security of the application. In .NET 10, SSR is the default mode, and authentication naturally relies on HttpContext.User, which is the standard ASP.NET Core approach.
Key concept: In the SSR-first model, security boundaries shift depending on where a component executes. Understanding these boundaries is essential for implementing correct security.
1.2 The 4 Render Modes
flowchart TD
A[Blazor Web App .NET 10] --> B{Chosen render mode}
B --> C[Static Server - SSR]
B --> D[Interactive Server]
B --> E[Interactive WebAssembly]
B --> F[Interactive Auto]
C --> C1[Static HTML server-side]
C --> C2[No SignalR]
C --> C3[HttpContext.User available]
C --> C4[Standard HTML forms]
D --> D1[Permanent SignalR connection]
D --> D2[State on the server]
D --> D3[HttpContext limited after init]
D --> D4[Full interactivity]
E --> E1[C# code in the browser]
E --> E2[Via WebAssembly runtime]
E --> E3[HttpContext NOT available]
E --> E4[Works offline]
F --> F1[Starts in Server mode]
F --> F2[Switches to WASM after cache]
F --> F3[Best user experience]
F --> F4[Hybrid security management]
style C fill:#90EE90
style D fill:#87CEEB
style E fill:#FFD700
style F fill:#FF9999
| Mode | Execution | HttpContext | Cookie sent | Security strategy |
|---|---|---|---|---|
| Static SSR | Server only | ✅ Available | ✅ Automatic | Full auth via cookies |
| Interactive Server | Server (SignalR) | ⚠️ Not reliable in interactive | ⚠️ Only at initial render | Use AuthStateProvider |
| Interactive WASM | Browser | ❌ Not available | ❌ Must configure manually | BFF pattern for APIs |
| Interactive Auto | Server → WASM | ⚠️ Hybrid | ⚠️ Hybrid | Combination of both approaches |
Static Server (Pure SSR)
This is the simplest and most secure mode from an authentication standpoint. Each interaction generates a complete HTTP request/response cycle. The authentication cookie is automatically sent by the browser with each request, and HttpContext.User is always available and reliable.
// In a pure SSR component - HttpContext always available
@page "/profile"
@inject IHttpContextAccessor HttpContextAccessor
@code {
private string? _userName;
protected override void OnInitialized()
{
// This works perfectly in SSR
_userName = HttpContextAccessor.HttpContext?.User.Identity?.Name;
}
}
Interactive Server
This mode maintains a persistent SignalR connection between the browser and the server. The UI is streamed via this WebSocket connection. The important thing to understand is that HttpContext is available during initial rendering (prerendering), but is not reliable during the interactive session, since there are no real HTTP requests — only SignalR messages.
// ❌ BAD - HttpContext not reliable in interactive mode
@rendermode InteractiveServer
@inject IHttpContextAccessor HttpContextAccessor
@code {
protected override async Task OnInitializedAsync()
{
// HttpContext may be null or stale here!
var user = HttpContextAccessor.HttpContext?.User; // DANGEROUS
}
}
// ✅ GOOD - Use AuthenticationStateProvider
@rendermode InteractiveServer
@inject AuthenticationStateProvider AuthStateProvider
@code {
protected override async Task OnInitializedAsync()
{
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
var user = authState.User; // Reliable and up to date
}
}
Interactive Auto (Recommended)
This is the recommended mode for most applications. It starts in Server mode for a quick initial response, then switches to WASM once the WebAssembly bundle is downloaded and cached.
// Component in Auto mode - must handle both cases
@rendermode InteractiveAuto
@inject AuthenticationStateProvider AuthStateProvider
@code {
// Works in both modes thanks to the AuthStateProvider injection
// On server: IdentityRevalidatingAuthenticationStateProvider
// On client: PersistentAuthenticationStateProvider (deserialized)
protected override async Task OnInitializedAsync()
{
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
}
}
1.3 Project Structure: BlazorAuthNZDemo
Solution BlazorAuthNZDemo
├── BlazorAuthNZDemo/ (Server project — Blazor host)
│ ├── Components/
│ │ ├── Account/ (Scaffolded Identity UI)
│ │ │ ├── Pages/
│ │ │ │ ├── Login.razor
│ │ │ │ ├── Register.razor
│ │ │ │ ├── Logout.razor
│ │ │ │ ├── ForgotPassword.razor
│ │ │ │ ├── ResetPassword.razor
│ │ │ │ └── Manage/
│ │ │ └── Shared/
│ │ │ └── RedirectToLogin.razor
│ │ ├── Pages/
│ │ │ ├── Home.razor
│ │ │ ├── InteractiveServerMode.razor
│ │ │ └── InteractiveAutoMode.razor
│ │ ├── Layout/
│ │ │ ├── NavMenu.razor
│ │ │ └── MainLayout.razor
│ │ ├── Routes.razor
│ │ └── App.razor
│ ├── Data/
│ │ ├── ApplicationDbContext.cs
│ │ └── BlazorAuthNZDemoUser.cs
│ ├── Services/
│ │ └── BandsRepository.cs
│ ├── TokenStores/
│ │ └── ServerSideTokenStore.cs
│ └── Program.cs
│
├── BlazorAuthNZDemo.Client/ (WASM client project)
│ ├── Components/
│ │ └── Pages/
│ │ └── InteractiveAutoMode.razor
│ ├── Services/
│ │ └── WasmBandsClient.cs
│ └── Program.cs
│
└── RemoteApi/ (Remote API — separate domain)
├── Controllers/
│ └── BandsController.cs
├── Authorization/
│ └── Policies.cs
└── Program.cs
2. Authentication with ASP.NET Core Identity
2.1 Comparison of Authentication Approaches
| Criterion | ASP.NET Core Identity (local) | OpenID Connect / Entra ID |
|---|---|---|
| User storage | Local database | Centralized at identity provider |
| Password management | App’s responsibility | Delegated to provider |
| MFA | Must implement yourself | Built into provider |
| SSO | No (unless special config) | ✅ Yes, native |
| Multi-app integration | Complex | Simple via standard protocols |
| Protocols | ASP.NET Core proprietary | OAuth2 + OpenID Connect |
| Implementation complexity | Low to medium | Medium |
| Recommended for | Standalone apps | Enterprise apps, SaaS |
Course recommendation: For most production scenarios, the OpenID Connect approach with a centralized identity provider (Entra ID, Auth0, Okta) is preferable. It centralizes user management and leverages proven security protocols.
2.2 Blazor Identity UI and ASP.NET Core Identity
Blazor Identity UI is a set of pre-built Razor components for common IAM tasks:
- User registration
- Sign in / Sign out
- Password reset
- Email confirmation
- Multi-factor authentication (MFA)
- External provider integration (Google, Microsoft, etc.)
classDiagram
class BlazorIdentityUI {
+Login.razor
+Register.razor
+Logout.razor
+ForgotPassword.razor
+ResetPassword.razor
+Manage/Index.razor
}
class ASPNETCoreIdentity {
+UserManager~TUser~
+SignInManager~TUser~
+RoleManager~TRole~
+IPasswordHasher~TUser~
+IUserValidator~TUser~
}
class EntityFramework {
+ApplicationDbContext
+IdentityUser
+IdentityRole
+Migrations
}
BlazorIdentityUI --> ASPNETCoreIdentity : uses
ASPNETCoreIdentity --> EntityFramework : persists via
Critical point: Even if the Razor components can be rendered client-side or server-side, the C# code that calls ASP.NET Core Identity classes (SignInManager, UserManager) always executes on the server. This is intentional — you don’t want authentication logic executing in the browser.
2.3 Scaffolding Blazor Identity
Option 1 — New project:
In Visual Studio, when creating the Blazor Web App project, select Individual Accounts as the authentication type.
Option 2 — Existing project:
Visual Studio → Right-click on server project
→ Add → New Scaffolded Item
→ Identity → Blazor Identity
→ Configure:
- DbContext class: ApplicationDbContext (new)
- User class: BlazorAuthNZDemoUser (inherits IdentityUser)
- Database provider: SQLite or SQL Server
→ Click Add
2.4 Program.cs Configuration (Identity)
// Program.cs — Complete Identity configuration for Blazor .NET 10
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration
.GetConnectionString("DefaultConnection")
?? throw new InvalidOperationException("Connection string not found.");
// ═══════════════════════════════════════════════════════════════
// 1. Razor Components with all render modes
// ═══════════════════════════════════════════════════════════════
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents()
.AddInteractiveWebAssemblyComponents();
// ═══════════════════════════════════════════════════════════════
// 2. Database Context for Identity
// ═══════════════════════════════════════════════════════════════
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlite(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
// ═══════════════════════════════════════════════════════════════
// 3. ASP.NET Core Identity
// ═══════════════════════════════════════════════════════════════
builder.Services.AddIdentityCore<BlazorAuthNZDemoUser>(options =>
{
options.Password.RequiredLength = 8;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequireUppercase = true;
options.SignIn.RequireConfirmedAccount = true;
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
options.Lockout.MaxFailedAccessAttempts = 5;
})
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddSignInManager()
.AddDefaultTokenProviders();
// ═══════════════════════════════════════════════════════════════
// 4. Cookie authentication scheme
// ═══════════════════════════════════════════════════════════════
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = IdentityConstants.ApplicationScheme;
options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
})
.AddIdentityCookies();
// ═══════════════════════════════════════════════════════════════
// 5. NEW .NET 10 — Cascading + serialized authentication state
// ═══════════════════════════════════════════════════════════════
builder.Services.AddCascadingAuthenticationState();
// Serializes auth state from server so it can be read by WASM client
builder.Services.AddAuthenticationStateSerialization();
// ═══════════════════════════════════════════════════════════════
// 6. Business services
// ═══════════════════════════════════════════════════════════════
builder.Services.AddScoped<BandsRepository>();
var app = builder.Build();
// ═══════════════════════════════════════════════════════════════
// Middleware pipeline (ORDER MATTERS!)
// ═══════════════════════════════════════════════════════════════
if (app.Environment.IsDevelopment())
{
app.UseWebAssemblyDebugging();
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
// IMPORTANT: UseAuthentication BEFORE UseAuthorization
app.UseAuthentication();
app.UseAuthorization();
app.UseAntiforgery();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode()
.AddInteractiveWebAssemblyRenderMode()
.AddAdditionalAssemblies(typeof(BlazorAuthNZDemo.Client._Imports).Assembly);
// Automatically generated Identity endpoints (login, logout, register, etc.)
app.MapAdditionalIdentityEndpoints();
app.Run();
WASM Client — Program.cs:
// BlazorAuthNZDemo.Client/Program.cs
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
// Client-side authorization services
builder.Services.AddAuthorizationCore();
builder.Services.AddCascadingAuthenticationState();
// NEW .NET 10 — Read auth state serialized by the server
builder.Services.AddAuthenticationStateDeserialization();
// HTTP client for local API calls
builder.Services.AddScoped(sp =>
new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
await builder.Build().RunAsync();
2.5 IdentityRevalidatingAuthenticationStateProvider
This crucial component is generated by the scaffolding. It ensures authentication state remains valid during an interactive Blazor Server session:
// Components/Account/IdentityRevalidatingAuthenticationStateProvider.cs
// (Generated by scaffolding — do not modify without reason)
internal sealed class IdentityRevalidatingAuthenticationStateProvider
: RevalidatingServerAuthenticationStateProvider
{
private readonly IServiceScopeFactory _scopeFactory;
public IdentityRevalidatingAuthenticationStateProvider(
ILoggerFactory loggerFactory,
IServiceScopeFactory scopeFactory)
: base(loggerFactory)
{
_scopeFactory = scopeFactory;
}
// Revalidates the security stamp every 30 minutes
// If the stamp no longer matches (e.g., password changed), disconnects the user
protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30);
protected override async Task<bool> ValidateAuthenticationStateAsync(
AuthenticationState authenticationState,
CancellationToken cancellationToken)
{
await using var scope = _scopeFactory.CreateAsyncScope();
var userManager = scope.ServiceProvider
.GetRequiredService<UserManager<BlazorAuthNZDemoUser>>();
return await ValidateSecurityStampAsync(userManager, authenticationState.User);
}
private static async Task<bool> ValidateSecurityStampAsync(
UserManager<BlazorAuthNZDemoUser> userManager,
ClaimsPrincipal principal)
{
var user = await userManager.GetUserAsync(principal);
if (user is null)
return false;
if (!userManager.SupportsUserSecurityStamp)
return true;
var principalStamp = principal.FindFirstValue(
new ClaimsIdentityOptions().SecurityStampClaimType);
var userStamp = await userManager.GetSecurityStampAsync(user);
return principalStamp == userStamp;
}
}
Why the security stamp? When a user changes their password, their roles change, or an administrator revokes a session, the security stamp changes. The revalidator detects this change and forces logout, preventing a compromised session from remaining active too long.
3. Authentication State Propagation
3.1 AuthenticationStateProvider — Central Concept
AuthenticationStateProvider is a fundamental abstract class in Blazor. It provides the current authentication state to components.
// Abstract definition (in Microsoft.AspNetCore.Components.Authorization)
public abstract class AuthenticationStateProvider
{
public abstract Task<AuthenticationState> GetAuthenticationStateAsync();
public event AuthenticationStateChangedHandler? AuthenticationStateChanged;
protected void NotifyAuthenticationStateChanged(Task<AuthenticationState> task);
}
public class AuthenticationState
{
public AuthenticationState(ClaimsPrincipal user) { User = user; }
public ClaimsPrincipal User { get; }
}
Implementations by context:
| Context | Implementation | How it works |
|---|---|---|
| Server (Identity) | IdentityRevalidatingAuthenticationStateProvider | Reads HttpContext.User + revalidates stamp |
| Server (OIDC) | Built-in ServerAuthenticationStateProvider | Reads HttpContext.User from OIDC cookie |
| WASM Client (.NET 10) | PersistentAuthenticationStateProvider | Deserializes state sent by server |
3.2 .NET 10 Serialization Mechanism
flowchart LR
subgraph Server["Server"]
SC[HttpContext.User\nClaimsPrincipal]
SS[AddAuthenticationStateSerialization\n.NET 10]
SC --> SS
SS --> |"Serializes claims\ninto HTML page"| HTML
end
subgraph Client["WASM Client"]
DS[AddAuthenticationStateDeserialization\n.NET 10]
CP[ClaimsPrincipal reconstructed\nclient-side]
DS --> CP
CP --> |"Available via\nAuthStateProvider"| UI
end
HTML --> |"WASM bundle loaded,\nclaims read from DOM"| DS
style Server fill:#e6f3ff
style Client fill:#fff3e6
Server-side — Claims serialization:
// In server Program.cs
builder.Services.AddAuthenticationStateSerialization();
// This service automatically injects serialized claims into the rendered HTML
// as a hidden <template> element with JSON data
Client-side — Deserialization:
// In WASM client Program.cs
builder.Services.AddAuthenticationStateDeserialization();
// This service reads the serialized data from the HTML and reconstructs the ClaimsPrincipal
// Result: AuthenticationStateProvider.GetAuthenticationStateAsync()
// returns the correct user client-side, without additional HTTP calls
3.3 Using Authentication State in Components
Approach 1 — CascadingParameter:
@code {
[CascadingParameter]
private Task<AuthenticationState>? authStateTask { get; set; }
private AuthenticationState? authState;
protected override async Task OnInitializedAsync()
{
if (authStateTask != null)
authState = await authStateTask;
}
}
Approach 2 — AuthorizeView (most common in template):
<AuthorizeView>
<Authorized>
<p>Welcome, @context.User.Identity!.Name!</p>
</Authorized>
<NotAuthorized>
<p>Please <a href="/Account/Login">sign in</a>.</p>
</NotAuthorized>
<Authorizing>
<p>Checking...</p>
</Authorizing>
</AuthorizeView>
@* With authorization policy *@
<AuthorizeView Policy="@Policies.IsFromBelgium">
<Authorized>
<p>Content for Belgian users only.</p>
</Authorized>
</AuthorizeView>
@* With role *@
<AuthorizeView Roles="Admin,Manager">
<Authorized>
<p>Content for admins and managers.</p>
</Authorized>
</AuthorizeView>
Approach 3 — [Authorize] attribute on pages:
@page "/protected"
@attribute [Authorize]
@* or with policy: @attribute [Authorize(Policy = "IsFromBelgium")] *@
@* or with role: @attribute [Authorize(Roles = "Admin")] *@
<h1>Protected page</h1>
3.4 Routes.razor and AuthorizeRouteView
@* Components/Routes.razor *@
<Router AppAssembly="typeof(Program).Assembly"
AdditionalAssemblies="new[] { typeof(Client._Imports).Assembly }">
<Found Context="routeData">
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)">
<NotAuthorized>
<RedirectToLogin />
</NotAuthorized>
<Authorizing>
<p>Loading...</p>
</Authorizing>
</AuthorizeRouteView>
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="typeof(Layout.MainLayout)">
<p role="alert">Sorry, this page does not exist.</p>
</LayoutView>
</NotFound>
</Router>
@* Components/Account/Shared/RedirectToLogin.razor *@
@inject NavigationManager NavigationManager
@code {
protected override void OnInitialized()
{
// forceLoad: true = full navigation (required for SSR pages)
var returnUrl = Uri.EscapeDataString(NavigationManager.Uri);
NavigationManager.NavigateTo($"/Account/Login?returnUrl={returnUrl}",
forceLoad: true);
}
}
4. Integration with OpenID Connect (Entra ID)
4.1 OAuth2 and OpenID Connect — Conceptual Overview
sequenceDiagram
participant User as User
participant App as Blazor Host
participant EntraID as Microsoft Entra ID
participant RemoteAPI as Remote API
User->>App: Access to protected page
App->>EntraID: Redirect to Authorization Endpoint\n(PKCE code_challenge)
EntraID->>User: Entra ID sign-in page
User->>EntraID: Credentials (email + password / MFA)
EntraID->>App: Authorization Code + state
App->>EntraID: POST Token Endpoint\n(code + code_verifier + client_secret)
EntraID->>App: identity_token + access_token + refresh_token
App->>App: Validate identity_token
App->>App: Create ClaimsPrincipal from identity_token
App->>App: Create local session cookie
App->>User: Redirect to protected page (with cookie)
Note over App,RemoteAPI: To access remote API:
App->>RemoteAPI: HTTP Request + Bearer access_token
RemoteAPI->>RemoteAPI: Validate access_token (signature, audience, expiration)
RemoteAPI->>App: JSON data
Key concepts:
| Term | Definition |
|---|---|
| OAuth2 | Authorization protocol — “what can I do?” |
| OpenID Connect | Identity layer on OAuth2 — “who are you?” |
| Authorization Code | Temporary code exchanged for tokens |
| PKCE | Protection against code injection (Proof Key for Code Exchange) |
| identity_token | JWT proving user identity |
| access_token | JWT authorizing access to a specific resource |
| refresh_token | Long-lived token to obtain new access_tokens |
| Audience (aud) | Who the token is intended for (the API that should accept it) |
| Scope | Requested permissions (openid, profile, email, custom_scope) |
4.2 Registering the Application in Entra ID
Registering the Blazor host application:
Azure Portal → Microsoft Entra ID → App registrations → New registration
→ Name: "Blazor OIDC Demo"
→ Supported account types: "Accounts in this organizational directory only"
→ Redirect URI:
- Type: Web
- URI: https://localhost:PORT/signin-oidc
→ Register
After creation:
→ Certificates & secrets → New client secret
- Description: "BlazorApp Secret"
- Expires: 24 months
- Copy the value immediately (won't be visible again)
→ Token configuration → Add optional claim
- Token type: ID
- Claim: ctry (country)
4.3 AddOpenIdConnect Configuration in Program.cs
// Program.cs — Complete OIDC configuration (without local Identity)
const string OidcScheme = "EntraIDOpenIdConnect";
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents()
.AddInteractiveWebAssemblyComponents();
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OidcScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.Cookie.Name = "BlazorApp.Session";
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Lax;
options.ExpireTimeSpan = TimeSpan.FromHours(8);
options.SlidingExpiration = true;
})
.AddOpenIdConnect(OidcScheme, options =>
{
options.Authority = $"https://login.microsoftonline.com/{tenantId}/v2.0";
options.ClientId = builder.Configuration["Entra:ClientId"];
options.ClientSecret = builder.Configuration["Entra:ClientSecret"];
options.ResponseType = OpenIdConnectResponseType.Code;
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
options.Scope.Add("offline_access");
options.Scope.Add($"api://{remoteApiClientId}/full_access");
options.SaveTokens = true;
options.MapInboundClaims = false;
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "roles"
};
options.Events = new OpenIdConnectEvents
{
OnTokenValidated = async context =>
{
var tokenStore = context.HttpContext.RequestServices
.GetRequiredService<ServerSideTokenStore>();
var userId = context.Principal?.FindFirstValue("sub")
?? throw new InvalidOperationException("Missing sub claim");
var accessToken = context.TokenEndpointResponse?.AccessToken
?? throw new InvalidOperationException("Missing access token");
var refreshToken = context.TokenEndpointResponse?.RefreshToken;
var expiresIn = int.Parse(context.TokenEndpointResponse?.ExpiresIn ?? "3600");
await tokenStore.StoreTokensAsync(userId, new TokenInfo(
AccessToken: accessToken,
RefreshToken: refreshToken,
ExpiresAt: DateTimeOffset.UtcNow.AddSeconds(expiresIn)
));
}
};
});
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddAuthenticationStateSerialization();
builder.Services.AddSingleton<ServerSideTokenStore>();
4.4 Login and Logout Endpoints
// Program.cs — Minimal endpoints for login/logout OIDC
// ─── LOGIN ──────────────────────────────────────────────────────
app.MapGet("/login", (string? returnUrl, HttpContext httpContext) =>
{
var validatedReturnUrl = ValidateReturnUrl(returnUrl, httpContext.Request.PathBase);
return TypedResults.Challenge(
new AuthenticationProperties { RedirectUri = validatedReturnUrl },
[OidcScheme]
);
}).AllowAnonymous();
// ─── LOGOUT ─────────────────────────────────────────────────────
app.MapPost("/logout", async (
[FromForm] string? returnUrl,
HttpContext httpContext) =>
{
var validatedReturnUrl = ValidateReturnUrl(returnUrl, httpContext.Request.PathBase);
// Sign out from BOTH schemes:
// 1. Local cookie (invalidates session in our app)
// 2. OIDC (redirects to Entra ID to invalidate its session too)
return TypedResults.SignOut(
new AuthenticationProperties { RedirectUri = validatedReturnUrl },
[CookieAuthenticationDefaults.AuthenticationScheme, OidcScheme]
);
}).RequireAntiForgeryToken();
// ─── HELPER METHOD ─────────────────────────────────────────────
static string ValidateReturnUrl(string? returnUrl, PathString pathBase)
{
if (string.IsNullOrEmpty(returnUrl))
return pathBase.Value ?? "/";
if (!Uri.IsWellFormedUriString(returnUrl, UriKind.RelativeOrAbsolute))
return pathBase.Value ?? "/";
// Reject absolute URLs (open redirect protection)
if (Uri.TryCreate(returnUrl, UriKind.Absolute, out _))
return pathBase.Value ?? "/";
if (!returnUrl.StartsWith('/'))
return pathBase.Value ?? "/";
return returnUrl;
}
Logout button in NavMenu:
@* In NavMenu.razor *@
<AuthorizeView>
<Authorized>
<form method="post" action="/logout">
<AntiForgeryToken />
<input type="hidden" name="returnUrl" value="/" />
<button type="submit" class="nav-link btn btn-link">
Sign out (@context.User.Identity!.Name)
</button>
</form>
</Authorized>
<NotAuthorized>
<a class="nav-link" href="/login">Sign in</a>
</NotAuthorized>
</AuthorizeView>
Why POST for logout? Logout must be a POST action (not GET) for two reasons: 1) Protect against CSRF attacks that could sign out the user via a malicious link. 2) Allow adding an anti-forgery token to validate that the user initiated the sign-out.
5. API Protection
5.1 Local API vs Remote API — Architecture
flowchart TB
subgraph Browser["Browser"]
WASM[Blazor WASM\nClient]
end
subgraph Host["host.example.com — Blazor Host"]
BlazorServer[Blazor Server\nSSR + SignalR]
LocalAPI["/localapi/bands\nLocal API"]
BFF["/forward-to-remote-api/bands\nBFF Proxy"]
TokenStore[ServerSideTokenStore\nAccess tokens]
BFF --> TokenStore
end
subgraph Remote["api.example.com — Remote API"]
RemoteAPI["/remoteapi/bands\nRemote API"]
end
WASM -->|"Cookie sent\nautomatically"| LocalAPI
WASM -->|"Cookie sent\nautomatically"| BFF
BFF -->|"Bearer access_token\n(from TokenStore)"| RemoteAPI
BlazorServer -->|"Direct (no HTTP)"| LocalAPI
BlazorServer -->|"Bearer access_token"| RemoteAPI
style Browser fill:#FFF3CD
style Host fill:#D4EDDA
style Remote fill:#D1ECF1
5.2 Local API — Cookie Protection
The principle is simple: since the cookie is automatically sent by the browser for all requests to the same domain, the local API benefits from this protection naturally.
Server side — Secure the endpoint:
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/localapi/bands", async (BandsRepository repo) =>
{
var bands = await repo.GetBandsAsync();
return Results.Ok(bands);
})
.RequireAuthorization();
WASM client side — Send the cookie:
// ⚠️ WASM does NOT automatically send cookies by default
// You must explicitly configure credentials
public class WasmBandsClient
{
private readonly HttpClient _httpClient;
public async Task<List<Band>?> GetLocalBandsAsync()
{
var request = new HttpRequestMessage(HttpMethod.Get, "/localapi/bands");
// Without this line, the cookie is NOT sent from WASM
request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include);
var response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<List<Band>>();
}
}
Interactive server side — Direct access without HTTP:
// InteractiveServerMode.razor — On server, no HTTP needed
@inject BandsRepository BandsRepo
@code {
private List<Band>? _bands;
protected override async Task OnInitializedAsync()
{
// ✅ Direct repository access — no local HTTP request
_bands = await BandsRepo.GetBandsAsync();
}
}
5.3 Remote API — Access Tokens and BFF Pattern
Why the BFF Pattern?
flowchart LR
subgraph Problem["❌ Without BFF (DANGEROUS)"]
W1[WASM] -->|"Access Token\nin the browser!"| R1[Remote API]
note1["Token exposed to all JS\non the page - XSS risk!"]
end
subgraph Solution["✅ With BFF (SECURE)"]
W2[WASM] -->|"Cookie only\n(HttpOnly)"| BFF2[BFF Proxy\non the Host]
BFF2 -->|"Bearer Token\n(server-side)"| R2[Remote API]
note2["Token never in\nthe browser!"]
end
style Problem fill:#FFE4E4
style Solution fill:#E4FFE4
Server-side token storage:
// TokenStores/ServerSideTokenStore.cs
public record TokenInfo(
string AccessToken,
string? RefreshToken,
DateTimeOffset ExpiresAt
);
public class ServerSideTokenStore
{
private readonly ConcurrentDictionary<string, TokenInfo> _tokens = new();
public Task StoreTokensAsync(string userId, TokenInfo tokenInfo)
{
_tokens[userId] = tokenInfo;
return Task.CompletedTask;
}
public Task<TokenInfo?> GetTokensAsync(string userId)
{
_tokens.TryGetValue(userId, out var tokenInfo);
return Task.FromResult(tokenInfo);
}
public Task ClearTokensAsync(string userId)
{
_tokens.TryRemove(userId, out _);
return Task.CompletedTask;
}
public Task<bool> HasValidTokenAsync(string userId)
{
if (!_tokens.TryGetValue(userId, out var tokenInfo))
return Task.FromResult(false);
return Task.FromResult(tokenInfo.ExpiresAt > DateTimeOffset.UtcNow.AddSeconds(30));
}
}
DelegatingHandler to attach the token:
// Infrastructure/AccessTokenDelegatingHandler.cs
public class AccessTokenDelegatingHandler : DelegatingHandler
{
private readonly ServerSideTokenStore _tokenStore;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILogger<AccessTokenDelegatingHandler> _logger;
public AccessTokenDelegatingHandler(
ServerSideTokenStore tokenStore,
IHttpContextAccessor httpContextAccessor,
ILogger<AccessTokenDelegatingHandler> logger)
{
_tokenStore = tokenStore;
_httpContextAccessor = httpContextAccessor;
_logger = logger;
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var userId = _httpContextAccessor.HttpContext?.User
.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId == null)
return new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized);
var tokenInfo = await _tokenStore.GetTokensAsync(userId);
if (tokenInfo == null)
return new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized);
if (tokenInfo.ExpiresAt <= DateTimeOffset.UtcNow.AddSeconds(30))
{
tokenInfo = await RefreshTokenAsync(userId, tokenInfo);
if (tokenInfo == null)
return new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized);
}
request.Headers.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue(
"Bearer", tokenInfo.AccessToken);
return await base.SendAsync(request, cancellationToken);
}
private async Task<TokenInfo?> RefreshTokenAsync(string userId, TokenInfo currentToken)
{
if (string.IsNullOrEmpty(currentToken.RefreshToken))
return null;
try
{
using var client = new HttpClient();
var response = await client.PostAsync(
$"https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token",
new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "refresh_token",
["client_id"] = clientId,
["client_secret"] = clientSecret,
["refresh_token"] = currentToken.RefreshToken,
["scope"] = $"api://{remoteApiClientId}/full_access offline_access"
}));
if (!response.IsSuccessStatusCode)
{
await _tokenStore.ClearTokensAsync(userId);
return null;
}
var tokenResponse = await response.Content
.ReadFromJsonAsync<TokenRefreshResponse>();
var newTokenInfo = new TokenInfo(
AccessToken: tokenResponse!.AccessToken,
RefreshToken: tokenResponse.RefreshToken ?? currentToken.RefreshToken,
ExpiresAt: DateTimeOffset.UtcNow.AddSeconds(tokenResponse.ExpiresIn)
);
await _tokenStore.StoreTokensAsync(userId, newTokenInfo);
return newTokenInfo;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error during token refresh for {UserId}", userId);
return null;
}
}
}
Handler registration and BFF endpoint:
builder.Services.AddScoped<AccessTokenDelegatingHandler>();
builder.Services.AddHttpContextAccessor();
builder.Services.AddHttpClient("RemoteApiClient", client =>
{
client.BaseAddress = new Uri("https://api.example.com");
})
.AddHttpMessageHandler<AccessTokenDelegatingHandler>();
// ─── BFF ENDPOINT ────────────────────────────────────────────────
app.MapGet("/forward-to-remote-api/bands", async (
IHttpClientFactory httpClientFactory) =>
{
var client = httpClientFactory.CreateClient("RemoteApiClient");
var bands = await client.GetFromJsonAsync<List<Band>>("/remoteapi/bands");
return Results.Ok(bands);
})
.RequireAuthorization();
5.4 Refresh Tokens — Managing Expiration
sequenceDiagram
participant WASM as Blazor WASM
participant BFF as Host (BFF)
participant TokenStore as ServerSideTokenStore
participant EntraID as Entra ID Token Endpoint
participant API as Remote API
WASM->>BFF: GET /forward-to-remote-api/bands (+ cookie)
BFF->>BFF: Validate cookie → OK
BFF->>TokenStore: GetTokensAsync(userId)
alt Valid token
TokenStore-->>BFF: TokenInfo (valid access_token)
BFF->>API: GET /remoteapi/bands (Bearer access_token)
API-->>BFF: 200 OK + data
BFF-->>WASM: 200 OK + data
else Expired token but refresh_token available
TokenStore-->>BFF: TokenInfo (expired access_token)
BFF->>EntraID: POST /token\n(grant_type=refresh_token)
EntraID-->>BFF: New access_token + refresh_token
BFF->>TokenStore: StoreTokensAsync(new TokenInfo)
BFF->>API: GET /remoteapi/bands (Bearer new access_token)
API-->>BFF: 200 OK + data
BFF-->>WASM: 200 OK + data
else Expired token AND no refresh_token
BFF-->>WASM: 401 Unauthorized
WASM->>WASM: Redirect to /login
end
6. Authorization Policies
6.1 RBAC vs ABAC — In-depth Comparison
flowchart LR
subgraph RBAC["RBAC — Role-Based Access Control"]
U1[User] -->|"has role"| R1[Admin]
U1 -->|"has role"| R2[Manager]
R1 -->|"can"| A1[Create]
R1 -->|"can"| A2[Delete]
R2 -->|"can"| A3[Read]
R2 -->|"can"| A4[Edit]
end
subgraph ABAC["ABAC — Attribute-Based Access Control"]
U2[User] -->|"has attributes"| C1["country = BE"]
U2 -->|"has attributes"| C2["dept = IT"]
U2 -->|"has attributes"| C3["level = Senior"]
POL[Policy] -->|"evaluates"| C1
POL -->|"evaluates"| C2
POL -->|"evaluates"| C3
POL -->|"decision"| DEC["Access granted/denied"]
end
style RBAC fill:#FFE4B5
style ABAC fill:#B5FFB5
Comparison table:
| Criterion | RBAC | ABAC |
|---|---|---|
| Granularity | Low (broad roles) | Fine (claim combinations) |
| Complexity | Simple | Moderate |
| Scalability | Role explosion | Composable policies |
| Maintenance | Role assignment | Policy definition |
| ASP.NET Core | RequireRole() | RequireClaim(), Requirements |
6.2 Defining Policies
// BlazorAuthNZDemo.Client/Authorization/Policies.cs
public static class Policies
{
public const string IsFromBelgium = "IsFromBelgium";
public const string RequiresAdminRole = "RequiresAdminRole";
public const string IsSeniorEmployee = "IsSeniorEmployee";
// Policy: authenticated user from Belgium
public static AuthorizationPolicy IsFromBelgiumPolicy()
{
return new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.RequireClaim("ctry", "BE")
.Build();
}
// Policy: Admin role
public static AuthorizationPolicy RequiresAdminRolePolicy()
{
return new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.RequireRole("Admin")
.Build();
}
// Combined policy: Belgian admin OR senior employee
public static AuthorizationPolicy IsSeniorEmployeePolicy()
{
return new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.RequireAssertion(context =>
{
var user = context.User;
var isAdminFromBelgium = user.IsInRole("Admin")
&& user.HasClaim("ctry", "BE");
var isSenior = user.HasClaim("jobLevel", "Senior");
return isAdminFromBelgium || isSenior;
})
.Build();
}
}
Server-side policy registration:
builder.Services.AddAuthorization(options =>
{
options.AddPolicy(Policies.IsFromBelgium, Policies.IsFromBelgiumPolicy());
options.AddPolicy(Policies.RequiresAdminRole, Policies.RequiresAdminRolePolicy());
options.AddPolicy(Policies.IsSeniorEmployee, Policies.IsSeniorEmployeePolicy());
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
6.3 Applying Policies
On Blazor pages:
@page "/belgium-only"
@attribute [Authorize(Policy = Policies.IsFromBelgium)]
@using BlazorAuthNZDemo.Client.Authorization
<h1>Content for Belgium only</h1>
On AuthorizeView:
<AuthorizeView Policy="@Policies.IsFromBelgium">
<Authorized>
<NavLink href="belgium-only">Belgium content</NavLink>
</Authorized>
</AuthorizeView>
On API endpoints:
app.MapGet("/localapi/bands", async (BandsRepository repo) =>
Results.Ok(await repo.GetBandsAsync()))
.RequireAuthorization(Policies.IsFromBelgium);
app.MapGet("/forward-to-remote-api/bands", async (IHttpClientFactory factory) =>
{
var client = factory.CreateClient("RemoteApiClient");
var bands = await client.GetFromJsonAsync<List<Band>>("/remoteapi/bands");
return Results.Ok(bands);
})
.RequireAuthorization(Policies.IsFromBelgium);
7. Common Pitfalls and Best Practices
7.1 The 6 Critical Mistakes to Avoid
flowchart TD
subgraph Pitfalls["Critical Pitfalls"]
P1["❌ 1. Trusting [Authorize] client-side in WASM"]
P2["❌ 2. Injecting HttpContext in interactive mode"]
P3["❌ 3. Exposing tokens to the browser"]
P4["❌ 4. Forgetting auth services on the client"]
P5["❌ 5. Ignoring prerendering"]
P6["❌ 6. Not validating open redirect"]
end
subgraph Solutions["Solutions"]
S1["✅ Always enforce at API/server"]
S2["✅ Use AuthStateProvider or CascadingParam"]
S3["✅ BFF Pattern — tokens stay on server"]
S4["✅ AddCascadingAuthenticationState + AddAuthorizationCore"]
S5["✅ Handle state during OnInitializedAsync"]
S6["✅ ValidateReturnUrl() on login/logout"]
end
P1 --> S1
P2 --> S2
P3 --> S3
P4 --> S4
P5 --> S5
P6 --> S6
Pitfall 1 — Trusting client-side authorization
// ❌ DANGEROUS — WASM code can be bypassed!
@page "/admin"
@attribute [Authorize(Roles = "Admin")]
@rendermode InteractiveWebAssembly
// Someone can modify the token client-side or use DevTools to circumvent this!
// ✅ CORRECT — Always check on the API
app.MapGet("/api/admin-data", async (...) => ...)
.RequireAuthorization(Policies.RequiresAdminRole); // ← The real security
Pitfall 2 — HttpContext in interactive mode
// ❌ BAD — HttpContext not reliable during SignalR
@inject IHttpContextAccessor HttpContextAccessor
@rendermode InteractiveServer
@code {
protected override void OnInitialized()
{
// MAY BE NULL or stale!
var user = HttpContextAccessor.HttpContext?.User;
}
}
// ✅ CORRECT — AuthStateProvider works in all modes
@inject AuthenticationStateProvider AuthStateProvider
@code {
protected override async Task OnInitializedAsync()
{
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
var user = authState.User; // Always reliable
}
}
Pitfall 3 — Tokens in the browser
// ❌ VERY DANGEROUS — Never do this!
localStorage.setItem('access_token', response.access_token);
// Any JS script on the page can read localStorage
// XSS attack → your token is compromised
// ✅ CORRECT — Tokens stay on the server (BFF Pattern)
// The browser only has access to an HttpOnly cookie
Pitfall 4 — Missing auth services on WASM client
// ❌ MISSING — Authorization silently broken client-side
var builder = WebAssemblyHostBuilder.CreateDefault(args);
// Missing AddCascadingAuthenticationState()!
// Missing AddAuthorizationCore()!
// ✅ CORRECT — All required services
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.Services.AddAuthorizationCore();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddAuthenticationStateDeserialization(); // .NET 10
Pitfall 5 — Ignoring prerendering
// ❌ PROBLEM — Prerendering on server even for WASM components
@rendermode InteractiveWebAssembly
@code {
protected override async Task OnInitializedAsync()
{
// During prerendering, we're on the server
// Browser APIs don't exist yet!
await JSRuntime.InvokeAsync<string>("localStorage.getItem", "key"); // ❌ Crash!
}
}
// ✅ CORRECT — Detect prerendering
@code {
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// Now we're in the browser — localStorage available
var value = await JSRuntime.InvokeAsync<string>("localStorage.getItem", "key");
StateHasChanged();
}
}
}
Pitfall 6 — Open redirect attack
// ❌ VULNERABLE — Unvalidated return URL
app.MapGet("/login", (string? returnUrl) =>
{
// If returnUrl = "https://evil.com/phishing"
// The user will be redirected to a malicious site after login!
return TypedResults.Challenge(
new AuthenticationProperties { RedirectUri = returnUrl }, // DANGEROUS
[OidcScheme]);
});
// ✅ CORRECT — Validate the return URL
app.MapGet("/login", (string? returnUrl, HttpContext httpContext) =>
{
var validatedUrl = ValidateReturnUrl(returnUrl, httpContext.Request.PathBase);
return TypedResults.Challenge(
new AuthenticationProperties { RedirectUri = validatedUrl },
[OidcScheme]);
});
7.2 Blazor Security Checklist
| Category | Check | Status |
|---|---|---|
| Authentication | HttpOnly + Secure + SameSite=Lax cookie | ✅ |
| Authentication | UseAuthentication() before UseAuthorization() | ✅ |
| Authentication | Anti-forgery configured (UseAntiforgery()) | ✅ |
| Authorization | API endpoints protected with RequireAuthorization() | ✅ |
| Authorization | Policies registered on server AND remote API | ✅ |
| Tokens | Access tokens stored server-side only | ✅ |
| Tokens | BFF Pattern for WASM → remote API calls | ✅ |
| Tokens | Refresh token implemented to avoid re-login | ✅ |
| WASM | AddCascadingAuthenticationState() + AddAuthorizationCore() | ✅ |
| WASM | SetBrowserRequestCredentials(Include) for cookies | ✅ |
| OIDC | ValidateReturnUrl() on login and logout | ✅ |
| OIDC | Logout from both schemes (cookie + OIDC) | ✅ |
| General | MapInboundClaims = false to avoid claim transformation | ✅ |
7.3 Secure appsettings.json Configuration
// appsettings.json — Don't put secrets here in production!
{
"ConnectionStrings": {
"DefaultConnection": "Data Source=app.db"
},
"Entra": {
"TenantId": "your-tenant-id-here",
"ClientId": "your-client-id-here"
// ClientSecret: use user secrets in development
// use Azure Key Vault or env vars in production
},
"RemoteApi": {
"BaseUrl": "https://api.example.com",
"ClientId": "remote-api-client-id"
}
}
// In development — Secret Manager (never commit!)
// dotnet user-secrets set "Entra:ClientSecret" "your-secret-here"
// In production — Azure App Service environment variables
// ENTRA__CLIENTSECRET=your-secret-here
// (Double underscore for nested sections)
8. Practical Exercises
Exercise 1 — Identity Scaffolding (Beginner)
Objective: Create a Blazor Web App with integrated Identity.
Steps:
- Create a new Blazor Web App with
.NET 10 - Render mode:
Interactive Auto - Authentication:
Individual Accounts - Launch the application and explore the generated pages
- Create a user account
- Implement a “My Profile” button visible only when signed in
Starter code:
@* Pages/Profile.razor *@
@page "/profile"
@attribute [Authorize]
@inject AuthenticationStateProvider AuthStateProvider
<h1>My Profile</h1>
@* TODO: Display the authenticated user's claims *@
@code {
private AuthenticationState? _authState;
protected override async Task OnInitializedAsync()
{
_authState = await AuthStateProvider.GetAuthenticationStateAsync();
}
}
Exercise 2 — OpenID Connect (Intermediate)
Objective: Replace Identity with OIDC using Entra ID.
Prerequisites:
- Active Azure account
- Access to an Entra ID tenant
Steps:
- Remove local Identity components
- Register the application in Entra ID
- Configure
AddOpenIdConnectin Program.cs - Implement the
/loginand/logoutendpoints - Test the complete sign-in/sign-out flow
Validation points:
- Redirect to Microsoft when clicking “Sign in”
- Cookie created after successful authentication
- Cookie deleted after sign-out
- Entra ID session also terminated
Exercise 3 — BFF Pattern (Advanced)
Objective: Implement the BFF Pattern to secure calls from WASM to a remote API.
Target architecture:
WASM → Host (BFF) → Remote API
Steps:
- Create a separate
RemoteApiproject - Protect endpoints with
RequireAuthorization() - Create
ServerSideTokenStore - Create
AccessTokenDelegatingHandler - Implement the BFF proxy endpoint
- Test with a valid and an expired token
Exercise 4 — Custom Policies (Advanced)
Scenario: Only users from the “Engineering” department with 3+ years of experience and from an approved country (BE, NL, DE) can access certain sensitive data.
// To implement:
public class MultiAttributeRequirement : IAuthorizationRequirement
{
public IEnumerable<string> AllowedCountries { get; }
public string RequiredDepartment { get; }
public int MinYearsExperience { get; }
}
public class MultiAttributeHandler : AuthorizationHandler<MultiAttributeRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
MultiAttributeRequirement requirement)
{
// TODO: Check country, department, and yearsExperience
return Task.CompletedTask;
}
}
9. Glossary
| Term | Definition |
|---|---|
| ABAC | Attribute-Based Access Control — access control based on attributes (claims) |
| Access Token | JWT authorizing access to a specific resource (API) |
| Anti-Forgery Token | Token to prevent CSRF attacks on forms |
| Audience (aud) | JWT claim identifying the intended recipient of the token |
| AuthenticationState | Object containing the ClaimsPrincipal of the current user |
| AuthenticationStateProvider | Blazor service providing authentication state to components |
| AuthorizeRouteView | Component that validates [Authorize] during routing |
| AuthorizeView | Component for conditional display based on auth state |
| BFF Pattern | Backend For Frontend — server proxy for secure WASM calls |
| Bearer Token | HTTP authorization mechanism where token is sent in Authorization header |
| Claim | Statement about a user (name, email, role, country, etc.) |
| ClaimsPrincipal | .NET representation of a user with their claims |
| Cookie | File stored in the browser containing the authentication ticket |
| CSRF | Cross-Site Request Forgery — attack exploiting site trust in user |
| DelegatingHandler | HttpClient middleware to intercept and modify requests |
| Entra ID | Microsoft’s identity service (formerly Azure Active Directory) |
| HttpContext | ASP.NET Core HTTP context containing request, response, user, etc. |
| Identity Token | JWT proving user identity (OpenID Connect) |
| IDP | Identity Provider — centralized identity provider |
| Interactive Auto | Blazor render mode combining Server (initial) and WASM (after cache) |
| Interactive Server | Blazor render mode using SignalR for UI updates |
| JWT | JSON Web Token — standard format for auth tokens |
| OAuth2 | Open authorization protocol (access delegation) |
| OIDC | OpenID Connect — identity layer on OAuth2 |
| PKCE | Proof Key for Code Exchange — OAuth2 protection against code injection |
| RBAC | Role-Based Access Control — access control based on roles |
| Redirect URI | URL the IDP redirects to after authentication |
| Refresh Token | Long-lived token to obtain a new access token without re-login |
| Scope | Permission requested from IDP (openid, profile, api://xxx/full_access) |
| Security Stamp | ASP.NET Identity hash that changes on critical account modifications |
| SignalR | ASP.NET Core technology for real-time communication via WebSocket |
| SSR | Server-Side Rendering — HTML rendered server-side |
| WASM | WebAssembly — bytecode running in the browser |
| XSS | Cross-Site Scripting — injection of malicious scripts into a web page |
Additional Resources
- Official documentation:
- JWT debugging tools: jwt.io — decode and inspect JWT tokens
- Security references: OWASP Top 10
Search Terms
asp.net · authentication · authorization · core · blazor · security · c# · .net · development · pitfall · identity · api · policies · configuration · connect · interactive · openid · tokens · architecture · bff · comparison · entra · local · modes