24 changed files with 2043 additions and 47 deletions
@ -0,0 +1,133 @@ |
|||||
|
/* |
||||
|
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
* See https://github.com/openiddict/openiddict-core for more information concerning
|
||||
|
* the license and the contributors participating to this project. |
||||
|
*/ |
||||
|
|
||||
|
using System.Collections.Immutable; |
||||
|
using System.Diagnostics; |
||||
|
using System.Net.Http; |
||||
|
using System.Net.Http.Headers; |
||||
|
using System.Text; |
||||
|
|
||||
|
namespace OpenIddict.Client.SystemNetHttp; |
||||
|
|
||||
|
public static partial class OpenIddictClientSystemNetHttpHandlers |
||||
|
{ |
||||
|
public static class Introspection |
||||
|
{ |
||||
|
public static ImmutableArray<OpenIddictClientHandlerDescriptor> DefaultHandlers { get; } = [ |
||||
|
/* |
||||
|
* Introspection request processing: |
||||
|
*/ |
||||
|
CreateHttpClient<PrepareIntrospectionRequestContext>.Descriptor, |
||||
|
PreparePostHttpRequest<PrepareIntrospectionRequestContext>.Descriptor, |
||||
|
AttachHttpVersion<PrepareIntrospectionRequestContext>.Descriptor, |
||||
|
AttachJsonAcceptHeaders<PrepareIntrospectionRequestContext>.Descriptor, |
||||
|
AttachUserAgentHeader<PrepareIntrospectionRequestContext>.Descriptor, |
||||
|
AttachFromHeader<PrepareIntrospectionRequestContext>.Descriptor, |
||||
|
AttachBasicAuthenticationCredentials.Descriptor, |
||||
|
AttachHttpParameters<PrepareIntrospectionRequestContext>.Descriptor, |
||||
|
SendHttpRequest<ApplyIntrospectionRequestContext>.Descriptor, |
||||
|
DisposeHttpRequest<ApplyIntrospectionRequestContext>.Descriptor, |
||||
|
|
||||
|
/* |
||||
|
* Introspection response processing: |
||||
|
*/ |
||||
|
DecompressResponseContent<ExtractIntrospectionResponseContext>.Descriptor, |
||||
|
ExtractJsonHttpResponse<ExtractIntrospectionResponseContext>.Descriptor, |
||||
|
ExtractWwwAuthenticateHeader<ExtractIntrospectionResponseContext>.Descriptor, |
||||
|
ValidateHttpResponse<ExtractIntrospectionResponseContext>.Descriptor, |
||||
|
DisposeHttpResponse<ExtractIntrospectionResponseContext>.Descriptor |
||||
|
]; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Contains the logic responsible for attaching the client credentials to the HTTP Authorization header.
|
||||
|
/// </summary>
|
||||
|
public sealed class AttachBasicAuthenticationCredentials : IOpenIddictClientHandler<PrepareIntrospectionRequestContext> |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Gets the default descriptor definition assigned to this handler.
|
||||
|
/// </summary>
|
||||
|
public static OpenIddictClientHandlerDescriptor Descriptor { get; } |
||||
|
= OpenIddictClientHandlerDescriptor.CreateBuilder<PrepareIntrospectionRequestContext>() |
||||
|
.AddFilter<RequireHttpMetadataUri>() |
||||
|
.UseSingletonHandler<AttachBasicAuthenticationCredentials>() |
||||
|
.SetOrder(AttachHttpParameters<PrepareIntrospectionRequestContext>.Descriptor.Order - 500) |
||||
|
.SetType(OpenIddictClientHandlerType.BuiltIn) |
||||
|
.Build(); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public ValueTask HandleAsync(PrepareIntrospectionRequestContext context) |
||||
|
{ |
||||
|
if (context is null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(context)); |
||||
|
} |
||||
|
|
||||
|
Debug.Assert(context.Request is not null, SR.GetResourceString(SR.ID4008)); |
||||
|
|
||||
|
// This handler only applies to System.Net.Http requests. If the HTTP request cannot be resolved,
|
||||
|
// this may indicate that the request was incorrectly processed by another client stack.
|
||||
|
var request = context.Transaction.GetHttpRequestMessage() ?? |
||||
|
throw new InvalidOperationException(SR.GetResourceString(SR.ID0173)); |
||||
|
|
||||
|
// The OAuth 2.0 specification recommends sending the client credentials using basic authentication.
|
||||
|
// However, this authentication method is known to have severe compatibility/interoperability issues:
|
||||
|
//
|
||||
|
// - While restricted to clients that have been given a secret (i.e confidential clients) by the
|
||||
|
// specification, basic authentication is also sometimes required by server implementations for
|
||||
|
// public clients that don't have a client secret: in this case, an empty password is used and
|
||||
|
// the client identifier is sent alone in the Authorization header (instead of being sent using
|
||||
|
// the standard "client_id" parameter present in the request body).
|
||||
|
//
|
||||
|
// - While the OAuth 2.0 specification requires that the client credentials be formURL-encoded
|
||||
|
// before being base64-encoded, many implementations are known to implement a non-standard
|
||||
|
// encoding scheme, where neither the client_id nor the client_secret are formURL-encoded.
|
||||
|
//
|
||||
|
// To guarantee that the OpenIddict implementation can be used with most servers implementions,
|
||||
|
// basic authentication is only used when a client secret is present and client_secret_post is
|
||||
|
// always preferred when it's explicitly listed as a supported client authentication method.
|
||||
|
// If client_secret_post is not listed or if the server returned an empty methods list,
|
||||
|
// client_secret_basic is always used, as it MUST be implemented by all OAuth 2.0 servers.
|
||||
|
//
|
||||
|
// See https://tools.ietf.org/html/rfc8414#section-2
|
||||
|
// and https://tools.ietf.org/html/rfc6749#section-2.3.1 for more information.
|
||||
|
if (request.Headers.Authorization is null && |
||||
|
!string.IsNullOrEmpty(context.Request.ClientId) && |
||||
|
!string.IsNullOrEmpty(context.Request.ClientSecret) && |
||||
|
UseBasicAuthentication(context.Configuration)) |
||||
|
{ |
||||
|
// Important: the credentials MUST be formURL-encoded before being base64-encoded.
|
||||
|
var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes(new StringBuilder() |
||||
|
.Append(EscapeDataString(context.Request.ClientId)) |
||||
|
.Append(':') |
||||
|
.Append(EscapeDataString(context.Request.ClientSecret)) |
||||
|
.ToString())); |
||||
|
|
||||
|
// Attach the authorization header containing the client credentials to the HTTP request.
|
||||
|
request.Headers.Authorization = new AuthenticationHeaderValue(Schemes.Basic, credentials); |
||||
|
|
||||
|
// Remove the client credentials from the request payload to ensure they are not sent twice.
|
||||
|
context.Request.ClientId = context.Request.ClientSecret = null; |
||||
|
} |
||||
|
|
||||
|
return default; |
||||
|
|
||||
|
static bool UseBasicAuthentication(OpenIddictConfiguration configuration) |
||||
|
=> configuration.IntrospectionEndpointAuthMethodsSupported switch |
||||
|
{ |
||||
|
// If at least one authentication method was explicit added, only use basic authentication
|
||||
|
// if it's supported AND if client_secret_post is not supported or enabled by the server.
|
||||
|
{ Count: > 0 } methods => methods.Contains(ClientAuthenticationMethods.ClientSecretBasic) && |
||||
|
!methods.Contains(ClientAuthenticationMethods.ClientSecretPost), |
||||
|
|
||||
|
// Otherwise, if no authentication method was explicit added, assume only basic is supported.
|
||||
|
{ Count: _ } => true |
||||
|
}; |
||||
|
|
||||
|
static string EscapeDataString(string value) => Uri.EscapeDataString(value).Replace("%20", "+"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,153 @@ |
|||||
|
/* |
||||
|
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
* See https://github.com/openiddict/openiddict-core for more information concerning
|
||||
|
* the license and the contributors participating to this project. |
||||
|
*/ |
||||
|
|
||||
|
using System.Security.Claims; |
||||
|
|
||||
|
namespace OpenIddict.Client; |
||||
|
|
||||
|
public static partial class OpenIddictClientEvents |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Represents an event called for each request to the introspection endpoint
|
||||
|
/// to give the user code a chance to add parameters to the introspection request.
|
||||
|
/// </summary>
|
||||
|
public sealed class PrepareIntrospectionRequestContext : BaseExternalContext |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Creates a new instance of the <see cref="PrepareIntrospectionRequestContext"/> class.
|
||||
|
/// </summary>
|
||||
|
public PrepareIntrospectionRequestContext(OpenIddictClientTransaction transaction) |
||||
|
: base(transaction) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the request.
|
||||
|
/// </summary>
|
||||
|
public OpenIddictRequest Request |
||||
|
{ |
||||
|
get => Transaction.Request!; |
||||
|
set => Transaction.Request = value; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the token sent to the introspection endpoint.
|
||||
|
/// </summary>
|
||||
|
public string? Token |
||||
|
{ |
||||
|
get => Request.Token; |
||||
|
set => Request.Token = value; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the token type sent to the introspection endpoint.
|
||||
|
/// </summary>
|
||||
|
public string? TokenTypeHint |
||||
|
{ |
||||
|
get => Request.TokenTypeHint; |
||||
|
set => Request.TokenTypeHint = value; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Represents an event called for each request to the introspection endpoint
|
||||
|
/// to send the introspection request to the remote authorization server.
|
||||
|
/// </summary>
|
||||
|
public sealed class ApplyIntrospectionRequestContext : BaseExternalContext |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Creates a new instance of the <see cref="ApplyIntrospectionRequestContext"/> class.
|
||||
|
/// </summary>
|
||||
|
public ApplyIntrospectionRequestContext(OpenIddictClientTransaction transaction) |
||||
|
: base(transaction) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the request.
|
||||
|
/// </summary>
|
||||
|
public OpenIddictRequest Request |
||||
|
{ |
||||
|
get => Transaction.Request!; |
||||
|
set => Transaction.Request = value; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Represents an event called for each introspection response
|
||||
|
/// to extract the response parameters from the server response.
|
||||
|
/// </summary>
|
||||
|
public sealed class ExtractIntrospectionResponseContext : BaseExternalContext |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Creates a new instance of the <see cref="ExtractIntrospectionResponseContext"/> class.
|
||||
|
/// </summary>
|
||||
|
public ExtractIntrospectionResponseContext(OpenIddictClientTransaction transaction) |
||||
|
: base(transaction) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the request.
|
||||
|
/// </summary>
|
||||
|
public OpenIddictRequest Request |
||||
|
{ |
||||
|
get => Transaction.Request!; |
||||
|
set => Transaction.Request = value; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the response, or <see langword="null"/> if it wasn't extracted yet.
|
||||
|
/// </summary>
|
||||
|
public OpenIddictResponse? Response |
||||
|
{ |
||||
|
get => Transaction.Response; |
||||
|
set => Transaction.Response = value; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Represents an event called for each introspection response.
|
||||
|
/// </summary>
|
||||
|
public sealed class HandleIntrospectionResponseContext : BaseExternalContext |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Creates a new instance of the <see cref="HandleIntrospectionResponseContext"/> class.
|
||||
|
/// </summary>
|
||||
|
public HandleIntrospectionResponseContext(OpenIddictClientTransaction transaction) |
||||
|
: base(transaction) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the request.
|
||||
|
/// </summary>
|
||||
|
public OpenIddictRequest Request |
||||
|
{ |
||||
|
get => Transaction.Request!; |
||||
|
set => Transaction.Request = value; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the response.
|
||||
|
/// </summary>
|
||||
|
public OpenIddictResponse Response |
||||
|
{ |
||||
|
get => Transaction.Response!; |
||||
|
set => Transaction.Response = value; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the token sent to the introspection endpoint.
|
||||
|
/// </summary>
|
||||
|
public string? Token { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the principal containing the claims resolved from the introspection response.
|
||||
|
/// </summary>
|
||||
|
public ClaimsPrincipal? Principal { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,406 @@ |
|||||
|
/* |
||||
|
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
* See https://github.com/openiddict/openiddict-core for more information concerning
|
||||
|
* the license and the contributors participating to this project. |
||||
|
*/ |
||||
|
|
||||
|
using System.Collections.Immutable; |
||||
|
using System.Diagnostics; |
||||
|
using System.Security.Claims; |
||||
|
using System.Text.Json; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
|
||||
|
namespace OpenIddict.Client; |
||||
|
|
||||
|
public static partial class OpenIddictClientHandlers |
||||
|
{ |
||||
|
public static class Introspection |
||||
|
{ |
||||
|
public static ImmutableArray<OpenIddictClientHandlerDescriptor> DefaultHandlers { get; } = [ |
||||
|
/* |
||||
|
* Introspection response handling: |
||||
|
*/ |
||||
|
ValidateWellKnownParameters.Descriptor, |
||||
|
HandleErrorResponse.Descriptor, |
||||
|
HandleInactiveResponse.Descriptor, |
||||
|
ValidateIssuer.Descriptor, |
||||
|
ValidateTokenUsage.Descriptor, |
||||
|
PopulateClaims.Descriptor |
||||
|
]; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Contains the logic responsible for validating the well-known parameters contained in the introspection response.
|
||||
|
/// </summary>
|
||||
|
public sealed class ValidateWellKnownParameters : IOpenIddictClientHandler<HandleIntrospectionResponseContext> |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Gets the default descriptor definition assigned to this handler.
|
||||
|
/// </summary>
|
||||
|
public static OpenIddictClientHandlerDescriptor Descriptor { get; } |
||||
|
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandleIntrospectionResponseContext>() |
||||
|
.UseSingletonHandler<ValidateWellKnownParameters>() |
||||
|
.SetOrder(int.MinValue + 100_000) |
||||
|
.SetType(OpenIddictClientHandlerType.BuiltIn) |
||||
|
.Build(); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public ValueTask HandleAsync(HandleIntrospectionResponseContext context) |
||||
|
{ |
||||
|
if (context is null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(context)); |
||||
|
} |
||||
|
|
||||
|
foreach (var parameter in context.Response.GetParameters()) |
||||
|
{ |
||||
|
if (!ValidateParameterType(parameter.Key, parameter.Value)) |
||||
|
{ |
||||
|
context.Reject( |
||||
|
error: Errors.ServerError, |
||||
|
description: SR.FormatID2107(parameter.Key), |
||||
|
uri: SR.FormatID8000(SR.ID2107)); |
||||
|
|
||||
|
return default; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return default; |
||||
|
|
||||
|
// Note: in the typical case, the response parameters should be deserialized from a
|
||||
|
// JSON response and thus natively stored as System.Text.Json.JsonElement instances.
|
||||
|
//
|
||||
|
// In the rare cases where the underlying value wouldn't be a JsonElement instance
|
||||
|
// (e.g when custom parameters are manually added to the response), the static
|
||||
|
// conversion operator would take care of converting the underlying value to a
|
||||
|
// JsonElement instance using the same value type as the original parameter value.
|
||||
|
static bool ValidateParameterType(string name, OpenIddictParameter value) => name switch |
||||
|
{ |
||||
|
// Error parameters MUST be formatted as unique strings:
|
||||
|
Parameters.Error or Parameters.ErrorDescription or Parameters.ErrorUri |
||||
|
=> ((JsonElement) value).ValueKind is JsonValueKind.String, |
||||
|
|
||||
|
// The following claims MUST be formatted as booleans:
|
||||
|
Claims.Active => ((JsonElement) value).ValueKind is JsonValueKind.True or JsonValueKind.False, |
||||
|
|
||||
|
// The following claims MUST be formatted as unique strings:
|
||||
|
Claims.JwtId or Claims.Issuer or Claims.Scope or Claims.TokenUsage |
||||
|
=> ((JsonElement) value).ValueKind is JsonValueKind.String, |
||||
|
|
||||
|
// The following claims MUST be formatted as strings or arrays of strings:
|
||||
|
//
|
||||
|
// Note: empty arrays and arrays that contain a single value are also considered valid.
|
||||
|
Claims.Audience => ((JsonElement) value) is JsonElement element && |
||||
|
element.ValueKind is JsonValueKind.String || |
||||
|
(element.ValueKind is JsonValueKind.Array && ValidateStringArray(element)), |
||||
|
|
||||
|
// The following claims MUST be formatted as numeric dates:
|
||||
|
Claims.ExpiresAt or Claims.IssuedAt or Claims.NotBefore |
||||
|
=> (JsonElement) value is { ValueKind: JsonValueKind.Number } element && |
||||
|
element.TryGetDecimal(out decimal result) && result is >= 0, |
||||
|
|
||||
|
// Claims that are not in the well-known list can be of any type.
|
||||
|
_ => true |
||||
|
}; |
||||
|
|
||||
|
static bool ValidateStringArray(JsonElement element) |
||||
|
{ |
||||
|
foreach (var item in element.EnumerateArray()) |
||||
|
{ |
||||
|
if (item.ValueKind is not JsonValueKind.String) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Contains the logic responsible for surfacing potential errors from the introspection response.
|
||||
|
/// </summary>
|
||||
|
public sealed class HandleErrorResponse : IOpenIddictClientHandler<HandleIntrospectionResponseContext> |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Gets the default descriptor definition assigned to this handler.
|
||||
|
/// </summary>
|
||||
|
public static OpenIddictClientHandlerDescriptor Descriptor { get; } |
||||
|
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandleIntrospectionResponseContext>() |
||||
|
.UseSingletonHandler<HandleErrorResponse>() |
||||
|
.SetOrder(ValidateWellKnownParameters.Descriptor.Order + 1_000) |
||||
|
.SetType(OpenIddictClientHandlerType.BuiltIn) |
||||
|
.Build(); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public ValueTask HandleAsync(HandleIntrospectionResponseContext context) |
||||
|
{ |
||||
|
if (context is null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(context)); |
||||
|
} |
||||
|
|
||||
|
// Note: the specification requires returning most errors (e.g invalid token errors)
|
||||
|
// as "active: false" responses instead of as proper OAuth 2.0 error responses.
|
||||
|
// For more information, see https://datatracker.ietf.org/doc/html/rfc7662#section-2.3.
|
||||
|
if (!string.IsNullOrEmpty(context.Response.Error)) |
||||
|
{ |
||||
|
context.Logger.LogInformation(SR.GetResourceString(SR.ID6205), context.Response); |
||||
|
|
||||
|
context.Reject( |
||||
|
error: context.Response.Error switch |
||||
|
{ |
||||
|
Errors.UnauthorizedClient => Errors.UnauthorizedClient, |
||||
|
_ => Errors.ServerError |
||||
|
}, |
||||
|
description: SR.GetResourceString(SR.ID2146), |
||||
|
uri: SR.FormatID8000(SR.ID2146)); |
||||
|
|
||||
|
return default; |
||||
|
} |
||||
|
|
||||
|
return default; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Contains the logic responsible for extracting the active: false marker from the response.
|
||||
|
/// </summary>
|
||||
|
public sealed class HandleInactiveResponse : IOpenIddictClientHandler<HandleIntrospectionResponseContext> |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Gets the default descriptor definition assigned to this handler.
|
||||
|
/// </summary>
|
||||
|
public static OpenIddictClientHandlerDescriptor Descriptor { get; } |
||||
|
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandleIntrospectionResponseContext>() |
||||
|
.UseSingletonHandler<HandleInactiveResponse>() |
||||
|
.SetOrder(HandleErrorResponse.Descriptor.Order + 1_000) |
||||
|
.SetType(OpenIddictClientHandlerType.BuiltIn) |
||||
|
.Build(); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public ValueTask HandleAsync(HandleIntrospectionResponseContext context) |
||||
|
{ |
||||
|
if (context is null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(context)); |
||||
|
} |
||||
|
|
||||
|
// Note: the introspection specification requires that server return "active: false" instead of a proper
|
||||
|
// OAuth 2.0 error when the token is invalid, expired, revoked or invalid for any other reason.
|
||||
|
// While OpenIddict's server can be tweaked to return a proper error (by removing NormalizeErrorResponse)
|
||||
|
// from the enabled handlers, supporting "active: false" is required to ensure total compatibility.
|
||||
|
|
||||
|
var active = (bool?) context.Response[Parameters.Active]; |
||||
|
if (active is null) |
||||
|
{ |
||||
|
context.Reject( |
||||
|
error: Errors.ServerError, |
||||
|
description: SR.FormatID2105(Parameters.Active), |
||||
|
uri: SR.FormatID8000(SR.ID2105)); |
||||
|
|
||||
|
return default; |
||||
|
} |
||||
|
|
||||
|
if (active is not true) |
||||
|
{ |
||||
|
context.Reject( |
||||
|
error: Errors.InvalidToken, |
||||
|
description: SR.GetResourceString(SR.ID2106), |
||||
|
uri: SR.FormatID8000(SR.ID2106)); |
||||
|
|
||||
|
return default; |
||||
|
} |
||||
|
|
||||
|
return default; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Contains the logic responsible for extracting the issuer from the introspection response.
|
||||
|
/// </summary>
|
||||
|
public sealed class ValidateIssuer : IOpenIddictClientHandler<HandleIntrospectionResponseContext> |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Gets the default descriptor definition assigned to this handler.
|
||||
|
/// </summary>
|
||||
|
public static OpenIddictClientHandlerDescriptor Descriptor { get; } |
||||
|
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandleIntrospectionResponseContext>() |
||||
|
.UseSingletonHandler<ValidateIssuer>() |
||||
|
.SetOrder(ValidateWellKnownParameters.Descriptor.Order + 1_000) |
||||
|
.SetType(OpenIddictClientHandlerType.BuiltIn) |
||||
|
.Build(); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public ValueTask HandleAsync(HandleIntrospectionResponseContext context) |
||||
|
{ |
||||
|
if (context is null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(context)); |
||||
|
} |
||||
|
|
||||
|
// The issuer claim is optional. If it's not null or empty, validate it to
|
||||
|
// ensure it matches the issuer registered in the server configuration.
|
||||
|
var issuer = (string?) context.Response[Claims.Issuer]; |
||||
|
if (!string.IsNullOrEmpty(issuer)) |
||||
|
{ |
||||
|
if (!Uri.TryCreate(issuer, UriKind.Absolute, out Uri? uri)) |
||||
|
{ |
||||
|
context.Reject( |
||||
|
error: Errors.ServerError, |
||||
|
description: SR.GetResourceString(SR.ID2108), |
||||
|
uri: SR.FormatID8000(SR.ID2108)); |
||||
|
|
||||
|
return default; |
||||
|
} |
||||
|
|
||||
|
// Ensure the issuer matches the expected value.
|
||||
|
if (uri != context.Configuration.Issuer) |
||||
|
{ |
||||
|
context.Reject( |
||||
|
error: Errors.ServerError, |
||||
|
description: SR.GetResourceString(SR.ID2109), |
||||
|
uri: SR.FormatID8000(SR.ID2109)); |
||||
|
|
||||
|
return default; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return default; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Contains the logic responsible for extracting and validating the token usage from the introspection response.
|
||||
|
/// </summary>
|
||||
|
public sealed class ValidateTokenUsage : IOpenIddictClientHandler<HandleIntrospectionResponseContext> |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Gets the default descriptor definition assigned to this handler.
|
||||
|
/// </summary>
|
||||
|
public static OpenIddictClientHandlerDescriptor Descriptor { get; } |
||||
|
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandleIntrospectionResponseContext>() |
||||
|
.UseSingletonHandler<ValidateTokenUsage>() |
||||
|
.SetOrder(ValidateIssuer.Descriptor.Order + 1_000) |
||||
|
.SetType(OpenIddictClientHandlerType.BuiltIn) |
||||
|
.Build(); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public ValueTask HandleAsync(HandleIntrospectionResponseContext context) |
||||
|
{ |
||||
|
if (context is null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(context)); |
||||
|
} |
||||
|
|
||||
|
// OpenIddict-based authorization servers always return the actual token type using
|
||||
|
// the special "token_usage" claim, that helps resource servers determine whether the
|
||||
|
// introspected token is of the expected type and prevent token substitution attacks.
|
||||
|
// In this handler, the "token_usage" is verified to ensure it corresponds to a supported
|
||||
|
// value so that the component that triggered the introspection request can determine
|
||||
|
// whether the returned token has an acceptable type depending on the context.
|
||||
|
var usage = (string?) context.Response[Claims.TokenUsage]; |
||||
|
if (string.IsNullOrEmpty(usage)) |
||||
|
{ |
||||
|
return default; |
||||
|
} |
||||
|
|
||||
|
// Note: by default, OpenIddict only allows access/refresh tokens to be
|
||||
|
// introspected but additional types can be added using the events model.
|
||||
|
if (usage is not (TokenTypeHints.AccessToken or TokenTypeHints.AuthorizationCode or |
||||
|
TokenTypeHints.DeviceCode or TokenTypeHints.IdToken or |
||||
|
TokenTypeHints.RefreshToken or TokenTypeHints.UserCode)) |
||||
|
{ |
||||
|
context.Reject( |
||||
|
error: Errors.ServerError, |
||||
|
description: SR.GetResourceString(SR.ID2118), |
||||
|
uri: SR.FormatID8000(SR.ID2118)); |
||||
|
|
||||
|
return default; |
||||
|
} |
||||
|
|
||||
|
return default; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Contains the logic responsible for extracting the claims from the introspection response.
|
||||
|
/// </summary>
|
||||
|
public sealed class PopulateClaims : IOpenIddictClientHandler<HandleIntrospectionResponseContext> |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Gets the default descriptor definition assigned to this handler.
|
||||
|
/// </summary>
|
||||
|
public static OpenIddictClientHandlerDescriptor Descriptor { get; } |
||||
|
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandleIntrospectionResponseContext>() |
||||
|
.UseSingletonHandler<PopulateClaims>() |
||||
|
.SetOrder(ValidateTokenUsage.Descriptor.Order + 1_000) |
||||
|
.SetType(OpenIddictClientHandlerType.BuiltIn) |
||||
|
.Build(); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public ValueTask HandleAsync(HandleIntrospectionResponseContext context) |
||||
|
{ |
||||
|
if (context is null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(context)); |
||||
|
} |
||||
|
|
||||
|
Debug.Assert(context.Registration.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013)); |
||||
|
|
||||
|
// Create a new claims-based identity using the same authentication type
|
||||
|
// and the name/role claims as the one used by IdentityModel for JWT tokens.
|
||||
|
var identity = new ClaimsIdentity( |
||||
|
context.Registration.TokenValidationParameters.AuthenticationType, |
||||
|
context.Registration.TokenValidationParameters.NameClaimType, |
||||
|
context.Registration.TokenValidationParameters.RoleClaimType); |
||||
|
|
||||
|
foreach (var parameter in context.Response.GetParameters()) |
||||
|
{ |
||||
|
// Always exclude null keys as they can't be represented as valid claims.
|
||||
|
if (string.IsNullOrEmpty(parameter.Key)) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
// Exclude OpenIddict-specific private claims, that MUST NOT be set based on data returned
|
||||
|
// by the remote authorization server (that may or may not be an OpenIddict server).
|
||||
|
if (parameter.Key.StartsWith(Claims.Prefixes.Private, StringComparison.OrdinalIgnoreCase)) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
// Ignore all protocol claims that shouldn't be mapped to CLR claims.
|
||||
|
if (parameter.Key is Claims.Active or Claims.Issuer or Claims.NotBefore or Claims.TokenType) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
// Note: in the typical case, the response parameters should be deserialized from a
|
||||
|
// JSON response and thus natively stored as System.Text.Json.JsonElement instances.
|
||||
|
//
|
||||
|
// In the rare cases where the underlying value wouldn't be a JsonElement instance
|
||||
|
// (e.g when custom parameters are manually added to the response), the static
|
||||
|
// conversion operator would take care of converting the underlying value to a
|
||||
|
// JsonElement instance using the same value type as the original parameter value.
|
||||
|
switch ((JsonElement) parameter.Value) |
||||
|
{ |
||||
|
// Top-level claims represented as arrays are split and mapped to multiple CLR claims
|
||||
|
// to match the logic implemented by IdentityModel for JWT token deserialization.
|
||||
|
case { ValueKind: JsonValueKind.Array } value: |
||||
|
identity.AddClaims(parameter.Key, value, context.Registration.Issuer.AbsoluteUri); |
||||
|
break; |
||||
|
|
||||
|
case { ValueKind: _ } value: |
||||
|
identity.AddClaim(parameter.Key, value, context.Registration.Issuer.AbsoluteUri); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
context.Principal = new ClaimsPrincipal(identity); |
||||
|
|
||||
|
return default; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue