@ -6,8 +6,12 @@
using System.Collections.Immutable ;
using System.ComponentModel ;
using System.Diagnostics ;
using System.Security.Claims ;
using Microsoft.Extensions.Logging ;
using Microsoft.IdentityModel.Tokens ;
using OpenIddict.Extensions ;
using static OpenIddict . Abstractions . OpenIddictExceptions ;
namespace OpenIddict.Validation ;
@ -21,6 +25,15 @@ public static partial class OpenIddictValidationHandlers
EvaluateValidatedTokens . Descriptor ,
ValidateRequiredTokens . Descriptor ,
ResolveServerConfiguration . Descriptor ,
ResolveIntrospectionEndpoint . Descriptor ,
EvaluateIntrospectionRequest . Descriptor ,
AttachIntrospectionRequestParameters . Descriptor ,
EvaluateGeneratedClientAssertion . Descriptor ,
PrepareClientAssertionPrincipal . Descriptor ,
GenerateClientAssertion . Descriptor ,
AttachIntrospectionRequestClientCredentials . Descriptor ,
SendIntrospectionRequest . Descriptor ,
ValidateIntrospectedTokenUsage . Descriptor ,
ValidateAccessToken . Descriptor ,
/ *
@ -169,6 +182,431 @@ public static partial class OpenIddictValidationHandlers
}
}
/// <summary>
/// Contains the logic responsible for resolving the URI of the introspection endpoint.
/// </summary>
public sealed class ResolveIntrospectionEndpoint : IOpenIddictValidationHandler < ProcessAuthenticationContext >
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get ; }
= OpenIddictValidationHandlerDescriptor . CreateBuilder < ProcessAuthenticationContext > ( )
. UseSingletonHandler < ResolveIntrospectionEndpoint > ( )
. SetOrder ( ResolveServerConfiguration . Descriptor . Order + 1 _ 0 0 0 )
. SetType ( OpenIddictValidationHandlerType . BuiltIn )
. Build ( ) ;
/// <inheritdoc/>
public ValueTask HandleAsync ( ProcessAuthenticationContext context )
{
if ( context is null )
{
throw new ArgumentNullException ( nameof ( context ) ) ;
}
// If the URI of the introspection endpoint wasn't explicitly set
// at this stage, try to extract it from the server configuration.
context . IntrospectionEndpoint ? ? = context . Configuration . IntrospectionEndpoint switch
{
{ IsAbsoluteUri : true } uri when uri . IsWellFormedOriginalString ( ) = > uri ,
_ = > null
} ;
return default ;
}
}
/// <summary>
/// Contains the logic responsible for determining whether an introspection request should be sent.
/// </summary>
public sealed class EvaluateIntrospectionRequest : IOpenIddictValidationHandler < ProcessAuthenticationContext >
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get ; }
= OpenIddictValidationHandlerDescriptor . CreateBuilder < ProcessAuthenticationContext > ( )
. UseSingletonHandler < EvaluateIntrospectionRequest > ( )
. SetOrder ( ResolveIntrospectionEndpoint . Descriptor . Order + 1 _ 0 0 0 )
. SetType ( OpenIddictValidationHandlerType . BuiltIn )
. Build ( ) ;
/// <inheritdoc/>
public ValueTask HandleAsync ( ProcessAuthenticationContext context )
{
if ( context is null )
{
throw new ArgumentNullException ( nameof ( context ) ) ;
}
context . SendIntrospectionRequest = context . Options . ValidationType is OpenIddictValidationType . Introspection ;
return default ;
}
}
/// <summary>
/// Contains the logic responsible for attaching the parameters to the introspection request, if applicable.
/// </summary>
public sealed class AttachIntrospectionRequestParameters : IOpenIddictValidationHandler < ProcessAuthenticationContext >
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get ; }
= OpenIddictValidationHandlerDescriptor . CreateBuilder < ProcessAuthenticationContext > ( )
. AddFilter < RequireIntrospectionRequest > ( )
. UseSingletonHandler < AttachIntrospectionRequestParameters > ( )
. SetOrder ( EvaluateIntrospectionRequest . Descriptor . Order + 1 _ 0 0 0 )
. Build ( ) ;
/// <inheritdoc/>
public ValueTask HandleAsync ( ProcessAuthenticationContext context )
{
if ( context is null )
{
throw new ArgumentNullException ( nameof ( context ) ) ;
}
// Attach a new request instance if necessary.
context . IntrospectionRequest ? ? = new OpenIddictRequest ( ) ;
context . IntrospectionRequest . Token = context . AccessToken ;
context . IntrospectionRequest . TokenTypeHint = TokenTypeHints . AccessToken ;
return default ;
}
}
/// <summary>
/// Contains the logic responsible for selecting the token types that should
/// be generated and optionally sent as part of the authentication demand.
/// </summary>
public sealed class EvaluateGeneratedClientAssertion : IOpenIddictValidationHandler < ProcessAuthenticationContext >
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get ; }
= OpenIddictValidationHandlerDescriptor . CreateBuilder < ProcessAuthenticationContext > ( )
. AddFilter < RequireIntrospectionRequest > ( )
. UseSingletonHandler < EvaluateGeneratedClientAssertion > ( )
. SetOrder ( AttachIntrospectionRequestParameters . Descriptor . Order + 1 _ 0 0 0 )
. SetType ( OpenIddictValidationHandlerType . BuiltIn )
. Build ( ) ;
/// <inheritdoc/>
public ValueTask HandleAsync ( ProcessAuthenticationContext context )
{
if ( context is null )
{
throw new ArgumentNullException ( nameof ( context ) ) ;
}
( context . GenerateClientAssertion ,
context . IncludeClientAssertion ) = context . Options . SigningCredentials . Count switch
{
// If a introspection request is going to be sent and if at least one signing key
// was attached to the validation options, generate and include a client assertion
// token if the configuration indicates the server supports private_key_jwt.
> 0 when context . Configuration . IntrospectionEndpointAuthMethodsSupported . Contains (
ClientAuthenticationMethods . PrivateKeyJwt ) = > ( true , true ) ,
_ = > ( false , false )
} ;
return default ;
}
}
/// <summary>
/// Contains the logic responsible for preparing and attaching the claims principal
/// used to generate the client assertion, if one is going to be sent.
/// </summary>
public sealed class PrepareClientAssertionPrincipal : IOpenIddictValidationHandler < ProcessAuthenticationContext >
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get ; }
= OpenIddictValidationHandlerDescriptor . CreateBuilder < ProcessAuthenticationContext > ( )
. AddFilter < RequireClientAssertionGenerated > ( )
. UseSingletonHandler < PrepareClientAssertionPrincipal > ( )
. SetOrder ( EvaluateGeneratedClientAssertion . Descriptor . Order + 1 _ 0 0 0 )
. SetType ( OpenIddictValidationHandlerType . BuiltIn )
. Build ( ) ;
/// <inheritdoc/>
public ValueTask HandleAsync ( ProcessAuthenticationContext context )
{
if ( context is null )
{
throw new ArgumentNullException ( nameof ( context ) ) ;
}
Debug . Assert ( context . Configuration . Issuer is { IsAbsoluteUri : true } , SR . GetResourceString ( SR . ID4013 ) ) ;
// Create a new principal that will be used to store the client assertion claims.
var principal = new ClaimsPrincipal ( new ClaimsIdentity ( TokenValidationParameters . DefaultAuthenticationType ) ) ;
principal . SetCreationDate ( DateTimeOffset . UtcNow ) ;
var lifetime = context . Options . ClientAssertionLifetime ;
if ( lifetime . HasValue )
{
principal . SetExpirationDate ( principal . GetCreationDate ( ) + lifetime . Value ) ;
}
// Use the issuer URI as the audience. Applications that need to
// use a different value can register a custom event handler.
principal . SetAudiences ( context . Configuration . Issuer . OriginalString ) ;
// Use the client_id as both the subject and the issuer, as required by the specifications.
principal . SetClaim ( Claims . Private . Issuer , context . Options . ClientId )
. SetClaim ( Claims . Subject , context . Options . ClientId ) ;
// Use a random GUID as the JWT unique identifier.
principal . SetClaim ( Claims . JwtId , Guid . NewGuid ( ) . ToString ( ) ) ;
context . ClientAssertionPrincipal = principal ;
return default ;
}
}
/// <summary>
/// Contains the logic responsible for generating a client
/// assertion for the current authentication operation.
/// </summary>
public sealed class GenerateClientAssertion : IOpenIddictValidationHandler < ProcessAuthenticationContext >
{
private readonly IOpenIddictValidationDispatcher _d ispatcher ;
public GenerateClientAssertion ( IOpenIddictValidationDispatcher dispatcher )
= > _d ispatcher = dispatcher ? ? throw new ArgumentNullException ( nameof ( dispatcher ) ) ;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get ; }
= OpenIddictValidationHandlerDescriptor . CreateBuilder < ProcessAuthenticationContext > ( )
. AddFilter < RequireClientAssertionGenerated > ( )
. UseScopedHandler < GenerateClientAssertion > ( )
. SetOrder ( PrepareClientAssertionPrincipal . Descriptor . Order + 1 _ 0 0 0 )
. SetType ( OpenIddictValidationHandlerType . BuiltIn )
. Build ( ) ;
/// <inheritdoc/>
public async ValueTask HandleAsync ( ProcessAuthenticationContext context )
{
if ( context is null )
{
throw new ArgumentNullException ( nameof ( context ) ) ;
}
var notification = new GenerateTokenContext ( context . Transaction )
{
CreateTokenEntry = false ,
IsReferenceToken = false ,
PersistTokenPayload = false ,
Principal = context . ClientAssertionPrincipal ! ,
TokenFormat = TokenFormats . Jwt ,
TokenType = TokenTypeHints . ClientAssertion
} ;
await _d ispatcher . DispatchAsync ( notification ) ;
if ( notification . IsRequestHandled )
{
context . HandleRequest ( ) ;
return ;
}
else if ( notification . IsRequestSkipped )
{
context . SkipRequest ( ) ;
return ;
}
else if ( notification . IsRejected )
{
context . Reject (
error : notification . Error ? ? Errors . InvalidRequest ,
description : notification . ErrorDescription ,
uri : notification . ErrorUri ) ;
return ;
}
context . ClientAssertion = notification . Token ;
context . ClientAssertionType = notification . TokenFormat switch
{
TokenFormats . Jwt = > ClientAssertionTypes . JwtBearer ,
TokenFormats . Saml2 = > ClientAssertionTypes . Saml2Bearer ,
_ = > null
} ;
}
}
/// <summary>
/// Contains the logic responsible for attaching the client credentials to the introspection request, if applicable.
/// </summary>
public sealed class AttachIntrospectionRequestClientCredentials : IOpenIddictValidationHandler < ProcessAuthenticationContext >
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get ; }
= OpenIddictValidationHandlerDescriptor . CreateBuilder < ProcessAuthenticationContext > ( )
. AddFilter < RequireIntrospectionRequest > ( )
. UseSingletonHandler < AttachIntrospectionRequestClientCredentials > ( )
. SetOrder ( GenerateClientAssertion . Descriptor . Order + 1 _ 0 0 0 )
. Build ( ) ;
/// <inheritdoc/>
public ValueTask HandleAsync ( ProcessAuthenticationContext context )
{
if ( context is null )
{
throw new ArgumentNullException ( nameof ( context ) ) ;
}
Debug . Assert ( context . IntrospectionRequest is not null , SR . GetResourceString ( SR . ID4008 ) ) ;
// Always attach the client_id to the request, even if an assertion is sent.
context . IntrospectionRequest . ClientId = context . Options . ClientId ;
// Note: client authentication methods are mutually exclusive so the client_assertion
// and client_secret parameters MUST never be sent at the same time. For more information,
// see https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.
if ( context . IncludeClientAssertion )
{
context . IntrospectionRequest . ClientAssertion = context . ClientAssertion ;
context . IntrospectionRequest . ClientAssertionType = context . ClientAssertionType ;
}
// Note: the client_secret may be null at this point (e.g for a public
// client or if a custom authentication method is used by the application).
else
{
context . IntrospectionRequest . ClientSecret = context . Options . ClientSecret ;
}
return default ;
}
}
/// <summary>
/// Contains the logic responsible for sending the introspection request, if applicable.
/// </summary>
public sealed class SendIntrospectionRequest : IOpenIddictValidationHandler < ProcessAuthenticationContext >
{
private readonly OpenIddictValidationService _ service ;
public SendIntrospectionRequest ( OpenIddictValidationService service )
= > _ service = service ? ? throw new ArgumentNullException ( nameof ( service ) ) ;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get ; }
= OpenIddictValidationHandlerDescriptor . CreateBuilder < ProcessAuthenticationContext > ( )
. AddFilter < RequireIntrospectionRequest > ( )
. UseSingletonHandler < SendIntrospectionRequest > ( )
. SetOrder ( AttachIntrospectionRequestClientCredentials . Descriptor . Order + 1 _ 0 0 0 )
. Build ( ) ;
/// <inheritdoc/>
public async ValueTask HandleAsync ( ProcessAuthenticationContext context )
{
if ( context is null )
{
throw new ArgumentNullException ( nameof ( context ) ) ;
}
Debug . Assert ( context . IntrospectionRequest is not null , SR . GetResourceString ( SR . ID4008 ) ) ;
// Ensure the introspection endpoint is present and is a valid absolute URI.
if ( context . IntrospectionEndpoint is not { IsAbsoluteUri : true } | |
! context . IntrospectionEndpoint . IsWellFormedOriginalString ( ) )
{
throw new InvalidOperationException ( SR . FormatID0301 ( Metadata . IntrospectionEndpoint ) ) ;
}
try
{
( context . IntrospectionResponse , context . AccessTokenPrincipal ) =
await _ service . SendIntrospectionRequestAsync (
context . Configuration , context . IntrospectionRequest ,
context . IntrospectionEndpoint ) ;
}
catch ( ProtocolException exception )
{
context . Logger . LogDebug ( exception , SR . GetResourceString ( SR . ID6155 ) ) ;
context . Reject (
error : exception . Error ,
description : exception . ErrorDescription ,
uri : exception . ErrorUri ) ;
return ;
}
context . Logger . LogTrace ( SR . GetResourceString ( SR . ID6154 ) , context . AccessToken , context . AccessTokenPrincipal . Claims ) ;
}
}
/// <summary>
/// Contains the logic responsible for validating the usage of the introspected token returned by the server, if applicable.
/// </summary>
public sealed class ValidateIntrospectedTokenUsage : IOpenIddictValidationHandler < ProcessAuthenticationContext >
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get ; }
= OpenIddictValidationHandlerDescriptor . CreateBuilder < ProcessAuthenticationContext > ( )
. AddFilter < RequireIntrospectionRequest > ( )
. UseSingletonHandler < ValidateIntrospectedTokenUsage > ( )
. SetOrder ( SendIntrospectionRequest . Descriptor . Order + 1 _ 0 0 0 )
. Build ( ) ;
/// <inheritdoc/>
public ValueTask HandleAsync ( ProcessAuthenticationContext context )
{
if ( context is null )
{
throw new ArgumentNullException ( nameof ( context ) ) ;
}
Debug . Assert ( context . AccessTokenPrincipal is { Identity : ClaimsIdentity } , SR . GetResourceString ( SR . ID4006 ) ) ;
// 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 one of the expected types and prevents token substitution attacks.
//
// If a "token_usage" claim can be extracted from the principal, use it to determine whether
// the token details returned by the authorization server correspond to an access token.
var usage = context . AccessTokenPrincipal . GetClaim ( Claims . TokenUsage ) ;
if ( ! string . IsNullOrEmpty ( usage ) & & usage is not TokenTypeHints . AccessToken )
{
context . Reject (
error : Errors . InvalidToken ,
description : SR . GetResourceString ( SR . ID2110 ) ,
uri : SR . FormatID8000 ( SR . ID2110 ) ) ;
return default ;
}
// Note: if no token usage could be resolved, the token is assumed to be an access token.
context . AccessTokenPrincipal = context . AccessTokenPrincipal . SetTokenType ( usage ? ? TokenTypeHints . AccessToken ) ;
return default ;
}
}
/// <summary>
/// Contains the logic responsible for ensuring a token was correctly resolved from the context.
/// </summary>
@ -186,7 +624,7 @@ public static partial class OpenIddictValidationHandlers
= OpenIddictValidationHandlerDescriptor . CreateBuilder < ProcessAuthenticationContext > ( )
. AddFilter < RequireAccessTokenValidated > ( )
. UseScopedHandler < ValidateAccessToken > ( )
. SetOrder ( ResolveServerConfiguration . Descriptor . Order + 1 _ 0 0 0 )
. SetOrder ( ValidateIntrospectedTokenUsage . Descriptor . Order + 1 _ 0 0 0 )
. SetType ( OpenIddictValidationHandlerType . BuiltIn )
. Build ( ) ;
@ -198,13 +636,16 @@ public static partial class OpenIddictValidationHandlers
throw new ArgumentNullException ( nameof ( context ) ) ;
}
if ( context . AccessTokenPrincipal is not null | | string . IsNullOrEmpty ( context . AccessToken ) )
if ( string . IsNullOrEmpty ( context . AccessToken ) )
{
return ;
}
var notification = new ValidateTokenContext ( context . Transaction )
{
// When using introspection, the principal is already available as it is extracted
// from the introspection response returned by the authorization server.
Principal = context . AccessTokenPrincipal ,
Token = context . AccessToken ,
ValidTokenTypes = { TokenTypeHints . AccessToken }
} ;