Skip to content

Commit 8b16fd9

Browse files
committed
Move to the additional resources article and cross-link
1 parent 14b0851 commit 8b16fd9

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
@@ -1063,3 +1063,212 @@ builder.Services.AddHttpClient("HttpMessageHandler")
10631063
```
10641064

10651065
:::moniker-end
1066+
1067+
## Opaque (reference) access token support
1068+
1069+
*The following guidance requires an authentication server that supports opaque (reference) access tokens. Currently, Microsoft Entra doesn't support opaque access token validation.*
1070+
1071+
<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.
1072+
1073+
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.
1074+
1075+
> [!IMPORTANT]
1076+
> [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.
1077+
1078+
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.
1079+
1080+
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.
1081+
1082+
[!INCLUDE[](~/blazor/security/includes/secure-authentication-flows.md)]
1083+
1084+
In the following handler, the authorization server's introspection endpoint client secret uses the configuration key `Authentication:Schemes:AuthServer:ClientSecret`.
1085+
1086+
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):
1087+
1088+
```dotnetcli
1089+
dotnet user-secrets init
1090+
```
1091+
1092+
Execute the following command to set the client secret for the authorization server. The `{SECRET}` placeholder is the client secret:
1093+
1094+
```dotnetcli
1095+
dotnet user-secrets set "Authentication:Schemes:AuthServer:ClientSecret" "{SECRET}"
1096+
```
1097+
1098+
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**.
1099+
1100+
`Extensions/HttpContextExtensions.cs`:
1101+
1102+
```csharp
1103+
namespace MinimalApiJwt.Extensions;
1104+
1105+
public static class HttpContextExtensions
1106+
{
1107+
public static string? ExtractBearerToken(this HttpRequest request)
1108+
{
1109+
var authorizationHeader = request.Headers["Authorization"].ToString();
1110+
1111+
if (!string.IsNullOrEmpty(authorizationHeader) &&
1112+
authorizationHeader.StartsWith("Bearer ",
1113+
StringComparison.OrdinalIgnoreCase))
1114+
{
1115+
var token = authorizationHeader["Bearer ".Length..].Trim();
1116+
1117+
if (!string.IsNullOrEmpty(token))
1118+
{
1119+
return token;
1120+
}
1121+
}
1122+
1123+
return null;
1124+
}
1125+
}
1126+
```
1127+
1128+
`Authentication/OpaqueTokenAuthenticationOptions.cs`:
1129+
1130+
```csharp
1131+
using Microsoft.AspNetCore.Authentication;
1132+
1133+
namespace MinimalApiJwt.Authentication;
1134+
1135+
public class OpaqueTokenAuthenticationOptions : AuthenticationSchemeOptions
1136+
{
1137+
public const string DefaultScheme = "OpaqueTokenAuthentication";
1138+
public string? IntrospectionEndpoint { get; set; }
1139+
public string? ClientId { get; set; }
1140+
}
1141+
```
1142+
1143+
`Authentication/OpaqueTokenAuthenticationHandler.cs`:
1144+
1145+
```csharp
1146+
using System.Net.Http.Headers;
1147+
using System.Security.Claims;
1148+
using System.Text.Encodings.Web;
1149+
using System.Text.Json;
1150+
using Microsoft.AspNetCore.Authentication;
1151+
using Microsoft.Extensions.Options;
1152+
using MinimalApiJwt.Extensions;
1153+
1154+
namespace MinimalApiJwt.Authentication;
1155+
1156+
public class OpaqueTokenAuthenticationHandler(
1157+
IOptionsMonitor<OpaqueTokenAuthenticationOptions> options,
1158+
ILoggerFactory logger,
1159+
UrlEncoder encoder,
1160+
IConfiguration config,
1161+
IHttpClientFactory httpClientFactory)
1162+
: AuthenticationHandler<OpaqueTokenAuthenticationOptions>(options, logger, encoder)
1163+
{
1164+
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
1165+
{
1166+
var opaqueToken = Request.ExtractBearerToken();
1167+
1168+
if (opaqueToken is null)
1169+
{
1170+
var failedResult = AuthenticateResult.Fail(
1171+
"Bearer token not found in Authorization header.");
1172+
return failedResult;
1173+
}
1174+
1175+
/*
1176+
The following example attempts to validate the opaque
1177+
(reference) access token.
1178+
1179+
An HTTP call is made to the authorization server's introspection
1180+
endpoint with the token and the API's credentials. The response
1181+
is processed to determine if the token is valid.
1182+
1183+
If the token is valid, an AuthenticationTicket is created
1184+
containing the user's claims.
1185+
1186+
If the token is invalid, a failed authorization result is
1187+
returned.
1188+
*/
1189+
1190+
var introspectionUri = options.IntrospectionEndpoint;
1191+
var clientId = options.ClientId;
1192+
var clientSecret = config["Authentication:Schemes:AuthServer:ClientSecret"];
1193+
1194+
using var client = httpClientFactory.CreateClient();
1195+
1196+
// Set the Authorization header (base64 encoded credentials)
1197+
var authString = Convert.ToBase64String(
1198+
System.Text.Encoding.ASCII.GetBytes($"{clientId}:{clientSecret}"));
1199+
client.DefaultRequestHeaders.Authorization =
1200+
new AuthenticationHeaderValue("Basic", authString);
1201+
1202+
// Prepare the form-encoded body containing the token
1203+
var content = new FormUrlEncodedContent(
1204+
[
1205+
new KeyValuePair<string, string>("token", opaqueToken)
1206+
// NOTE: Some servers require "token_type_hint", for example
1207+
// set to "access_token"
1208+
]);
1209+
1210+
// Post to the introspection endpoint
1211+
var response = await client.PostAsync(introspectionUri, content);
1212+
1213+
if (!response.IsSuccessStatusCode)
1214+
{
1215+
var failedResult = AuthenticateResult.Fail(
1216+
"Introspection endpoint failure.");
1217+
1218+
return failedResult;
1219+
}
1220+
1221+
// Parse the JSON response
1222+
var responseString = await response.Content.ReadAsStringAsync();
1223+
using var doc = JsonDocument.Parse(responseString);
1224+
1225+
// The 'active' property determines if the token is valid and not expired
1226+
var tokenIsValid = doc.RootElement.GetProperty("active").GetBoolean();
1227+
1228+
if (tokenIsValid)
1229+
{
1230+
// TODO: Replace the '{USER ID}' placeholder with extracted claim value
1231+
// from the token introspection response
1232+
var claims = new[] { new Claim(ClaimTypes.Name, "{USER ID}") };
1233+
var identity = new ClaimsIdentity(claims,
1234+
OpaqueTokenAuthenticationOptions.DefaultScheme);
1235+
var principal = new ClaimsPrincipal(identity);
1236+
var ticket = new AuthenticationTicket(principal,
1237+
OpaqueTokenAuthenticationOptions.DefaultScheme);
1238+
1239+
var result = AuthenticateResult.Success(ticket);
1240+
1241+
return result;
1242+
}
1243+
else
1244+
{
1245+
var failedResult = AuthenticateResult.Fail("Bearer token invalid.");
1246+
1247+
return failedResult;
1248+
}
1249+
}
1250+
}
1251+
```
1252+
1253+
In the `Program` file:
1254+
1255+
```csharp
1256+
builder.Services.AddHttpClient();
1257+
builder.Services.AddAuthentication()
1258+
.AddScheme<OpaqueTokenAuthenticationOptions, OpaqueTokenAuthenticationHandler>(
1259+
OpaqueTokenAuthenticationOptions.DefaultScheme,
1260+
options =>
1261+
{
1262+
options.IntrospectionEndpoint = "{AUTH SERVER INTROSPECTION URI}";
1263+
options.ClientId = "{API CLIENT ID}";
1264+
});
1265+
```
1266+
1267+
The preceding example's placeholders:
1268+
1269+
* `{AUTH SERVER INTROSPECTION URI}`: Authentication server's introspection URI
1270+
* `{API CLIENT ID}`: API Client ID
1271+
1272+
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.
1273+
1274+
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
@@ -1175,3 +1175,4 @@ We also recommend using a shared [Data Protection](xref:security/data-protection
11751175
* <xref:security/data-protection/configuration/overview>
11761176
* <xref:security/data-protection/implementation/key-storage-providers>
11771177
* <xref:security/data-protection/implementation/key-encryption-at-rest>
1178+
* [Opaque (reference) access token support](xref:blazor/security/additional-scenarios#opaque-reference-access-token-support)

0 commit comments

Comments
 (0)