From 9ab2a1f05d45f16499f120df7b742692b89fe037 Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Thu, 28 May 2026 16:16:46 -0400 Subject: [PATCH 1/2] Opaque (reference) access token guidance (#36588) --- .../blazor/security/additional-scenarios.md | 307 ++++++++++++++++++ .../security/blazor-web-app-with-entra.md | 3 +- .../security/blazor-web-app-with-oidc.md | 3 +- aspnetcore/blazor/security/index.md | 2 + 4 files changed, 313 insertions(+), 2 deletions(-) diff --git a/aspnetcore/blazor/security/additional-scenarios.md b/aspnetcore/blazor/security/additional-scenarios.md index c077fd2445bf..582b547ae980 100644 --- a/aspnetcore/blazor/security/additional-scenarios.md +++ b/aspnetcore/blazor/security/additional-scenarios.md @@ -1063,3 +1063,310 @@ builder.Services.AddHttpClient("HttpMessageHandler") ``` :::moniker-end + +## Opaque (reference) access token support + +The guidance in this section explains how to implement opaque (reference) access token support, which offers the following advantages over JSON Web Tokens (JWTs): + +* Strict revocation: Invalidate access tokens at any time before they naturally expire. +* Token size limits: Store a large number of user claims in the token to avoid a prohibitively large JWT. +* Security: Prevent API consumers or third parties from reading access token claims. + +> [!NOTE] +> The following guidance requires an authentication server that supports opaque (reference) access tokens. Currently, Microsoft Entra doesn't support opaque access token validation. [Keycloak](https://www.keycloak.org/) and [Okta](https://developer.okta.com/code/) issue JWT access tokens by default. The opaque token handler in this section still works against Keycloak and Okta because it relies only on RFC 7662 introspection. "Opaque" in this section describes how the client treats the token rather than how the server mints it. Alternatively, [Duende IdentityServer](https://duendesoftware.com/products/identityserver) can be configured to only issue opaque tokens. +> +> When testing this pattern against Keycloak, the API's introspection client must be a different OIDC client than the one that issued the user's access token. Introspecting a token using the client that minted it returns `{"active": false}` with "`Access token JWT check failed`" in the server's log. This doesn't happen naturally for the following scenario because the Blazor Web App and the Minimal API (`MinimalApiJwt`) are separate clients. + + supports opaque tokens because it doesn't perform access token validation when configured for Proof Key for Code Exchange (PKCE) authorization code flow. It relies on the ASP.NET Core server's HTTPS backchannel to the OIDC authentication service to obtain the ID token using the authorization code received when the user redirects back to the ASP.NET Core app after signing in. If the app is only required to log a user in with OIDC to get a valid authentication cookie, opaque access tokens are supported without modifying the app. + +A failure occurs only when the opaque token acquired by is passed to another service that attempts to validate it with . Unlike self-contained JWTs, opaque tokens require a request to an authorization server to validate the status and to retrieve the claims. To work around this limitation, either use a third-party API, such as the [Duende Introspection Authentication Handler](https://docs.duendesoftware.com/introspection/), or create a [custom `AuthenticationHandler`](xref:security/authentication/index#authentication-handler) to validate the token. + +> [!IMPORTANT] +> [Duende Software](https://duendesoftware.com/) and [Okta](https://www.okta.com) aren't owned or controlled by Microsoft and might require you to pay a license fee for production use of their services and libraries. + +The following and associated configuration and helper code is provided as a general approach, which might require further development to suit a specific authorization server's requirements. The following handler extracts the opaque token from the `Authorization` header for an HTTP call to an authorization server's introspection endpoint and creates an containing the user's claims. + +Calling an authorization server's introspection endpoint requires authentication. The following example relies on setting the client secret for authentication in the request's Authorization header (base64 encoded credentials) using the [Secret Manager tool](xref:security/app-secrets) for local development and testing. + +[!INCLUDE[](~/blazor/security/includes/secure-authentication-flows.md)] + +In the following handler, the authorization server's introspection endpoint client secret uses the configuration key `Authentication:Schemes:OpaqueTokenAuthentication:ClientSecret`. For production apps, consider using *client assertions*. For more information, see [Confidential client assertions (Microsoft Entra documentation)](/entra/msal/dotnet/acquiring-tokens/web-apps-apis/confidential-client-assertions). + +If the Blazor server project hasn't been initialized for the Secret Manager tool, use a command shell, such as the Developer PowerShell command shell in Visual Studio, to execute the following command. Before executing the command, change the directory with the `cd` command to the server project's directory. The command establishes a user secrets identifier (`` in the server app's project file): + +```dotnetcli +dotnet user-secrets init +``` + +Execute the following command to set the client secret for the authorization server. The `{SECRET}` placeholder is the client secret: + +```dotnetcli +dotnet user-secrets set "Authentication:Schemes:OpaqueTokenAuthentication:ClientSecret" "{SECRET}" +``` + +If using Visual Studio, you can confirm the secret is set by right-clicking the server project in **Solution Explorer** and selecting **Manage User Secrets**. + +`Extensions/HttpRequestExtensions.cs`: + +```csharp +namespace MinimalApiJwt.Extensions; + +public static class HttpRequestExtensions +{ + public static string? ExtractBearerToken(this HttpRequest request) + { + var authorizationHeader = request.Headers.Authorization.ToString(); + + if (!string.IsNullOrEmpty(authorizationHeader) && + authorizationHeader.StartsWith("Bearer ", + StringComparison.OrdinalIgnoreCase)) + { + var token = authorizationHeader["Bearer ".Length..].Trim(); + + if (!string.IsNullOrEmpty(token)) + { + return token; + } + } + + return null; + } +} +``` + +`Authentication/OpaqueTokenAuthenticationOptions.cs`: + +```csharp +using Microsoft.AspNetCore.Authentication; + +namespace MinimalApiJwt.Authentication; + +public class OpaqueTokenAuthenticationOptions : AuthenticationSchemeOptions +{ + public const string DefaultScheme = "OpaqueTokenAuthentication"; + public string? IntrospectionEndpoint { get; set; } + public string? ClientId { get; set; } + public string? ClientSecret { get; set; } +} +``` + +The following handler attempts to validate an opaque (reference) access token. An HTTP call is made to the authorization server's introspection endpoint with the token and the API's credentials. The response is processed to determine if the token is valid: + +* If the token is valid, an is created containing the user's claims. +* If the token is invalid, a failed authorization result is returned. + +The handler's options (`Options`) is an instance of `OpaqueTokenAuthenticationOptions` provided by the base type, which is configured in the app's `Program` file with the authorization server's introspection endpoint and the API's client ID. The API's client secret is provided by the Secret Manager tool during development. + +`IOptionsMonitor` (`optionsMonitor`) isn't used directly by the handler, but it could be used to support dynamic configuration changes at runtime. + +For the request's content in , some servers require a token type hint (`token_type_hint`). For example, the required value might be `access_token`. See your authentication server's documentation for details. + +`Authentication/OpaqueTokenAuthenticationHandler.cs`: + +```csharp +using System.Net.Http.Headers; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Text.Json; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Options; +using MinimalApiJwt.Extensions; + +namespace MinimalApiJwt.Authentication; + +public class OpaqueTokenAuthenticationHandler( + IOptionsMonitor optionsMonitor, + ILoggerFactory logger, + UrlEncoder encoder, + IHttpClientFactory httpClientFactory) + : AuthenticationHandler(optionsMonitor, + logger, encoder) +{ + protected override async Task HandleAuthenticateAsync() + { + var opaqueToken = Request.ExtractBearerToken(); + + if (opaqueToken is null) + { + var failedResult = AuthenticateResult.Fail( + "Bearer token not found in Authorization header."); + return failedResult; + } + + var introspectionUri = Options.IntrospectionEndpoint; + var clientId = Options.ClientId; + var clientSecret = Options.ClientSecret; + + if (string.IsNullOrWhiteSpace(introspectionUri) || + string.IsNullOrWhiteSpace(clientId) || + string.IsNullOrWhiteSpace(clientSecret)) + { + var failedResult = AuthenticateResult.Fail( + "Opaque token authentication isn't fully configured."); + return failedResult; + } + + using var client = httpClientFactory.CreateClient(); + + // Set the Authorization header (base64 encoded credentials) + var authString = Convert.ToBase64String( + System.Text.Encoding.ASCII.GetBytes($"{clientId}:{clientSecret}")); + client.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Basic", authString); + + // Prepare the form-encoded body containing the token + var content = new FormUrlEncodedContent( + [ + new KeyValuePair("token", opaqueToken) + ]); + + // Post to the introspection endpoint + var response = await client.PostAsync(introspectionUri, content); + + if (!response.IsSuccessStatusCode) + { + var failedResult = AuthenticateResult.Fail( + "Introspection endpoint failure."); + + return failedResult; + } + + // Parse the JSON response + var responseString = await response.Content.ReadAsStringAsync(); + using var doc = JsonDocument.Parse(responseString); + + // The 'active' property determines if the token is valid and not expired + var tokenIsValid = + doc.RootElement.TryGetProperty("active", out var activeProperty) && + activeProperty.ValueKind == JsonValueKind.True; + + if (tokenIsValid) + { + // Map standard introspection response fields onto claims. + // Field names below match what Keycloak, Duende IdentityServer, + // Auth0, and Okta return; adjust the role source for your provider. + var claims = new List(); + + string? Get(string name) => + doc.RootElement.TryGetProperty(name, out var v) && + v.ValueKind == JsonValueKind.String ? v.GetString() : null; + + var sub = Get("sub"); + var username = Get("preferred_username") ?? Get("username") ?? sub; + + if (sub is not null) claims.Add(new Claim(ClaimTypes.NameIdentifier, sub)); + if (username is not null) claims.Add(new Claim(ClaimTypes.Name, username)); + if (Get("email") is { } email) claims.Add(new Claim(ClaimTypes.Email, email)); + if ((Get("client_id") ?? Get("azp")) is { } cid) + claims.Add(new Claim("client_id", cid)); + if (Get("scope") is { } scope) + foreach (var s in scope.Split(' ', StringSplitOptions.RemoveEmptyEntries)) + claims.Add(new Claim("scope", s)); + + // Keycloak surfaces realm roles under realm_access.roles. + // Duende/IdentityServer uses a flat "role" claim; Auth0 uses a + // configurable custom claim. Adjust for your authorization server. + if (doc.RootElement.TryGetProperty("realm_access", out var ra) && + ra.ValueKind == JsonValueKind.Object && + ra.TryGetProperty("roles", out var roles) && + roles.ValueKind == JsonValueKind.Array) + { + foreach (var r in roles.EnumerateArray()) + if (r.ValueKind == JsonValueKind.String) + claims.Add(new Claim(ClaimTypes.Role, r.GetString()!)); + } + + var identity = new ClaimsIdentity(claims, + OpaqueTokenAuthenticationOptions.DefaultScheme, + nameType: ClaimTypes.Name, + roleType: ClaimTypes.Role); + var principal = new ClaimsPrincipal(identity); + var ticket = new AuthenticationTicket(principal, + OpaqueTokenAuthenticationOptions.DefaultScheme); + + var result = AuthenticateResult.Success(ticket); + + return result; + } + else + { + var failedResult = AuthenticateResult.Fail("Bearer token invalid."); + + return failedResult; + } + } +} +``` + +> [!NOTE] +> The preceding approach can be further improved by using the OpenID Connect discovery endpoint and adding a cache for the client's introspection requests. + +In the `Program` file: + +```csharp +using MinimalApiJwt.Authentication; + +... + +builder.Services.AddHttpClient(); +builder.Services.AddAuthentication() + .AddScheme( + OpaqueTokenAuthenticationOptions.DefaultScheme, + options => + { + options.IntrospectionEndpoint = "{AUTH SERVER INTROSPECTION URI}"; + options.ClientId = "{API CLIENT ID}"; + options.ClientSecret = + builder.Configuration[ + "Authentication:Schemes:OpaqueTokenAuthentication:ClientSecret"]; + }); +``` + +The preceding example's placeholders: + +* `{AUTH SERVER INTROSPECTION URI}`: Authentication server's introspection URI +* `{API CLIENT ID}`: API client ID + +Values for the authentication server introspection URI (`{AUTH SERVER INTROSPECTION URI}`) and the API client ID (`{API CLIENT ID}`) can be supplied from app settings or any other configuration source. + +Tokens are typically invalidated on a logout event using the revocation endpoint. The following example is a starting point for further development: + +```csharp +app.MapPost("/logout", + async ([FromForm] string? returnUrl, HttpContext context, + IHttpClientFactory httpClientFactory) => +{ + var accessToken = await context.GetTokenAsync("access_token"); + + if (!string.IsNullOrEmpty(accessToken)) + { + // Prepare the revocation request (RFC 7009) + var content = + new FormUrlEncodedContent(new Dictionary + { + { "token", accessToken }, + { "token_type_hint", "access_token" }, + { "client_id", "{API CLIENT ID}" }, + { "client_secret", "{CLIENT SECRET}" } + }); + + // POST to the revocation endpoint + using var client = httpClientFactory.CreateClient(); + await client.PostAsync("{AUTH SERVER TOKEN REVOCATION URI}", content); + } + + return TypedResults.SignOut(new AuthenticationProperties { RedirectUri = "{REDIRECT URI}" }, + [CookieAuthenticationDefaults.AuthenticationScheme]); +}); +``` + +The preceding example's placeholders: + +* `{AUTH SERVER TOKEN REVOCATION URI}`: The authentication server's token revocation URI. +* `{API CLIENT ID}`: The API client ID. +* `{CLIENT SECRET}`: The client secret obtained securely. +* `{REDIRECT URI}`: The redirect URI. + +In [Duende IdentityServer](https://duendesoftware.com/products/identityserver), tokens are revoked automatically by setting the `CoordinateLifetimeWithUserSession` client configuration property to `true`, which automatically cleans up associated tokens when a session ends. For more information, see [Session Cleanup and Logout (Duende documentation)](https://docs.duendesoftware.com/identityserver/ui/logout/session-cleanup/). + +Built-in opaque access token support is under consideration for a future release of .NET. For more information, see [Opaque - reference token validation (`dotnet/aspnetcore` #46026)](https://github.com/dotnet/aspnetcore/issues/46026). diff --git a/aspnetcore/blazor/security/blazor-web-app-with-entra.md b/aspnetcore/blazor/security/blazor-web-app-with-entra.md index 2d89af4fe931..58a237bbe51c 100644 --- a/aspnetcore/blazor/security/blazor-web-app-with-entra.md +++ b/aspnetcore/blazor/security/blazor-web-app-with-entra.md @@ -85,7 +85,7 @@ For more information on using Aspire and details on the `.AppHost` and `.Service Confirm that you've met the prerequisites for Aspire. For more information, see the *Prerequisites* section of [Quickstart: Build your first Aspire solution](/dotnet/aspire/get-started/build-your-first-aspire-app?tabs=visual-studio#prerequisites). -The sample app only configures an insecure HTTP launch profile (`http`) for use during development testing. For more information, including an example of insecure and secure launch settings profiles, see [Allow unsecure transport in Aspire (Aspire documentation)](/dotnet/aspire/troubleshooting/allow-unsecure-transport). +The sample app only configures an insecure HTTP launch profile (`http`) for use during development testing. ## Server-side Blazor Web App project (`BlazorWebAppEntra`) @@ -1239,3 +1239,4 @@ For more information, see [Access tokens in the Microsoft identity platform: Tok * * * +* [Opaque (reference) access token support](xref:blazor/security/additional-scenarios#opaque-reference-access-token-support) diff --git a/aspnetcore/blazor/security/blazor-web-app-with-oidc.md b/aspnetcore/blazor/security/blazor-web-app-with-oidc.md index 94b57fe3b2bf..2e100452f145 100644 --- a/aspnetcore/blazor/security/blazor-web-app-with-oidc.md +++ b/aspnetcore/blazor/security/blazor-web-app-with-oidc.md @@ -126,7 +126,7 @@ For more information on using Aspire and details on the `.AppHost` and `.Service Confirm that you've met the prerequisites for Aspire. For more information, see the *Prerequisites* section of [Quickstart: Build your first Aspire solution](/dotnet/aspire/get-started/build-your-first-aspire-app?tabs=visual-studio#prerequisites). -The sample app only configures an insecure HTTP launch profile (`http`) for use during development testing. For more information, including an example of insecure and secure launch settings profiles, see [Allow unsecure transport in Aspire (Aspire documentation)](/dotnet/aspire/troubleshooting/allow-unsecure-transport). +The sample app only configures an insecure HTTP launch profile (`http`) for use during development testing. ## `MinimalApiJwt` project @@ -1525,3 +1525,4 @@ We also recommend using a shared [Data Protection](xref:security/data-protection * [Refresh token during http request in Blazor Interactive Server with OIDC (`dotnet/aspnetcore` #55213)](https://github.com/dotnet/aspnetcore/issues/55213) * [Secure data in Blazor Web Apps with Interactive Auto rendering](xref:blazor/security/index#secure-data-in-blazor-web-apps-with-interactive-auto-rendering) * [How to access an `AuthenticationStateProvider` from a `DelegatingHandler`](xref:blazor/security/additional-scenarios#access-authenticationstateprovider-in-outgoing-request-middleware) +* [Opaque (reference) access token support](xref:blazor/security/additional-scenarios#opaque-reference-access-token-support) diff --git a/aspnetcore/blazor/security/index.md b/aspnetcore/blazor/security/index.md index ce0a1f516a15..58366c7c59fe 100644 --- a/aspnetcore/blazor/security/index.md +++ b/aspnetcore/blazor/security/index.md @@ -1708,6 +1708,7 @@ PII refers any information relating to an identified or identifiable natural per * [Build a custom version of the Authentication.MSAL JavaScript library](xref:blazor/security/webassembly/additional-scenarios#build-a-custom-version-of-the-authenticationmsal-javascript-library) * [Awesome Blazor: Authentication](https://github.com/AdrienTorris/awesome-blazor#authentication) community sample links * +* [Opaque (reference) access token support](xref:blazor/security/additional-scenarios#opaque-reference-access-token-support) :::moniker-end @@ -1730,5 +1731,6 @@ PII refers any information relating to an identified or identifiable natural per * * [Build a custom version of the Authentication.MSAL JavaScript library](xref:blazor/security/webassembly/additional-scenarios#build-a-custom-version-of-the-authenticationmsal-javascript-library) * [Awesome Blazor: Authentication](https://github.com/AdrienTorris/awesome-blazor#authentication) community sample links +* [Opaque (reference) access token support](xref:blazor/security/additional-scenarios#opaque-reference-access-token-support) :::moniker-end From f61161442a418acac48274ac43077be1a5d3fa4e Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Thu, 28 May 2026 17:10:25 -0400 Subject: [PATCH 2/2] Enhance sign-out guidance (#37198) --- aspnetcore/blazor/security/index.md | 89 +++++++++++++++++++++++++++-- 1 file changed, 83 insertions(+), 6 deletions(-) diff --git a/aspnetcore/blazor/security/index.md b/aspnetcore/blazor/security/index.md index 58366c7c59fe..77341d01f323 100644 --- a/aspnetcore/blazor/security/index.md +++ b/aspnetcore/blazor/security/index.md @@ -1,5 +1,6 @@ --- title: ASP.NET Core Blazor authentication and authorization +ai-usage: ai-assisted author: guardrex description: Learn about Blazor authentication and authorization scenarios. monikerRange: '>= aspnetcore-3.1' @@ -535,21 +536,97 @@ Two additional abstractions participate in managing authentication state: * ([reference source](https://github.com/dotnet/aspnetcore/blob/main/src/Components/Endpoints/src/DependencyInjection/ServerAuthenticationStateProvider.cs)): An used by the Blazor framework to obtain authentication state from the server. -* ([reference source](https://github.com/dotnet/aspnetcore/blob/main/src/Components/Server/src/Circuits/RevalidatingServerAuthenticationStateProvider.cs)): A base class for services used by the Blazor framework to receive an authentication state from the host environment and revalidate it at regular intervals. +* ([reference source](https://github.com/dotnet/aspnetcore/blob/main/src/Components/Server/src/Circuits/RevalidatingServerAuthenticationStateProvider.cs)): A base class for services used by the Blazor framework to receive an authentication state from the host environment and revalidate it at regular intervals, 30 minutes by default. [!INCLUDE[](~/includes/aspnetcore-repo-ref-source-links.md)] -In apps generated from the Blazor project template for .NET 8 or later, adjust the default 30 minute revalidation interval in `IdentityRevalidatingAuthenticationStateProvider`. Earlier than .NET 8, adjust the interval in `RevalidatingIdentityAuthenticationStateProvider`. The following example shortens the interval to 20 minutes: +### Authentication state management at sign out + +The default revalidation interval is 30 minutes for either ASP.NET Core Identity-based authentication or cookie-based authentication without Identity. Within the 30-minute window, it remains possible under certain sign-out conditions for a user to retain access to areas of the app that you might wish to prevent. + +To control the revalidation period and enforce a complete sign out process for users, begin by implementing a with a shorter . + +For an example implementation showing the default 30-minute interval, see the [`IdentityRevalidatingAuthenticationStateProvider` class (reference source)](https://github.com/dotnet/aspnetcore/blob/main/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Components/Account/IdentityRevalidatingAuthenticationStateProvider.cs) in the Blazor Web App project template. + +In the following example, the interval is set to five minutes: ```csharp -protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(20); +protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(5); ``` -### Authentication state management at sign out +Implementing with a short only revalidates the authentication state held by the current Blazor circuit. Returning `false` from flips the circuit's state to unauthenticated, so instances of / re-evaluate and redirect the user to sign in. However, returning `false` from doesn't affect the underlying authentication cookie. The next full navigation that occurs before the cookie expires or is invalidated recreates the principal from the cookie. When that happens, the cookie indicates a signed-in user to the , so the user appears signed in until the next tick fires. -Server-side Blazor persists user authentication state for the lifetime of the circuit, including across browser tabs. To proactively sign off a user across browser tabs when the user signs out on one tab, you must implement a ([reference source](https://github.com/dotnet/aspnetcore/blob/main/src/Components/Server/src/Circuits/RevalidatingServerAuthenticationStateProvider.cs)) with a short . +> [!NOTE] +> Each browser tab requires a separate circuit, so additional tabs opened by a user don't observe an authentication state change until their circuits revalidate. However, the approach in this section sets the revalidation interval for all of the tabs opened by a user. -[!INCLUDE[](~/includes/aspnetcore-repo-ref-source-links.md)] +To control the revalidation interval in apps that adopt ASP.NET Core Identity with cookie authentication, see the following [Sign out for ASP.NET Core Identity](#sign-out-for-aspnet-core-identity) subsection for details. For apps that adopt cookie-based authentication without Identity, see the following [Sign out for cookie-based authentication](#sign-out-for-cookie-based-authentication) subsection. + + +#### Sign out for ASP.NET Core Identity + +To force a complete sign-out within less than the default 30-minute revalidation interval in apps that adopt ASP.NET Core Identity, use the guidance in this section. + +For Blazor apps that target .NET 8 or later, reduce the default 30-minute in the `IdentityRevalidatingAuthenticationStateProvider` class (`Components/Account/IdentityRevalidatingAuthenticationStateProvider.cs`). If the app targets .NET earlier than .NET 8, reduce the interval in `RevalidatingIdentityAuthenticationStateProvider`. + +Whether or not the authentication cookie remains valid is checked by the *security stamp validator* (), which hooks into the event of an authentication cookie and queries the user datastore to determine if the user is still signed in. The security stamp validator's interval is governed by , which defaults to 30 minutes because validating users on every request triggers a database query on every request for every user. + +The following example shortens the default 30-minute interval to four minutes: + +```csharp +builder.Services.Configure( + o => o.ValidationInterval = TimeSpan.FromMinutes(4)); +``` + +The call interval is a tradeoff between hitting the user datastore too frequently and not often enough. Checking with a short interval can result in high demand on the user datastore and reduced app performance but with the benefit of more timely sign-outs. Checking with a long interval results in stale claims, which can make it appear that a user is still signed in if a full navigation recreates the principal from the authentication cookie before it naturally expires. + +To catch the next interval tick of the , set the to a period just inside the value set for . + +Custom C# code that's required to force a sign out on a user can call , which immediately invalidates existing cookies the next time they're checked. + +For more information, see . + +#### Sign out for cookie-based authentication + +To proactively, completely sign a user off within less than the default 30-minute revalidation interval in apps that adopt cookie-based authentication without ASP.NET Core Identity, use the guidance in this section. + +There are two approaches that you can take. The first approach is to wait for a revalidation check to occur and ensure cookie invalidation when the check is made. To adopt this approach, pair an implementation of with a shorter (default: 30 minutes) and a sign-out trigger. Implement the sign-out trigger using ***either*** of the following approaches. + +* Sign out on GET in the app's login page. This approach is only valid for static SSR because is `null` during interactive rendering: + + ```csharp + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + protected override async Task OnInitializedAsync() + { + ... + + if (HttpMethods.IsGet(HttpContext.Request.Method)) + { + await HttpContext.SignOutAsync( + CookieAuthenticationDefaults.AuthenticationScheme); + } + } + ``` + +* Call `NavigationManager.NavigateTo("/Account/Logout", forceLoad: true)` where you sign out users. Create an endpoint for `/Account/Logout` with a call to in the app's `Program` file, which in turn calls : + + ```csharp + app.MapGet("/Account/Logout", async (HttpContext context) => + { + await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + + return TypedResults.LocalRedirect("/"); + }) + .RequireAuthorization(); + ``` + +The alternative second approach is aimed at first-request freshness without waiting for the next revalidation check. To adopt this approach, perform a user datastore authentication check in and call with . + +For more information, see the following sections of the *Use cookie authentication without ASP.NET Core Identity* article: + +* [Sign out](xref:security/authentication/cookie#sign-out) +* [React to back-end changes](xref:security/authentication/cookie#react-to-back-end-changes) :::moniker range=">= aspnetcore-8.0"