Skip to content

Commit b071c5a

Browse files
committed
Move to the additional resources article and cross-link
1 parent 0897b46 commit b071c5a

4 files changed

Lines changed: 213 additions & 209 deletions

File tree

aspnetcore/blazor/security/additional-scenarios.md

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -908,3 +908,212 @@ builder.Services.AddHttpClient("HttpMessageHandler")
908908
```
909909

910910
:::moniker-end
911+
912+
## Opaque (reference) access token support
913+
914+
*The following guidance requires an authentication server that supports opaque (reference) access tokens. Currently, Microsoft Entra doesn't support opaque access token validation.*
915+
916+
<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.
917+
918+
A failure occurs only when 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>. 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.
919+
920+
> [!IMPORTANT]
921+
> [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.
922+
923+
The following <xref:Microsoft.AspNetCore.Authentication.AuthenticationHandler%601> 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 <xref:Microsoft.AspNetCore.Authentication.AuthenticationTicket> containing the user's claims.
924+
925+
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.
926+
927+
[!INCLUDE[](~/blazor/security/includes/secure-authentication-flows.md)]
928+
929+
In the following handler, the authorization server's introspection endpoint client secret uses the configuration key `Authentication:Schemes:AuthServer:ClientSecret`.
930+
931+
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 (`<UserSecretsId>` in the server app's project file):
932+
933+
```dotnetcli
934+
dotnet user-secrets init
935+
```
936+
937+
Execute the following command to set the client secret for the authorization server. The `{SECRET}` placeholder is the client secret:
938+
939+
```dotnetcli
940+
dotnet user-secrets set "Authentication:Schemes:AuthServer:ClientSecret" "{SECRET}"
941+
```
942+
943+
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**.
944+
945+
`Extensions/HttpContextExtensions.cs`:
946+
947+
```csharp
948+
namespace MinimalApiJwt.Extensions;
949+
950+
public static class HttpContextExtensions
951+
{
952+
public static string? ExtractBearerToken(this HttpRequest request)
953+
{
954+
var authorizationHeader = request.Headers["Authorization"].ToString();
955+
956+
if (!string.IsNullOrEmpty(authorizationHeader) &&
957+
authorizationHeader.StartsWith("Bearer ",
958+
StringComparison.OrdinalIgnoreCase))
959+
{
960+
var token = authorizationHeader["Bearer ".Length..].Trim();
961+
962+
if (!string.IsNullOrEmpty(token))
963+
{
964+
return token;
965+
}
966+
}
967+
968+
return null;
969+
}
970+
}
971+
```
972+
973+
`Authentication/OpaqueTokenAuthenticationOptions.cs`:
974+
975+
```csharp
976+
using Microsoft.AspNetCore.Authentication;
977+
978+
namespace MinimalApiJwt.Authentication;
979+
980+
public class OpaqueTokenAuthenticationOptions : AuthenticationSchemeOptions
981+
{
982+
public const string DefaultScheme = "OpaqueTokenAuthentication";
983+
public string? IntrospectionEndpoint { get; set; }
984+
public string? ClientId { get; set; }
985+
}
986+
```
987+
988+
`Authentication/OpaqueTokenAuthenticationHandler.cs`:
989+
990+
```csharp
991+
using System.Net.Http.Headers;
992+
using System.Security.Claims;
993+
using System.Text.Encodings.Web;
994+
using System.Text.Json;
995+
using Microsoft.AspNetCore.Authentication;
996+
using Microsoft.Extensions.Options;
997+
using MinimalApiJwt.Extensions;
998+
999+
namespace MinimalApiJwt.Authentication;
1000+
1001+
public class OpaqueTokenAuthenticationHandler(
1002+
IOptionsMonitor<OpaqueTokenAuthenticationOptions> options,
1003+
ILoggerFactory logger,
1004+
UrlEncoder encoder,
1005+
IConfiguration config,
1006+
IHttpClientFactory httpClientFactory)
1007+
: AuthenticationHandler<OpaqueTokenAuthenticationOptions>(options, logger, encoder)
1008+
{
1009+
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
1010+
{
1011+
var opaqueToken = Request.ExtractBearerToken();
1012+
1013+
if (opaqueToken is null)
1014+
{
1015+
var failedResult = AuthenticateResult.Fail(
1016+
"Bearer token not found in Authorization header.");
1017+
return failedResult;
1018+
}
1019+
1020+
/*
1021+
The following example attempts to validate the opaque
1022+
(reference) access token.
1023+
1024+
An HTTP call is made to the authorization server's introspection
1025+
endpoint with the token and the API's credentials. The response
1026+
is processed to determine if the token is valid.
1027+
1028+
If the token is valid, an AuthenticationTicket is created
1029+
containing the user's claims.
1030+
1031+
If the token is invalid, a failed authorization result is
1032+
returned.
1033+
*/
1034+
1035+
var introspectionUri = options.IntrospectionEndpoint;
1036+
var clientId = options.ClientId;
1037+
var clientSecret = config["Authentication:Schemes:AuthServer:ClientSecret"];
1038+
1039+
using var client = httpClientFactory.CreateClient();
1040+
1041+
// Set the Authorization header (base64 encoded credentials)
1042+
var authString = Convert.ToBase64String(
1043+
System.Text.Encoding.ASCII.GetBytes($"{clientId}:{clientSecret}"));
1044+
client.DefaultRequestHeaders.Authorization =
1045+
new AuthenticationHeaderValue("Basic", authString);
1046+
1047+
// Prepare the form-encoded body containing the token
1048+
var content = new FormUrlEncodedContent(
1049+
[
1050+
new KeyValuePair<string, string>("token", opaqueToken)
1051+
// NOTE: Some servers require "token_type_hint", for example
1052+
// set to "access_token"
1053+
]);
1054+
1055+
// Post to the introspection endpoint
1056+
var response = await client.PostAsync(introspectionUri, content);
1057+
1058+
if (!response.IsSuccessStatusCode)
1059+
{
1060+
var failedResult = AuthenticateResult.Fail(
1061+
"Introspection endpoint failure.");
1062+
1063+
return failedResult;
1064+
}
1065+
1066+
// Parse the JSON response
1067+
var responseString = await response.Content.ReadAsStringAsync();
1068+
using var doc = JsonDocument.Parse(responseString);
1069+
1070+
// The 'active' property determines if the token is valid and not expired
1071+
var tokenIsValid = doc.RootElement.GetProperty("active").GetBoolean();
1072+
1073+
if (tokenIsValid)
1074+
{
1075+
// TODO: Replace the '{USER ID}' placeholder with extracted claim value
1076+
// from the token introspection response
1077+
var claims = new[] { new Claim(ClaimTypes.Name, "{USER ID}") };
1078+
var identity = new ClaimsIdentity(claims,
1079+
OpaqueTokenAuthenticationOptions.DefaultScheme);
1080+
var principal = new ClaimsPrincipal(identity);
1081+
var ticket = new AuthenticationTicket(principal,
1082+
OpaqueTokenAuthenticationOptions.DefaultScheme);
1083+
1084+
var result = AuthenticateResult.Success(ticket);
1085+
1086+
return result;
1087+
}
1088+
else
1089+
{
1090+
var failedResult = AuthenticateResult.Fail("Bearer token invalid.");
1091+
1092+
return failedResult;
1093+
}
1094+
}
1095+
}
1096+
```
1097+
1098+
In the `Program` file:
1099+
1100+
```csharp
1101+
builder.Services.AddHttpClient();
1102+
builder.Services.AddAuthentication()
1103+
.AddScheme<OpaqueTokenAuthenticationOptions, OpaqueTokenAuthenticationHandler>(
1104+
OpaqueTokenAuthenticationOptions.DefaultScheme,
1105+
options =>
1106+
{
1107+
options.IntrospectionEndpoint = "{AUTH SERVER INTROSPECTION URI}";
1108+
options.ClientId = "{API CLIENT ID}";
1109+
});
1110+
```
1111+
1112+
The preceding example's placeholders:
1113+
1114+
* `{AUTH SERVER INTROSPECTION URI}`: Authentication server's introspection URI
1115+
* `{API CLIENT ID}`: API Client ID
1116+
1117+
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.
1118+
1119+
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).

aspnetcore/blazor/security/blazor-web-app-with-entra.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1162,3 +1162,4 @@ For more information on how this app secures its weather data, see [Secure data
11621162
* [`AuthenticationStateProvider` service](xref:blazor/security/index#authenticationstateprovider-service)
11631163
* [Manage authentication state in Blazor Web Apps](xref:blazor/security/index#manage-authentication-state-in-blazor-web-apps)
11641164
* [Service abstractions in Blazor Web Apps](xref:blazor/call-web-api#service-abstractions-for-web-api-calls)
1165+
* [Opaque (reference) access token support](xref:blazor/security/additional-scenarios#opaque-reference-access-token-support)

0 commit comments

Comments
 (0)