From 1f486592f86d15ba9559f4a8c5703252e95039f6 Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Wed, 7 Jan 2026 08:05:21 -0500 Subject: [PATCH 1/7] Opaque (reference) access token guidance --- .../security/blazor-web-app-with-oidc.md | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/aspnetcore/blazor/security/blazor-web-app-with-oidc.md b/aspnetcore/blazor/security/blazor-web-app-with-oidc.md index 0ac7cabad9b5..a0e8c97fa728 100644 --- a/aspnetcore/blazor/security/blazor-web-app-with-oidc.md +++ b/aspnetcore/blazor/security/blazor-web-app-with-oidc.md @@ -1462,6 +1462,134 @@ At this point, Razor components can adopt [role-based and policy-based authoriza * Security groups appear in `groups` claims, one claim per group. The security group GUIDs appear in the Azure portal when you create a security group and are listed when selecting **Identity** > **Overview** > **Groups** > **View**. * Built-in ME-ID administrator roles appear in `wids` claims, one claim per role. The `wids` claim with a value of `b79fbf4d-3ef9-4689-8143-76b194e85509` is always sent by ME-ID for non-guest accounts of the tenant and doesn't refer to an administrator role. Administrator role GUIDs (*role template IDs*) appear in the Azure portal when selecting **Roles & admins**, followed by the ellipsis (**…**) > **Description** for the listed role. The role template IDs are also listed in [Microsoft Entra built-in roles (Entra documentation)](/entra/identity/role-based-access-control/permissions-reference). +## Opaque (reference) access token support + + 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. Only if the opaque (reference) access token acquired by is passed to another service that attempts to validate it with results in a failure to authenticate the user for accessing the external service. Unlike self-contained JWTs, opaque tokens require a request to an authorization server to validate their status and retrieve claims. To work around this limitation, 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 an opaque token. + +> [!IMPORTANT] +> [Duende Software](https://duendesoftware.com/) isn't owned or controlled by Microsoft and might require you to pay a license fee for production use of the Duende Introspection Authentication Handler. + +The following and associated configuration and helper code is provided as a starting point for further development. The 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. + +`HttpContextExtensions.cs`: + +```csharp +namespace MinimalApiJwt.Extensions; + +public static class HttpContextExtensions +{ + 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; + } +} +``` + +`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; } +} +``` + +`OpaqueTokenAuthenticationHandler.cs`: + +```csharp +using System.Security.Claims; +using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.Options; +using MinimalApiJwt.Authentication; +using MinimalApiJwt.Extensions; + +namespace MinimalApiJwt.Services; + +public class OpaqueTokenAuthenticationHandler( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder) + : AuthenticationHandler(options, logger, encoder) +{ + protected override Task HandleAuthenticateAsync() + { + var token = Request.ExtractBearerToken(); + + if (token is null) + { + var failedResult = AuthenticateResult.Fail("Authorization failed."); + return Task.FromResult(failedResult); + } + + /* Validate the opaque (reference) access token + + Make an HTTP call to the authorization server's introspection endpoint + with the token and the API's credentials, process the response to + determine if the token is valid. + + If the token is invalid, return a failed authorization result. + + If the token is valid, create an AuthenticationTicket containing the + user's claims. + */ + + var claims = new[] { new Claim(ClaimTypes.Name, "user_id") }; + var identity = new ClaimsIdentity(claims, + OpaqueTokenAuthenticationOptions.DefaultScheme); + var principal = new ClaimsPrincipal(identity); + var ticket = new AuthenticationTicket(principal, + OpaqueTokenAuthenticationOptions.DefaultScheme); + + var result = AuthenticateResult.Success(ticket); + + return Task.FromResult(result); + } +} +``` + +In the `Program` file: + +```csharp +builder.Services.AddHttpClient(); +builder.Services.AddAuthentication() + .AddScheme( + OpaqueTokenAuthenticationOptions.DefaultScheme, + options => + { + options.IntrospectionEndpoint = "{AUTH SERVER URI}"; + options.ClientId = "{API CLIENT ID}"; + }); +``` + +The preceding example's placeholders: + +* `{AUTH SERVER URI}`: Authentication server URI +* `{API CLIENT ID}`: API Client ID + +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). + ## Alternative: Duende Access Token Management In the sample app, a custom cookie refresher (`CookieOidcRefresher.cs`) implementation is used to perform automatic non-interactive token refresh. An alternative solution can be found in the open source [`Duende.AccessTokenManagement.OpenIdConnect` package](https://docs.duendesoftware.com/accesstokenmanagement/web-apps/). From 20f592778d9d8f41ee7629ce5022fbeabc0878db Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Wed, 7 Jan 2026 08:47:09 -0500 Subject: [PATCH 2/7] Updates --- aspnetcore/blazor/security/blazor-web-app-with-oidc.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aspnetcore/blazor/security/blazor-web-app-with-oidc.md b/aspnetcore/blazor/security/blazor-web-app-with-oidc.md index a0e8c97fa728..1c776efe2e7f 100644 --- a/aspnetcore/blazor/security/blazor-web-app-with-oidc.md +++ b/aspnetcore/blazor/security/blazor-web-app-with-oidc.md @@ -1464,7 +1464,9 @@ At this point, Razor components can adopt [role-based and policy-based authoriza ## Opaque (reference) access token support - 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. Only if the opaque (reference) access token acquired by is passed to another service that attempts to validate it with results in a failure to authenticate the user for accessing the external service. Unlike self-contained JWTs, opaque tokens require a request to an authorization server to validate their status and retrieve claims. To work around this limitation, 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 an opaque token. + 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. + +Only if the opaque token acquired by is passed to another service that attempts to validate it with is there a failure to authenticate the user. Unlike self-contained JWTs, opaque tokens require a request to an authorization server to validate their status and retrieve 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/) isn't owned or controlled by Microsoft and might require you to pay a license fee for production use of the Duende Introspection Authentication Handler. From 32282c4e82623d1f684d4058425aa2d3f112d0b5 Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Wed, 7 Jan 2026 09:03:01 -0500 Subject: [PATCH 3/7] Update aspnetcore/blazor/security/blazor-web-app-with-oidc.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- aspnetcore/blazor/security/blazor-web-app-with-oidc.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aspnetcore/blazor/security/blazor-web-app-with-oidc.md b/aspnetcore/blazor/security/blazor-web-app-with-oidc.md index 1c776efe2e7f..873e206f13a8 100644 --- a/aspnetcore/blazor/security/blazor-web-app-with-oidc.md +++ b/aspnetcore/blazor/security/blazor-web-app-with-oidc.md @@ -1557,7 +1557,8 @@ public class OpaqueTokenAuthenticationHandler( user's claims. */ - var claims = new[] { new Claim(ClaimTypes.Name, "user_id") }; + // TODO: Replace "{USER ID}" with a claim value extracted from the token introspection response. + var claims = new[] { new Claim(ClaimTypes.Name, "{USER ID}") }; var identity = new ClaimsIdentity(claims, OpaqueTokenAuthenticationOptions.DefaultScheme); var principal = new ClaimsPrincipal(identity); From acbbf516ae15b5a112107a9bce50ade65884c4c6 Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Wed, 7 Jan 2026 09:03:32 -0500 Subject: [PATCH 4/7] Update aspnetcore/blazor/security/blazor-web-app-with-oidc.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- aspnetcore/blazor/security/blazor-web-app-with-oidc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aspnetcore/blazor/security/blazor-web-app-with-oidc.md b/aspnetcore/blazor/security/blazor-web-app-with-oidc.md index 873e206f13a8..3737f86c8c27 100644 --- a/aspnetcore/blazor/security/blazor-web-app-with-oidc.md +++ b/aspnetcore/blazor/security/blazor-web-app-with-oidc.md @@ -1541,7 +1541,7 @@ public class OpaqueTokenAuthenticationHandler( if (token is null) { - var failedResult = AuthenticateResult.Fail("Authorization failed."); + var failedResult = AuthenticateResult.Fail("Bearer token not found in Authorization header."); return Task.FromResult(failedResult); } From 55b6012e2a6150dbec4831173d10cfcdaa73235f Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Wed, 7 Jan 2026 09:04:46 -0500 Subject: [PATCH 5/7] Update aspnetcore/blazor/security/blazor-web-app-with-oidc.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- aspnetcore/blazor/security/blazor-web-app-with-oidc.md | 1 - 1 file changed, 1 deletion(-) diff --git a/aspnetcore/blazor/security/blazor-web-app-with-oidc.md b/aspnetcore/blazor/security/blazor-web-app-with-oidc.md index 3737f86c8c27..b24eb464e6b8 100644 --- a/aspnetcore/blazor/security/blazor-web-app-with-oidc.md +++ b/aspnetcore/blazor/security/blazor-web-app-with-oidc.md @@ -1522,7 +1522,6 @@ public class OpaqueTokenAuthenticationOptions : AuthenticationSchemeOptions using System.Security.Claims; using System.Text.Encodings.Web; using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Components; using Microsoft.Extensions.Options; using MinimalApiJwt.Authentication; using MinimalApiJwt.Extensions; From e9ec825c65488a0dff2f6fcc5f911d2922739585 Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Wed, 7 Jan 2026 09:08:08 -0500 Subject: [PATCH 6/7] Updates --- aspnetcore/blazor/security/blazor-web-app-with-oidc.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/aspnetcore/blazor/security/blazor-web-app-with-oidc.md b/aspnetcore/blazor/security/blazor-web-app-with-oidc.md index b24eb464e6b8..f9f8b0275444 100644 --- a/aspnetcore/blazor/security/blazor-web-app-with-oidc.md +++ b/aspnetcore/blazor/security/blazor-web-app-with-oidc.md @@ -1466,7 +1466,7 @@ At this point, Razor components can adopt [role-based and policy-based authoriza 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. -Only if the opaque token acquired by is passed to another service that attempts to validate it with is there a failure to authenticate the user. Unlike self-contained JWTs, opaque tokens require a request to an authorization server to validate their status and retrieve 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. +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 their status and retrieve 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/) isn't owned or controlled by Microsoft and might require you to pay a license fee for production use of the Duende Introspection Authentication Handler. @@ -1540,7 +1540,8 @@ public class OpaqueTokenAuthenticationHandler( if (token is null) { - var failedResult = AuthenticateResult.Fail("Bearer token not found in Authorization header."); + var failedResult = AuthenticateResult.Fail( + "Bearer token not found in Authorization header."); return Task.FromResult(failedResult); } @@ -1556,7 +1557,8 @@ public class OpaqueTokenAuthenticationHandler( user's claims. */ - // TODO: Replace "{USER ID}" with a claim value extracted from the token introspection response. + // TODO: Replace the '{USER ID}' placeholder with a claim value extracted + // from the token introspection response. var claims = new[] { new Claim(ClaimTypes.Name, "{USER ID}") }; var identity = new ClaimsIdentity(claims, OpaqueTokenAuthenticationOptions.DefaultScheme); From 254e5f6b726b38eb5c64930dd76da932280bde80 Mon Sep 17 00:00:00 2001 From: guardrex <1622880+guardrex@users.noreply.github.com> Date: Wed, 7 Jan 2026 09:09:59 -0500 Subject: [PATCH 7/7] Updates --- aspnetcore/blazor/security/blazor-web-app-with-oidc.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aspnetcore/blazor/security/blazor-web-app-with-oidc.md b/aspnetcore/blazor/security/blazor-web-app-with-oidc.md index f9f8b0275444..9e3af4a9ed75 100644 --- a/aspnetcore/blazor/security/blazor-web-app-with-oidc.md +++ b/aspnetcore/blazor/security/blazor-web-app-with-oidc.md @@ -1582,14 +1582,14 @@ builder.Services.AddAuthentication() OpaqueTokenAuthenticationOptions.DefaultScheme, options => { - options.IntrospectionEndpoint = "{AUTH SERVER URI}"; + options.IntrospectionEndpoint = "{AUTH SERVER INTROSPECTION URI}"; options.ClientId = "{API CLIENT ID}"; }); ``` The preceding example's placeholders: -* `{AUTH SERVER URI}`: Authentication server URI +* `{AUTH SERVER INTROSPECTION URI}`: Authentication server's introspection URI * `{API CLIENT ID}`: API Client ID 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).