Skip to content
Open
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions aspnetcore/blazor/security/blazor-web-app-with-oidc.md
Original file line number Diff line number Diff line change
Expand Up @@ -1462,6 +1462,136 @@ 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

<xref:Microsoft.Extensions.DependencyInjection.OpenIdConnectExtensions.AddOpenIdConnect%2A> 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 <xref:Microsoft.Extensions.DependencyInjection.OpenIdConnectExtensions.AddOpenIdConnect%2A> is passed to another service that attempts to validate it with <xref:Microsoft.Extensions.DependencyInjection.JwtBearerExtensions.AddJwtBearer%2A> 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.
Comment thread
guardrex marked this conversation as resolved.
Outdated

> [!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 <xref:Microsoft.AspNetCore.Authentication.AuthenticationHandler%601> 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 <xref:Microsoft.AspNetCore.Authentication.AuthenticationTicket> containing the user's claims.

`HttpContextExtensions.cs`:

```csharp
namespace MinimalApiJwt.Extensions;
Comment thread
guardrex marked this conversation as resolved.

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;
Comment thread
guardrex marked this conversation as resolved.

public class OpaqueTokenAuthenticationOptions : AuthenticationSchemeOptions
{
public const string DefaultScheme = "OpaqueTokenAuthentication";
public string? IntrospectionEndpoint { get; set; }
public string? ClientId { get; set; }
Comment thread
guardrex marked this conversation as resolved.
}
```

`OpaqueTokenAuthenticationHandler.cs`:

```csharp
using System.Security.Claims;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Components;
Comment thread
guardrex marked this conversation as resolved.
Outdated
using Microsoft.Extensions.Options;
using MinimalApiJwt.Authentication;
using MinimalApiJwt.Extensions;

namespace MinimalApiJwt.Services;
Comment thread
guardrex marked this conversation as resolved.
Comment thread
guardrex marked this conversation as resolved.

public class OpaqueTokenAuthenticationHandler(
IOptionsMonitor<OpaqueTokenAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder)
: AuthenticationHandler<OpaqueTokenAuthenticationOptions>(options, logger, encoder)
{
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var token = Request.ExtractBearerToken();

if (token is null)
{
var failedResult = AuthenticateResult.Fail("Authorization failed.");
Comment thread
guardrex marked this conversation as resolved.
Outdated
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") };
Comment thread
guardrex marked this conversation as resolved.
Outdated
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);
Comment thread
guardrex marked this conversation as resolved.
}
}
```

In the `Program` file:

```csharp
builder.Services.AddHttpClient();
builder.Services.AddAuthentication()
.AddScheme<OpaqueTokenAuthenticationOptions, OpaqueTokenAuthenticationHandler>(
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
Comment thread
guardrex marked this conversation as resolved.
Outdated

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).
Comment thread
guardrex marked this conversation as resolved.

## 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/).
Expand Down