diff --git a/samples/Mvc.Server/Startup.cs b/samples/Mvc.Server/Startup.cs index 5745de46..433a1e7b 100644 --- a/samples/Mvc.Server/Startup.cs +++ b/samples/Mvc.Server/Startup.cs @@ -45,6 +45,7 @@ namespace Mvc.Server options.ClaimsIdentity.UserNameClaimType = Claims.Name; options.ClaimsIdentity.UserIdClaimType = Claims.Subject; options.ClaimsIdentity.RoleClaimType = Claims.Role; + options.ClaimsIdentity.EmailClaimType = Claims.Email; }); // OpenIddict offers native integration with Quartz.NET to perform scheduled tasks diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs index e48137d8..df816eae 100644 --- a/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs +++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs @@ -341,6 +341,15 @@ namespace OpenIddict.Abstractions /// true if the token has the specified type, false otherwise. ValueTask HasTypeAsync(object token, string type, CancellationToken cancellationToken = default); + /// + /// Determines whether a given token has any of the specified types. + /// + /// The token. + /// The expected types. + /// The that can be used to abort the operation. + /// true if the token has any of the specified types, false otherwise. + ValueTask HasTypeAsync(object token, ImmutableArray types, CancellationToken cancellationToken = default); + /// /// Executes the specified query and returns all the corresponding elements. /// diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx index d618a837..781766e3 100644 --- a/src/OpenIddict.Abstractions/OpenIddictResources.resx +++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx @@ -134,7 +134,7 @@ To validate tokens received by custom API endpoints, the OpenIddict validation h When implementing custom token deserialization, a 'oi_tkn_typ' claim containing the type of the token being processed must be added to the security principal. - The type of token associated with the deserialized principal ({0}) doesn't match the expected token type ({1}). + The type of token associated with the deserialized principal ({0}) doesn't match one of the expected token types ({1}). A challenge response cannot be returned from this endpoint. @@ -447,10 +447,10 @@ To use key rollover, register both the new certificate and the old one in the cr No custom verification request validation handler was found. When enabling the degraded mode, a custom 'IOpenIddictServerHandler<ValidateVerificationRequestContext>' must be implemented to validate verification requests (e.g to ensure the user_code is valid). - No custom verification authentication handler was found. When enabling the degraded mode, a custom 'IOpenIddictServerHandler<ProcessAuthenticationContext>' must be implemented to handle device and user codes (e.g by retrieving them from a database). + No custom token validation handler was found. When enabling the degraded mode, a custom 'IOpenIddictServerHandler<ValidateTokenContext>' must be implemented to handle device and user codes (e.g by retrieving them from a database). - No custom verification sign-in handler was found. When enabling the degraded mode, a custom 'IOpenIddictServerHandler<ProcessSignInContext>' must be implemented to handle device and user codes and store them in a database, if applicable. + No custom token generation handler was found. When enabling the degraded mode, a custom 'IOpenIddictServerHandler<GenerateTokenContext>' must be implemented to handle device and user codes (e.g by storing them in a database). The event handler of type '{0}' couldn't be resolved. @@ -1476,6 +1476,9 @@ To register the OpenIddict core services, reference the 'OpenIddict.Core' packag The specified authorization type is not supported by the default token manager. + + The token usage returned by the authorization server is not supported. + The '{0}' parameter shouldn't be null or empty at this point. @@ -1552,65 +1555,18 @@ To register the OpenIddict core services, reference the 'OpenIddict.Core' packag '{Claim}' was excluded from the identity token claims. - The token entry for access token '{Identifier}' was successfully created. + The token entry for '{Type}' token '{Identifier}' was successfully created. - A new access token was successfully created: {Payload}. + A new '{Type}' token was successfully created: {Payload}. The principal used to create the token contained the following claims: {Claims}. - The token entry for access token '{Identifier}' was successfully converted to a reference token with the identifier '{ReferenceId}'. - - - The token entry for authorization code '{Identifier}' was successfully created. - - - A new authorization code was successfully created: {Payload}. -The principal used to create the token contained the following claims: {Claims}. - - - The token entry for authorization code '{Identifier}' was successfully converted to a reference token with the identifier '{ReferenceId}'. - - - The token entry for device code '{Identifier}' was successfully created. - - - A new device code was successfully created: {Payload}. -The principal used to create the token contained the following claims: {Claims}. - - - The token entry for device code '{Identifier}' was successfully converted to a reference token with the identifier '{ReferenceId}'. + The token entry for '{Type}' token '{Identifier}' was successfully converted to a reference token with the identifier '{ReferenceId}'. The reference token entry for device code '{Identifier}' was successfully updated'. - - The token entry for refresh token '{Identifier}' was successfully created. - - - A new refresh token was successfully created: {Payload}. -The principal used to create the token contained the following claims: {Claims}. - - - The token entry for refresh token '{Identifier}' was successfully converted to a reference token with the identifier '{ReferenceId}'. - - - The token entry for user code '{Identifier}' was successfully created. - - - A new user code was successfully created: {Payload}. -The principal used to create the token contained the following claims: {Claims}. - - - The token entry for user code '{Identifier}' was successfully converted to a reference token with the identifier '{ReferenceId}'. - - - The token entry for identity token '{Identifier}' was successfully created. - - - A new identity token was successfully created: {Payload}. -The principal used to create the token contained the following claims: {Claims}. - The authorization request was successfully extracted: {Request}. diff --git a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs index 7f52d3c0..f1da7661 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs @@ -898,6 +898,37 @@ namespace OpenIddict.Core return string.Equals(await Store.GetTypeAsync(token, cancellationToken), type, StringComparison.OrdinalIgnoreCase); } + /// + /// Determines whether a given token has any of the specified types. + /// + /// The token. + /// The expected types. + /// The that can be used to abort the operation. + /// true if the token has any of the specified types, false otherwise. + public virtual async ValueTask HasTypeAsync(TToken token, ImmutableArray types, CancellationToken cancellationToken = default) + { + if (token is null) + { + throw new ArgumentNullException(nameof(token)); + } + + var type = await Store.GetTypeAsync(token, cancellationToken); + if (string.IsNullOrEmpty(type)) + { + return false; + } + + for (var index = 0; index < types.Length; index++) + { + if (string.Equals(type, types[index], StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + /// /// Executes the specified query and returns all the corresponding elements. /// @@ -1430,6 +1461,10 @@ namespace OpenIddict.Core ValueTask IOpenIddictTokenManager.HasTypeAsync(object token, string type, CancellationToken cancellationToken) => HasTypeAsync((TToken) token, type, cancellationToken); + /// + ValueTask IOpenIddictTokenManager.HasTypeAsync(object token, ImmutableArray types, CancellationToken cancellationToken) + => HasTypeAsync((TToken) token, types, cancellationToken); + /// IAsyncEnumerable IOpenIddictTokenManager.ListAsync(int? count, int? offset, CancellationToken cancellationToken) => ListAsync(count, offset, cancellationToken); diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConstants.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConstants.cs index 87844bb4..227e96cc 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConstants.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConstants.cs @@ -28,10 +28,16 @@ namespace OpenIddict.Server.AspNetCore public static class Properties { + public const string AccessTokenPrincipal = ".access_token_principal"; + public const string AuthorizationCodePrincipal = ".authorization_code_principal"; + public const string DeviceCodePrincipal = ".device_code_principal"; public const string Error = ".error"; public const string ErrorDescription = ".error_description"; public const string ErrorUri = ".error_uri"; + public const string IdentityTokenPrincipal = ".identity_token_principal"; + public const string RefreshTokenPrincipal = ".refresh_token_principal"; public const string Scope = ".scope"; + public const string UserCodePrincipal = ".user_code_principal"; } } } diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs index 536c2fbc..d30405a1 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Security.Claims; using System.Text.Encodings.Web; using System.Threading.Tasks; @@ -17,6 +16,7 @@ using Microsoft.Extensions.Options; using OpenIddict.Abstractions; using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Server.OpenIddictServerEvents; +using Properties = OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreConstants.Properties; using SR = OpenIddict.Abstractions.OpenIddictResources; namespace OpenIddict.Server.AspNetCore @@ -145,9 +145,9 @@ namespace OpenIddict.Server.AspNetCore var properties = new AuthenticationProperties(new Dictionary { - [OpenIddictServerAspNetCoreConstants.Properties.Error] = context.Error, - [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = context.ErrorDescription, - [OpenIddictServerAspNetCoreConstants.Properties.ErrorUri] = context.ErrorUri + [Properties.Error] = context.Error, + [Properties.ErrorDescription] = context.ErrorDescription, + [Properties.ErrorUri] = context.ErrorUri }); return AuthenticateResult.Fail(SR.GetResourceString(SR.ID0113), properties); @@ -155,29 +155,131 @@ namespace OpenIddict.Server.AspNetCore else { - Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); - Debug.Assert(!string.IsNullOrEmpty(context.Principal.GetTokenType()), SR.GetResourceString(SR.ID4009)); - Debug.Assert(!string.IsNullOrEmpty(context.Token), SR.GetResourceString(SR.ID4010)); + // A single main claims-based principal instance can be attached to an authentication ticket. + // To return the most appropriate one, the principal is selected based on the endpoint type. + // Independently of the selected main principal, all principals resolved from validated tokens + // are attached to the authentication properties bag so they can be accessed from user code. + var principal = context.EndpointType switch + { + OpenIddictServerEndpointType.Authorization or OpenIddictServerEndpointType.Logout + => context.IdentityTokenPrincipal, + + OpenIddictServerEndpointType.Introspection or OpenIddictServerEndpointType.Revocation + => context.AccessTokenPrincipal ?? + context.RefreshTokenPrincipal ?? + context.IdentityTokenPrincipal ?? + context.AuthorizationCodePrincipal ?? + context.DeviceCodePrincipal ?? + context.UserCodePrincipal, + + OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() + => context.AuthorizationCodePrincipal, + OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() + => context.DeviceCodePrincipal, + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() + => context.RefreshTokenPrincipal, + + OpenIddictServerEndpointType.Userinfo => context.AccessTokenPrincipal, + + OpenIddictServerEndpointType.Verification => context.UserCodePrincipal, + + _ => null + }; + + if (principal is null) + { + return AuthenticateResult.NoResult(); + } - // Store the token to allow any OWIN/Katana component (e.g a controller) - // to retrieve it (e.g to make an API request to another application). var properties = new AuthenticationProperties { - ExpiresUtc = context.Principal.GetExpirationDate(), - IssuedUtc = context.Principal.GetCreationDate() + ExpiresUtc = principal.GetExpirationDate(), + IssuedUtc = principal.GetCreationDate() }; - properties.StoreTokens(new[] + List? tokens = null; + + // Attach the tokens to allow any ASP.NET Core component (e.g a controller) + // to retrieve them (e.g to make an API request to another application). + + if (context.AccessTokenPrincipal is not null && !string.IsNullOrEmpty(context.AccessToken)) { - new AuthenticationToken + tokens ??= new(capacity: 1); + tokens.Add(new AuthenticationToken { - Name = context.Principal.GetTokenType()!, - Value = context.Token - } - }); + Name = TokenTypeHints.AccessToken, + Value = context.AccessToken + }); + + properties.SetParameter(Properties.AccessTokenPrincipal, context.AccessTokenPrincipal); + } + + if (context.AuthorizationCodePrincipal is not null && !string.IsNullOrEmpty(context.AuthorizationCode)) + { + tokens ??= new(capacity: 1); + tokens.Add(new AuthenticationToken + { + Name = TokenTypeHints.AuthorizationCode, + Value = context.AuthorizationCode + }); + + properties.SetParameter(Properties.AuthorizationCodePrincipal, context.AuthorizationCodePrincipal); + } + + if (context.DeviceCodePrincipal is not null && !string.IsNullOrEmpty(context.DeviceCode)) + { + tokens ??= new(capacity: 1); + tokens.Add(new AuthenticationToken + { + Name = TokenTypeHints.DeviceCode, + Value = context.DeviceCode + }); + + properties.SetParameter(Properties.DeviceCodePrincipal, context.DeviceCodePrincipal); + } + + if (context.IdentityTokenPrincipal is not null && !string.IsNullOrEmpty(context.IdentityToken)) + { + tokens ??= new(capacity: 1); + tokens.Add(new AuthenticationToken + { + Name = TokenTypeHints.IdToken, + Value = context.IdentityToken + }); + + properties.SetParameter(Properties.IdentityTokenPrincipal, context.IdentityTokenPrincipal); + } + + if (context.RefreshTokenPrincipal is not null && !string.IsNullOrEmpty(context.RefreshToken)) + { + tokens ??= new(capacity: 1); + tokens.Add(new AuthenticationToken + { + Name = TokenTypeHints.RefreshToken, + Value = context.RefreshToken + }); + + properties.SetParameter(Properties.RefreshTokenPrincipal, context.RefreshTokenPrincipal); + } + + if (context.UserCodePrincipal is not null && !string.IsNullOrEmpty(context.UserCode)) + { + tokens ??= new(capacity: 1); + tokens.Add(new AuthenticationToken + { + Name = TokenTypeHints.UserCode, + Value = context.UserCode + }); + + properties.SetParameter(Properties.UserCodePrincipal, context.UserCodePrincipal); + } + + if (tokens is { Count: > 0 }) + { + properties.StoreTokens(tokens); + } - return AuthenticateResult.Success(new AuthenticationTicket( - context.Principal, properties, + return AuthenticateResult.Success(new AuthenticationTicket(principal, properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)); } } diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlerFilters.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlerFilters.cs index 3f7b3473..e598b117 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlerFilters.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlerFilters.cs @@ -143,13 +143,13 @@ namespace OpenIddict.Server.AspNetCore } /// - /// Represents a filter that excludes the associated handlers if the HTTPS requirement was disabled. + /// Represents a filter that excludes the associated handlers if status code pages support was not enabled. /// - public class RequireTransportSecurityRequirementEnabled : IOpenIddictServerHandlerFilter + public class RequireStatusCodePagesIntegrationEnabled : IOpenIddictServerHandlerFilter { private readonly IOptionsMonitor _options; - public RequireTransportSecurityRequirementEnabled(IOptionsMonitor options) + public RequireStatusCodePagesIntegrationEnabled(IOptionsMonitor options) => _options = options; public ValueTask IsActiveAsync(BaseContext context) @@ -159,18 +159,18 @@ namespace OpenIddict.Server.AspNetCore throw new ArgumentNullException(nameof(context)); } - return new ValueTask(!_options.CurrentValue.DisableTransportSecurityRequirement); + return new ValueTask(_options.CurrentValue.EnableStatusCodePagesIntegration); } } /// - /// Represents a filter that excludes the associated handlers if status code pages support was not enabled. + /// Represents a filter that excludes the associated handlers if the HTTPS requirement was disabled. /// - public class RequireStatusCodePagesIntegrationEnabled : IOpenIddictServerHandlerFilter + public class RequireTransportSecurityRequirementEnabled : IOpenIddictServerHandlerFilter { private readonly IOptionsMonitor _options; - public RequireStatusCodePagesIntegrationEnabled(IOptionsMonitor options) + public RequireTransportSecurityRequirementEnabled(IOptionsMonitor options) => _options = options; public ValueTask IsActiveAsync(BaseContext context) @@ -180,7 +180,7 @@ namespace OpenIddict.Server.AspNetCore throw new ArgumentNullException(nameof(context)); } - return new ValueTask(_options.CurrentValue.EnableStatusCodePagesIntegration); + return new ValueTask(!_options.CurrentValue.DisableTransportSecurityRequirement); } } diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionExtensions.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionExtensions.cs index 1c4cbd88..5b3247a6 100644 --- a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionExtensions.cs +++ b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionExtensions.cs @@ -10,7 +10,6 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; using OpenIddict.Server; using OpenIddict.Server.DataProtection; -using static OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionHandlerFilters; using static OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionHandlers; namespace Microsoft.Extensions.DependencyInjection @@ -40,13 +39,6 @@ namespace Microsoft.Extensions.DependencyInjection // Note: the order used here is not important, as the actual order is set in the options. builder.Services.TryAdd(DefaultHandlers.Select(descriptor => descriptor.ServiceDescriptor)); - // Register the built-in filter used by the default OpenIddict Data Protection event handlers. - builder.Services.TryAddSingleton(); - builder.Services.TryAddSingleton(); - builder.Services.TryAddSingleton(); - builder.Services.TryAddSingleton(); - builder.Services.TryAddSingleton(); - // Note: TryAddEnumerable() is used here to ensure the initializers are registered only once. builder.Services.TryAddEnumerable(new[] { diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlerFilters.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlerFilters.cs deleted file mode 100644 index 44b918b0..00000000 --- a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlerFilters.cs +++ /dev/null @@ -1,131 +0,0 @@ -/* - * 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; -using System.ComponentModel; -using System.Threading.Tasks; -using Microsoft.Extensions.Options; -using static OpenIddict.Server.OpenIddictServerEvents; - -namespace OpenIddict.Server.DataProtection -{ - /// - /// Contains a collection of event handler filters commonly used by the Data Protection handlers. - /// - [EditorBrowsable(EditorBrowsableState.Advanced)] - public static class OpenIddictServerDataProtectionHandlerFilters - { - /// - /// Represents a filter that excludes the associated handlers if OpenIddict - /// was not configured to issue ASP.NET Core Data Protection access tokens. - /// - public class RequireDataProtectionAccessTokenFormatEnabled : IOpenIddictServerHandlerFilter - { - private readonly IOptionsMonitor _options; - - public RequireDataProtectionAccessTokenFormatEnabled(IOptionsMonitor options) - => _options = options; - - public ValueTask IsActiveAsync(BaseContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - return new ValueTask(!_options.CurrentValue.PreferDefaultAccessTokenFormat); - } - } - - /// - /// Represents a filter that excludes the associated handlers if OpenIddict - /// was not configured to issue ASP.NET Core Data Protection authorization codes. - /// - public class RequireDataProtectionAuthorizationCodeFormatEnabled : IOpenIddictServerHandlerFilter - { - private readonly IOptionsMonitor _options; - - public RequireDataProtectionAuthorizationCodeFormatEnabled(IOptionsMonitor options) - => _options = options; - - public ValueTask IsActiveAsync(BaseContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - return new ValueTask(!_options.CurrentValue.PreferDefaultAuthorizationCodeFormat); - } - } - - /// - /// Represents a filter that excludes the associated handlers if OpenIddict - /// was not configured to issue ASP.NET Core Data Protection device codes. - /// - public class RequireDataProtectionDeviceCodeFormatEnabled : IOpenIddictServerHandlerFilter - { - private readonly IOptionsMonitor _options; - - public RequireDataProtectionDeviceCodeFormatEnabled(IOptionsMonitor options) - => _options = options; - - public ValueTask IsActiveAsync(BaseContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - return new ValueTask(!_options.CurrentValue.PreferDefaultDeviceCodeFormat); - } - } - - /// - /// Represents a filter that excludes the associated handlers if OpenIddict - /// was not configured to issue ASP.NET Core Data Protection refresh tokens. - /// - public class RequireDataProtectionRefreshTokenFormatEnabled : IOpenIddictServerHandlerFilter - { - private readonly IOptionsMonitor _options; - - public RequireDataProtectionRefreshTokenFormatEnabled(IOptionsMonitor options) - => _options = options; - - public ValueTask IsActiveAsync(BaseContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - return new ValueTask(!_options.CurrentValue.PreferDefaultRefreshTokenFormat); - } - } - - /// - /// Represents a filter that excludes the associated handlers if OpenIddict - /// was not configured to issue ASP.NET Core Data Protection user codes. - /// - public class RequireDataProtectionUserCodeFormatEnabled : IOpenIddictServerHandlerFilter - { - private readonly IOptionsMonitor _options; - - public RequireDataProtectionUserCodeFormatEnabled(IOptionsMonitor options) - => _options = options; - - public ValueTask IsActiveAsync(BaseContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - return new ValueTask(!_options.CurrentValue.PreferDefaultUserCodeFormat); - } - } - } -} diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.Protection.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.Protection.cs new file mode 100644 index 00000000..a6fa2961 --- /dev/null +++ b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.Protection.cs @@ -0,0 +1,332 @@ +/* + * 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; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using OpenIddict.Abstractions; +using static OpenIddict.Abstractions.OpenIddictConstants; +using static OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionConstants.Purposes; +using static OpenIddict.Server.OpenIddictServerEvents; +using static OpenIddict.Server.OpenIddictServerHandlers.Protection; +using Schemes = OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionConstants.Purposes.Schemes; +using SR = OpenIddict.Abstractions.OpenIddictResources; + +namespace OpenIddict.Server.DataProtection +{ + public static partial class OpenIddictServerDataProtectionHandlers + { + public static class Protection + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Token validation: + */ + ValidateDataProtectionToken.Descriptor, + + /* + * Token validation: + */ + GenerateDataProtectionToken.Descriptor); + + /// + /// Contains the logic responsible of validating tokens generated using Data Protection. + /// + public class ValidateDataProtectionToken : IOpenIddictServerHandler + { + private readonly IOptionsMonitor _options; + + public ValidateDataProtectionToken(IOptionsMonitor options) + => _options = options; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateIdentityModelToken.Descriptor.Order + 500) + .SetType(OpenIddictServerHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ValidateTokenContext context) + { + // If a principal was already attached, don't overwrite it. + if (context.Principal is not null) + { + return default; + } + + // Note: ASP.NET Core Data Protection tokens always start with "CfDJ8", that corresponds + // to the base64 representation of the magic "09 F0 C9 F0" header identifying DP payloads. + if (!context.Token.StartsWith("CfDJ8", StringComparison.Ordinal)) + { + return default; + } + + // Tokens generated using ASP.NET Core Data Protection are encrypted by symmetric keys + // that are derived from both a master key resolved from the key ring and a specific value + // known as "purpose" that helps ensure that Data Protection payloads can't be decrypted + // without the correct "purpose" value, which is different for all types of tokens. + // + // While offering extensive protection at the cryptographic level, this prevents decrypting + // unknown tokens without re-executing the entire decryption routine for each type of token + // considered valid. To speed up this process when supporting multiple types is required, + // the Data Protection integration relies on the "token_type_hint" parameter specified + // by the client when it is available (e.g with introspection or revocation requests). + + var principal = context.ValidTokenTypes.Count switch + { + // If no valid token type was set, all supported token types are allowed. + // + // Note: if a "token_type_hint" was specified by the client, use it to optimize + // the token decryption lookup but fall back to other types of tokens + // if the token can't be decrypted using the specified token type hint. + // + // In this case, common types (e.g access/refresh tokens) are checked first. + 0 => context.TokenTypeHint switch + { + TokenTypeHints.AuthorizationCode => + ValidateToken(context.Token, TokenTypeHints.AuthorizationCode) ?? + ValidateToken(context.Token, TokenTypeHints.AccessToken) ?? + ValidateToken(context.Token, TokenTypeHints.RefreshToken) ?? + ValidateToken(context.Token, TokenTypeHints.DeviceCode) ?? + ValidateToken(context.Token, TokenTypeHints.UserCode), + + TokenTypeHints.DeviceCode => + ValidateToken(context.Token, TokenTypeHints.DeviceCode) ?? + ValidateToken(context.Token, TokenTypeHints.AccessToken) ?? + ValidateToken(context.Token, TokenTypeHints.RefreshToken) ?? + ValidateToken(context.Token, TokenTypeHints.AuthorizationCode) ?? + ValidateToken(context.Token, TokenTypeHints.UserCode), + + TokenTypeHints.RefreshToken => + ValidateToken(context.Token, TokenTypeHints.RefreshToken) ?? + ValidateToken(context.Token, TokenTypeHints.AccessToken) ?? + ValidateToken(context.Token, TokenTypeHints.AuthorizationCode) ?? + ValidateToken(context.Token, TokenTypeHints.DeviceCode) ?? + ValidateToken(context.Token, TokenTypeHints.UserCode), + + TokenTypeHints.UserCode => + ValidateToken(context.Token, TokenTypeHints.UserCode) ?? + ValidateToken(context.Token, TokenTypeHints.AccessToken) ?? + ValidateToken(context.Token, TokenTypeHints.RefreshToken) ?? + ValidateToken(context.Token, TokenTypeHints.AuthorizationCode) ?? + ValidateToken(context.Token, TokenTypeHints.DeviceCode), + + _ => + ValidateToken(context.Token, TokenTypeHints.AccessToken) ?? + ValidateToken(context.Token, TokenTypeHints.RefreshToken) ?? + ValidateToken(context.Token, TokenTypeHints.AuthorizationCode) ?? + ValidateToken(context.Token, TokenTypeHints.DeviceCode) ?? + ValidateToken(context.Token, TokenTypeHints.UserCode), + }, + + // If a single valid token type was set, ignore the specified token type hint. + 1 => context.ValidTokenTypes.ElementAt(0) switch + { + TokenTypeHints.AccessToken => ValidateToken(context.Token, TokenTypeHints.AccessToken), + TokenTypeHints.RefreshToken => ValidateToken(context.Token, TokenTypeHints.RefreshToken), + TokenTypeHints.AuthorizationCode => ValidateToken(context.Token, TokenTypeHints.AuthorizationCode), + TokenTypeHints.DeviceCode => ValidateToken(context.Token, TokenTypeHints.DeviceCode), + TokenTypeHints.UserCode => ValidateToken(context.Token, TokenTypeHints.UserCode), + + _ => null // The token type is not supported by the Data Protection integration (e.g identity tokens). + }, + + // If multiple valid types were set, use the specified token type hint + // and select the first non-null token that can be successfully decrypted. + _ => context.ValidTokenTypes.OrderBy(type => type switch + { + // If the token type hint corresponds to one of the valid types, test it first. + string value when value == context.TokenTypeHint => 0, + + TokenTypeHints.AccessToken => 1, + TokenTypeHints.RefreshToken => 2, + TokenTypeHints.AuthorizationCode => 3, + TokenTypeHints.DeviceCode => 4, + TokenTypeHints.UserCode => 5, + + _ => int.MaxValue + }) + .Select(type => type switch + { + TokenTypeHints.AccessToken => ValidateToken(context.Token, TokenTypeHints.AccessToken), + TokenTypeHints.RefreshToken => ValidateToken(context.Token, TokenTypeHints.RefreshToken), + TokenTypeHints.AuthorizationCode => ValidateToken(context.Token, TokenTypeHints.AuthorizationCode), + TokenTypeHints.DeviceCode => ValidateToken(context.Token, TokenTypeHints.DeviceCode), + TokenTypeHints.UserCode => ValidateToken(context.Token, TokenTypeHints.UserCode), + + _ => null // The token type is not supported by the Data Protection integration (e.g identity tokens). + }) + .Where(static principal => principal is not null) + .FirstOrDefault() + }; + + if (principal is null) + { + context.Reject( + error: Errors.InvalidToken, + description: SR.GetResourceString(SR.ID2004), + uri: SR.FormatID8000(SR.ID2004)); + + return default; + } + + context.Principal = principal; + + context.Logger.LogTrace(SR.GetResourceString(SR.ID6152), context.Token, context.Principal.Claims); + + return default; + + ClaimsPrincipal? ValidateToken(string token, string type) + { + // Create a Data Protection protector using the provider registered in the options. + var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(type switch + { + // Note: reference tokens are encrypted using a different "purpose" string than non-reference tokens. + TokenTypeHints.AccessToken when !string.IsNullOrEmpty(context.TokenId) + => new[] { Handlers.Server, Formats.AccessToken, Features.ReferenceTokens, Schemes.Server }, + TokenTypeHints.AccessToken => new[] { Handlers.Server, Formats.AccessToken, Schemes.Server }, + + TokenTypeHints.AuthorizationCode when !string.IsNullOrEmpty(context.TokenId) + => new[] { Handlers.Server, Formats.AuthorizationCode, Features.ReferenceTokens, Schemes.Server }, + TokenTypeHints.AuthorizationCode => new[] { Handlers.Server, Formats.AuthorizationCode, Schemes.Server }, + + TokenTypeHints.DeviceCode when !string.IsNullOrEmpty(context.TokenId) + => new[] { Handlers.Server, Formats.DeviceCode, Features.ReferenceTokens, Schemes.Server }, + TokenTypeHints.DeviceCode => new[] { Handlers.Server, Formats.DeviceCode, Schemes.Server }, + + TokenTypeHints.RefreshToken when !string.IsNullOrEmpty(context.TokenId) + => new[] { Handlers.Server, Formats.RefreshToken, Features.ReferenceTokens, Schemes.Server }, + TokenTypeHints.RefreshToken => new[] { Handlers.Server, Formats.RefreshToken, Schemes.Server }, + + TokenTypeHints.UserCode when !string.IsNullOrEmpty(context.TokenId) + => new[] { Handlers.Server, Formats.UserCode, Features.ReferenceTokens, Schemes.Server }, + TokenTypeHints.UserCode => new[] { Handlers.Server, Formats.UserCode, Schemes.Server }, + + _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003)) + }); + + try + { + using var buffer = new MemoryStream(protector.Unprotect(Base64UrlEncoder.DecodeBytes(token))); + using var reader = new BinaryReader(buffer); + + // Note: since the data format relies on a data protector using different "purposes" strings + // per token type, the token processed at this stage is guaranteed to be of the expected type. + return _options.CurrentValue.Formatter.ReadToken(reader)?.SetTokenType(type); + } + + catch (Exception exception) + { + context.Logger.LogTrace(exception, SR.GetResourceString(SR.ID6153), token); + + return null; + } + } + } + } + + /// + /// Contains the logic responsible of generating a token using Data Protection. + /// + public class GenerateDataProtectionToken : IOpenIddictServerHandler + { + private readonly IOptionsMonitor _options; + + public GenerateDataProtectionToken(IOptionsMonitor options) + => _options = options; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(GenerateIdentityModelToken.Descriptor.Order - 500) + .SetType(OpenIddictServerHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(GenerateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // If an access token was already attached by another handler, don't overwrite it. + if (!string.IsNullOrEmpty(context.Token)) + { + return default; + } + + if (context.TokenType switch + { + TokenTypeHints.AccessToken => _options.CurrentValue.PreferDefaultAccessTokenFormat, + TokenTypeHints.AuthorizationCode => _options.CurrentValue.PreferDefaultAuthorizationCodeFormat, + TokenTypeHints.DeviceCode => _options.CurrentValue.PreferDefaultDeviceCodeFormat, + TokenTypeHints.RefreshToken => _options.CurrentValue.PreferDefaultRefreshTokenFormat, + TokenTypeHints.UserCode => _options.CurrentValue.PreferDefaultUserCodeFormat, + + _ => true // The token type is not supported by the Data Protection integration (e.g identity tokens). + }) + { + return default; + } + + // Create a Data Protection protector using the provider registered in the options. + var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(context.TokenType switch + { + // Note: reference tokens are encrypted using a different "purpose" string than non-reference tokens. + TokenTypeHints.AccessToken when context.Options.UseReferenceAccessTokens + => new[] { Handlers.Server, Formats.AccessToken, Features.ReferenceTokens, Schemes.Server }, + TokenTypeHints.AccessToken => new[] { Handlers.Server, Formats.AccessToken, Schemes.Server }, + + TokenTypeHints.AuthorizationCode when !context.Options.DisableTokenStorage + => new[] { Handlers.Server, Formats.AuthorizationCode, Features.ReferenceTokens, Schemes.Server }, + TokenTypeHints.AuthorizationCode => new[] { Handlers.Server, Formats.AuthorizationCode, Schemes.Server }, + + TokenTypeHints.DeviceCode when !context.Options.DisableTokenStorage + => new[] { Handlers.Server, Formats.DeviceCode, Features.ReferenceTokens, Schemes.Server }, + TokenTypeHints.DeviceCode => new[] { Handlers.Server, Formats.DeviceCode, Schemes.Server }, + + TokenTypeHints.RefreshToken when context.Options.UseReferenceRefreshTokens + => new[] { Handlers.Server, Formats.RefreshToken, Features.ReferenceTokens, Schemes.Server }, + TokenTypeHints.RefreshToken => new[] { Handlers.Server, Formats.RefreshToken, Schemes.Server }, + + TokenTypeHints.UserCode when !context.Options.DisableTokenStorage + => new[] { Handlers.Server, Formats.UserCode, Features.ReferenceTokens, Schemes.Server }, + TokenTypeHints.UserCode => new[] { Handlers.Server, Formats.UserCode, Schemes.Server }, + + _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003)) + }); + + using var buffer = new MemoryStream(); + using var writer = new BinaryWriter(buffer); + + _options.CurrentValue.Formatter.WriteToken(writer, context.Principal); + + context.Token = Base64UrlEncoder.Encode(protector.Protect(buffer.ToArray())); + + context.Logger.LogTrace(SR.GetResourceString(SR.ID6013), context.TokenType, + context.Token, context.Principal.Claims); + + return default; + } + } + } + } +} diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.cs index 72d21610..2463b253 100644 --- a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.cs +++ b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.cs @@ -4,478 +4,15 @@ * the license and the contributors participating to this project. */ -using System; using System.Collections.Immutable; using System.ComponentModel; -using System.IO; -using System.Security.Claims; -using System.Threading.Tasks; -using Microsoft.AspNetCore.DataProtection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.IdentityModel.Tokens; -using OpenIddict.Abstractions; -using static OpenIddict.Abstractions.OpenIddictConstants; -using static OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionConstants.Purposes; -using static OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionHandlerFilters; -using static OpenIddict.Server.OpenIddictServerEvents; -using static OpenIddict.Server.OpenIddictServerHandlerFilters; -using static OpenIddict.Server.OpenIddictServerHandlers; -using Properties = OpenIddict.Server.OpenIddictServerConstants.Properties; -using Schemes = OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionConstants.Purposes.Schemes; -using SR = OpenIddict.Abstractions.OpenIddictResources; namespace OpenIddict.Server.DataProtection { [EditorBrowsable(EditorBrowsableState.Never)] public static partial class OpenIddictServerDataProtectionHandlers { - public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( - /* - * Authentication processing: - */ - ValidateDataProtectionToken.Descriptor, - - /* - * Sign-in processing: - */ - GenerateDataProtectionAccessToken.Descriptor, - GenerateDataProtectionAuthorizationCode.Descriptor, - GenerateDataProtectionDeviceCode.Descriptor, - GenerateDataProtectionRefreshToken.Descriptor, - GenerateDataProtectionUserCode.Descriptor); - - /// - /// Contains the logic responsible of validating tokens generated using Data Protection. - /// - public class ValidateDataProtectionToken : IOpenIddictServerHandler - { - private readonly IOptionsMonitor _options; - - public ValidateDataProtectionToken(IOptionsMonitor options) - => _options = options; - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(ValidateIdentityModelToken.Descriptor.Order + 500) - .SetType(OpenIddictServerHandlerType.BuiltIn) - .Build(); - - /// - public ValueTask HandleAsync(ProcessAuthenticationContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - // If a principal was already attached, don't overwrite it. - if (context.Principal is not null) - { - return default; - } - - // Note: ASP.NET Core Data Protection tokens always start with "CfDJ8", that corresponds - // to the base64 representation of the magic "09 F0 C9 F0" header identifying DP payloads. - if (string.IsNullOrEmpty(context.Token) || !context.Token.StartsWith("CfDJ8", StringComparison.Ordinal)) - { - return default; - } - - var principal = !string.IsNullOrEmpty(context.TokenType) ? - ValidateToken(context.Token, context.TokenType) : - ValidateToken(context.Token, TokenTypeHints.AccessToken) ?? - ValidateToken(context.Token, TokenTypeHints.RefreshToken) ?? - ValidateToken(context.Token, TokenTypeHints.AuthorizationCode) ?? - ValidateToken(context.Token, TokenTypeHints.DeviceCode) ?? - ValidateToken(context.Token, TokenTypeHints.UserCode); - - if (principal is null) - { - context.Reject( - error: context.EndpointType switch - { - OpenIddictServerEndpointType.Token => Errors.InvalidGrant, - _ => Errors.InvalidToken - }, - description: SR.GetResourceString(SR.ID2004), - uri: SR.FormatID8000(SR.ID2004)); - - return default; - } - - context.Principal = principal; - - context.Logger.LogTrace(SR.GetResourceString(SR.ID6152), context.Token, context.Principal.Claims); - - return default; - - ClaimsPrincipal? ValidateToken(string token, string type) - { - // Create a Data Protection protector using the provider registered in the options. - var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(type switch - { - TokenTypeHints.AccessToken when context.Transaction.Properties.ContainsKey(Properties.ReferenceTokenIdentifier) - => new[] { Handlers.Server, Formats.AccessToken, Features.ReferenceTokens, Schemes.Server }, - - TokenTypeHints.AuthorizationCode when context.Transaction.Properties.ContainsKey(Properties.ReferenceTokenIdentifier) - => new[] { Handlers.Server, Formats.AuthorizationCode, Features.ReferenceTokens, Schemes.Server }, - - TokenTypeHints.DeviceCode when context.Transaction.Properties.ContainsKey(Properties.ReferenceTokenIdentifier) - => new[] { Handlers.Server, Formats.DeviceCode, Features.ReferenceTokens, Schemes.Server }, - - TokenTypeHints.RefreshToken when context.Transaction.Properties.ContainsKey(Properties.ReferenceTokenIdentifier) - => new[] { Handlers.Server, Formats.RefreshToken, Features.ReferenceTokens, Schemes.Server }, - - TokenTypeHints.UserCode when context.Transaction.Properties.ContainsKey(Properties.ReferenceTokenIdentifier) - => new[] { Handlers.Server, Formats.UserCode, Features.ReferenceTokens, Schemes.Server }, - - TokenTypeHints.AccessToken => new[] { Handlers.Server, Formats.AccessToken, Schemes.Server }, - TokenTypeHints.AuthorizationCode => new[] { Handlers.Server, Formats.AuthorizationCode, Schemes.Server }, - TokenTypeHints.DeviceCode => new[] { Handlers.Server, Formats.DeviceCode, Schemes.Server }, - TokenTypeHints.RefreshToken => new[] { Handlers.Server, Formats.RefreshToken, Schemes.Server }, - TokenTypeHints.UserCode => new[] { Handlers.Server, Formats.UserCode, Schemes.Server }, - - _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003)) - }); - - try - { - using var buffer = new MemoryStream(protector.Unprotect(Base64UrlEncoder.DecodeBytes(token))); - using var reader = new BinaryReader(buffer); - - // Note: since the data format relies on a data protector using different "purposes" strings - // per token type, the token processed at this stage is guaranteed to be of the expected type. - return _options.CurrentValue.Formatter.ReadToken(reader)?.SetTokenType(type); - } - - catch (Exception exception) - { - context.Logger.LogTrace(exception, SR.GetResourceString(SR.ID6153), token); - - return null; - } - } - } - } - - /// - /// Contains the logic responsible of generating an access token using Data Protection. - /// - public class GenerateDataProtectionAccessToken : IOpenIddictServerHandler - { - private readonly IOptionsMonitor _options; - - public GenerateDataProtectionAccessToken(IOptionsMonitor options) - => _options = options; - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .UseSingletonHandler() - .SetOrder(GenerateIdentityModelAccessToken.Descriptor.Order - 500) - .SetType(OpenIddictServerHandlerType.BuiltIn) - .Build(); - - /// - public ValueTask HandleAsync(ProcessSignInContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - // If an access token was already attached by another handler, don't overwrite it. - if (!string.IsNullOrEmpty(context.Response.AccessToken)) - { - return default; - } - - if (context.AccessTokenPrincipal is null) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0022)); - } - - // Create a Data Protection protector using the provider registered in the options. - var protector = context.Options.UseReferenceAccessTokens ? - _options.CurrentValue.DataProtectionProvider.CreateProtector( - Handlers.Server, Formats.AccessToken, Features.ReferenceTokens, Schemes.Server) : - _options.CurrentValue.DataProtectionProvider.CreateProtector( - Handlers.Server, Formats.AccessToken, Schemes.Server); - - using var buffer = new MemoryStream(); - using var writer = new BinaryWriter(buffer); - - _options.CurrentValue.Formatter.WriteToken(writer, context.AccessTokenPrincipal); - - context.AccessToken = Base64UrlEncoder.Encode(protector.Protect(buffer.ToArray())); - - context.Logger.LogTrace(SR.GetResourceString(SR.ID6013), - context.AccessTokenPrincipal.GetClaim(Claims.JwtId), - context.AccessToken, context.AccessTokenPrincipal.Claims); - - return default; - } - } - - /// - /// Contains the logic responsible of generating an authorization code using Data Protection. - /// - public class GenerateDataProtectionAuthorizationCode : IOpenIddictServerHandler - { - private readonly IOptionsMonitor _options; - - public GenerateDataProtectionAuthorizationCode(IOptionsMonitor options) - => _options = options; - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .UseSingletonHandler() - .SetOrder(GenerateIdentityModelAuthorizationCode.Descriptor.Order - 500) - .SetType(OpenIddictServerHandlerType.BuiltIn) - .Build(); - - /// - public ValueTask HandleAsync(ProcessSignInContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - // If an authorization code was already attached by another handler, don't overwrite it. - if (!string.IsNullOrEmpty(context.Response.Code)) - { - return default; - } - - if (context.AuthorizationCodePrincipal is null) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0022)); - } - - // Create a Data Protection protector using the provider registered in the options. - var protector = !context.Options.DisableTokenStorage ? - _options.CurrentValue.DataProtectionProvider.CreateProtector( - Handlers.Server, Formats.AuthorizationCode, Features.ReferenceTokens, Schemes.Server) : - _options.CurrentValue.DataProtectionProvider.CreateProtector( - Handlers.Server, Formats.AuthorizationCode, Schemes.Server); - - using var buffer = new MemoryStream(); - using var writer = new BinaryWriter(buffer); - - _options.CurrentValue.Formatter.WriteToken(writer, context.AuthorizationCodePrincipal); - - context.AuthorizationCode = Base64UrlEncoder.Encode(protector.Protect(buffer.ToArray())); - - context.Logger.LogTrace(SR.GetResourceString(SR.ID6016), - context.AuthorizationCodePrincipal.GetClaim(Claims.JwtId), - context.AuthorizationCode, context.AuthorizationCodePrincipal.Claims); - - return default; - } - } - - /// - /// Contains the logic responsible of generating a device code using Data Protection. - /// - public class GenerateDataProtectionDeviceCode : IOpenIddictServerHandler - { - private readonly IOptionsMonitor _options; - - public GenerateDataProtectionDeviceCode(IOptionsMonitor options) - => _options = options; - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .UseSingletonHandler() - .SetOrder(GenerateIdentityModelDeviceCode.Descriptor.Order - 500) - .SetType(OpenIddictServerHandlerType.BuiltIn) - .Build(); - - /// - public ValueTask HandleAsync(ProcessSignInContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - // If a device code was already attached by another handler, don't overwrite it. - if (!string.IsNullOrEmpty(context.Response.DeviceCode)) - { - return default; - } - - if (context.DeviceCodePrincipal is null) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0022)); - } - - // Create a Data Protection protector using the provider registered in the options. - var protector = !context.Options.DisableTokenStorage ? - _options.CurrentValue.DataProtectionProvider.CreateProtector( - Handlers.Server, Formats.DeviceCode, Features.ReferenceTokens, Schemes.Server) : - _options.CurrentValue.DataProtectionProvider.CreateProtector( - Handlers.Server, Formats.DeviceCode, Schemes.Server); - - using var buffer = new MemoryStream(); - using var writer = new BinaryWriter(buffer); - - _options.CurrentValue.Formatter.WriteToken(writer, context.DeviceCodePrincipal); - - context.DeviceCode = Base64UrlEncoder.Encode(protector.Protect(buffer.ToArray())); - - context.Logger.LogTrace(SR.GetResourceString(SR.ID6019), - context.DeviceCodePrincipal.GetClaim(Claims.JwtId), - context.DeviceCode, context.DeviceCodePrincipal.Claims); - - return default; - } - } - - /// - /// Contains the logic responsible of generating a refresh token using Data Protection. - /// - public class GenerateDataProtectionRefreshToken : IOpenIddictServerHandler - { - private readonly IOptionsMonitor _options; - - public GenerateDataProtectionRefreshToken(IOptionsMonitor options) - => _options = options; - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .UseSingletonHandler() - .SetOrder(GenerateIdentityModelRefreshToken.Descriptor.Order - 500) - .SetType(OpenIddictServerHandlerType.BuiltIn) - .Build(); - - /// - public ValueTask HandleAsync(ProcessSignInContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - // If a refresh token was already attached by another handler, don't overwrite it. - if (!string.IsNullOrEmpty(context.Response.RefreshToken)) - { - return default; - } - - if (context.RefreshTokenPrincipal is null) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0022)); - } - - // Create a Data Protection protector using the provider registered in the options. - var protector = context.Options.UseReferenceRefreshTokens ? - _options.CurrentValue.DataProtectionProvider.CreateProtector( - Handlers.Server, Formats.RefreshToken, Features.ReferenceTokens, Schemes.Server) : - _options.CurrentValue.DataProtectionProvider.CreateProtector( - Handlers.Server, Formats.RefreshToken, Schemes.Server); - - using var buffer = new MemoryStream(); - using var writer = new BinaryWriter(buffer); - - _options.CurrentValue.Formatter.WriteToken(writer, context.RefreshTokenPrincipal); - - context.RefreshToken = Base64UrlEncoder.Encode(protector.Protect(buffer.ToArray())); - - context.Logger.LogTrace(SR.GetResourceString(SR.ID6023), - context.RefreshTokenPrincipal.GetClaim(Claims.JwtId), - context.RefreshToken, context.RefreshTokenPrincipal.Claims); - - return default; - } - } - - /// - /// Contains the logic responsible of generating a user code using Data Protection. - /// - public class GenerateDataProtectionUserCode : IOpenIddictServerHandler - { - private readonly IOptionsMonitor _options; - - public GenerateDataProtectionUserCode(IOptionsMonitor options) - => _options = options; - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .UseSingletonHandler() - .SetOrder(GenerateIdentityModelUserCode.Descriptor.Order - 500) - .SetType(OpenIddictServerHandlerType.BuiltIn) - .Build(); - - /// - public ValueTask HandleAsync(ProcessSignInContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - // If a user code was already attached by another handler, don't overwrite it. - if (!string.IsNullOrEmpty(context.Response.UserCode)) - { - return default; - } - - if (context.UserCodePrincipal is null) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0022)); - } - - // Create a Data Protection protector using the provider registered in the options. - var protector = !context.Options.DisableTokenStorage ? - _options.CurrentValue.DataProtectionProvider.CreateProtector( - Handlers.Server, Formats.UserCode, Features.ReferenceTokens, Schemes.Server) : - _options.CurrentValue.DataProtectionProvider.CreateProtector( - Handlers.Server, Formats.UserCode, Schemes.Server); - - using var buffer = new MemoryStream(); - using var writer = new BinaryWriter(buffer); - - _options.CurrentValue.Formatter.WriteToken(writer, context.UserCodePrincipal); - - context.UserCode = Base64UrlEncoder.Encode(protector.Protect(buffer.ToArray())); - - context.Logger.LogTrace(SR.GetResourceString(SR.ID6026), - context.UserCodePrincipal.GetClaim(Claims.JwtId), - context.UserCode, context.UserCodePrincipal.Claims); - - return default; - } - } + public static ImmutableArray DefaultHandlers { get; } + = ImmutableArray.CreateRange(Protection.DefaultHandlers); } } diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinConstants.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinConstants.cs index 6d534859..e51b547e 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinConstants.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinConstants.cs @@ -39,10 +39,16 @@ namespace OpenIddict.Server.Owin public static class Properties { + public const string AccessTokenPrincipal = ".access_token_principal"; + public const string AuthorizationCodePrincipal = ".authorization_code_principal"; + public const string DeviceCodePrincipal = ".device_code_principal"; public const string Error = ".error"; public const string ErrorDescription = ".error_description"; public const string ErrorUri = ".error_uri"; + public const string IdentityTokenPrincipal = ".identity_token_principal"; + public const string RefreshTokenPrincipal = ".refresh_token_principal"; public const string Scope = ".scope"; + public const string UserCodePrincipal = ".user_code_principal"; } } } diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs index e015ab32..2238f199 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.Owin; @@ -15,6 +14,7 @@ using Microsoft.Owin.Security.Infrastructure; using OpenIddict.Abstractions; using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Server.OpenIddictServerEvents; +using Properties = OpenIddict.Server.Owin.OpenIddictServerOwinConstants.Properties; using SR = OpenIddict.Abstractions.OpenIddictResources; namespace OpenIddict.Server.Owin @@ -155,11 +155,11 @@ namespace OpenIddict.Server.Owin return null; } - var properties = new AuthenticationProperties(new Dictionary + var properties = new OpenIddictServerOwinProperties(new Dictionary { - [OpenIddictServerOwinConstants.Properties.Error] = context.Error, - [OpenIddictServerOwinConstants.Properties.ErrorDescription] = context.ErrorDescription, - [OpenIddictServerOwinConstants.Properties.ErrorUri] = context.ErrorUri + [Properties.Error] = context.Error, + [Properties.ErrorDescription] = context.ErrorDescription, + [Properties.ErrorUri] = context.ErrorUri }); return new AuthenticationTicket(null, properties); @@ -167,22 +167,88 @@ namespace OpenIddict.Server.Owin else { - Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); - Debug.Assert(!string.IsNullOrEmpty(context.Principal.GetTokenType()), SR.GetResourceString(SR.ID4009)); - Debug.Assert(!string.IsNullOrEmpty(context.Token), SR.GetResourceString(SR.ID4010)); + // A single main claims-based principal instance can be attached to an authentication ticket. + // To return the most appropriate one, the principal is selected based on the endpoint type. + // Independently of the selected main principal, all principals resolved from validated tokens + // are attached to the authentication properties bag so they can be accessed from user code. + var principal = context.EndpointType switch + { + OpenIddictServerEndpointType.Authorization or OpenIddictServerEndpointType.Logout + => context.IdentityTokenPrincipal, + + OpenIddictServerEndpointType.Introspection or OpenIddictServerEndpointType.Revocation + => context.AccessTokenPrincipal ?? + context.RefreshTokenPrincipal ?? + context.IdentityTokenPrincipal ?? + context.AuthorizationCodePrincipal ?? + context.DeviceCodePrincipal ?? + context.UserCodePrincipal, + + OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() + => context.AuthorizationCodePrincipal, + OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() + => context.DeviceCodePrincipal, + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() + => context.RefreshTokenPrincipal, + + OpenIddictServerEndpointType.Userinfo => context.AccessTokenPrincipal, - // Store the token to allow any OWIN/Katana component (e.g a controller) - // to retrieve it (e.g to make an API request to another application). - var properties = new AuthenticationProperties(new Dictionary + OpenIddictServerEndpointType.Verification => context.UserCodePrincipal, + + _ => null + }; + + if (principal is null) { - [context.Principal.GetTokenType()!] = context.Token - }) + return null; + } + + var properties = new OpenIddictServerOwinProperties { - ExpiresUtc = context.Principal.GetExpirationDate(), - IssuedUtc = context.Principal.GetCreationDate() + ExpiresUtc = principal.GetExpirationDate(), + IssuedUtc = principal.GetCreationDate() }; - return new AuthenticationTicket((ClaimsIdentity) context.Principal.Identity, properties); + // Attach the tokens to allow any ASP.NET Core component (e.g a controller) + // to retrieve them (e.g to make an API request to another application). + + if (context.AccessTokenPrincipal is not null && !string.IsNullOrEmpty(context.AccessToken)) + { + properties.Dictionary[TokenTypeHints.AccessToken] = context.AccessToken; + properties.SetParameter(Properties.AccessTokenPrincipal, context.AccessTokenPrincipal); + } + + if (context.AuthorizationCodePrincipal is not null && !string.IsNullOrEmpty(context.AuthorizationCode)) + { + properties.Dictionary[TokenTypeHints.AuthorizationCode] = context.AuthorizationCode; + properties.SetParameter(Properties.AuthorizationCodePrincipal, context.AuthorizationCodePrincipal); + } + + if (context.DeviceCodePrincipal is not null && !string.IsNullOrEmpty(context.DeviceCode)) + { + properties.Dictionary[TokenTypeHints.DeviceCode] = context.DeviceCode; + properties.SetParameter(Properties.DeviceCodePrincipal, context.DeviceCodePrincipal); + } + + if (context.IdentityTokenPrincipal is not null && !string.IsNullOrEmpty(context.IdentityToken)) + { + properties.Dictionary[TokenTypeHints.IdToken] = context.IdentityToken; + properties.SetParameter(Properties.IdentityTokenPrincipal, context.IdentityTokenPrincipal); + } + + if (context.RefreshTokenPrincipal is not null && !string.IsNullOrEmpty(context.RefreshToken)) + { + properties.Dictionary[TokenTypeHints.RefreshToken] = context.RefreshToken; + properties.SetParameter(Properties.RefreshTokenPrincipal, context.RefreshTokenPrincipal); + } + + if (context.UserCodePrincipal is not null && !string.IsNullOrEmpty(context.UserCode)) + { + properties.Dictionary[TokenTypeHints.UserCode] = context.UserCode; + properties.SetParameter(Properties.UserCodePrincipal, context.UserCodePrincipal); + } + + return new AuthenticationTicket((ClaimsIdentity) principal.Identity, properties); } } @@ -205,7 +271,7 @@ namespace OpenIddict.Server.Owin // corresponds to a challenge response, as LookupChallenge() will always return a non-null // value when active authentication is used, even if no challenge was actually triggered. var challenge = Helper.LookupChallenge(Options.AuthenticationType, Options.AuthenticationMode); - if (challenge is not null && (Response.StatusCode == 401 || Response.StatusCode == 403)) + if (challenge is not null && Response.StatusCode is 401 or 403) { var transaction = Context.Get(typeof(OpenIddictServerTransaction).FullName) ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0112)); diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinProperties.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinProperties.cs new file mode 100644 index 00000000..1f0b0bc1 --- /dev/null +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinProperties.cs @@ -0,0 +1,89 @@ +/* + * 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; +using System.Collections.Generic; +using Microsoft.Owin.Security; +using SR = OpenIddict.Abstractions.OpenIddictResources; + +namespace OpenIddict.Server.Owin +{ + /// + public class OpenIddictServerOwinProperties : AuthenticationProperties + { + /// + public OpenIddictServerOwinProperties() + : this(items: null) + { + } + + /// + public OpenIddictServerOwinProperties(IDictionary? items) + : this(items, parameters: null) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// State values dictionary to use. + /// Parameters dictionary to use. + public OpenIddictServerOwinProperties( + IDictionary? items, + IDictionary? parameters) + : base(items) + => Parameters = parameters ?? new Dictionary(StringComparer.Ordinal); + + /// + /// Gets the collection of parameters passed to the authentication handler. + /// + /// + /// Note: these properties are not intended for serialization or persistence, + /// only for flowing data between call sites. + /// + public IDictionary Parameters { get; } + + /// + /// Gets a parameter from the collection. + /// + /// The parameter type. + /// The parameter name. + /// The parameter value or a default value if the property is not set. + public T? GetParameter(string name) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException(SR.ID0190, nameof(name)); + } + + return Parameters.TryGetValue(name, out var parameter) && parameter is T value ? value : default; + } + + /// + /// Sets a parameter value in the collection. + /// + /// The parameter type. + /// The parameter key. + /// The value to set. + public void SetParameter(string name, T? value) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException(SR.ID0190, nameof(name)); + } + + if (value is null) + { + Parameters.Remove(name); + } + + else + { + Parameters[name] = value; + } + } + } +} diff --git a/src/OpenIddict.Server/OpenIddictServerConfiguration.cs b/src/OpenIddict.Server/OpenIddictServerConfiguration.cs index 6879179e..9329c19b 100644 --- a/src/OpenIddict.Server/OpenIddictServerConfiguration.cs +++ b/src/OpenIddict.Server/OpenIddictServerConfiguration.cs @@ -227,13 +227,13 @@ namespace OpenIddict.Server throw new InvalidOperationException(SR.GetResourceString(SR.ID0095)); } - // If the degraded mode was enabled, ensure custom authentication/sign-in handlers + // If the degraded mode was enabled, ensure custom validation/generation handlers // have been registered to deal with device/user codes validation and generation. if (options.GrantTypes.Contains(GrantTypes.DeviceCode)) { if (!options.Handlers.Any( - descriptor => descriptor.ContextType == typeof(ProcessAuthenticationContext) && + descriptor => descriptor.ContextType == typeof(ValidateTokenContext) && descriptor.Type == OpenIddictServerHandlerType.Custom && descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type)))) { @@ -241,7 +241,7 @@ namespace OpenIddict.Server } if (!options.Handlers.Any( - descriptor => descriptor.ContextType == typeof(ProcessSignInContext) && + descriptor => descriptor.ContextType == typeof(GenerateTokenContext) && descriptor.Type == OpenIddictServerHandlerType.Custom && descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type)))) { @@ -258,8 +258,7 @@ namespace OpenIddict.Server options.SigningCredentials.Sort((left, right) => Compare(left.Key, right.Key)); // Generate a key identifier for the encryption/signing keys that don't already have one. - foreach (var key in options.EncryptionCredentials - .Select(credentials => credentials.Key) + foreach (var key in options.EncryptionCredentials.Select(credentials => credentials.Key) .Concat(options.SigningCredentials.Select(credentials => credentials.Key)) .Where(key => string.IsNullOrEmpty(key.KeyId))) { diff --git a/src/OpenIddict.Server/OpenIddictServerConstants.cs b/src/OpenIddict.Server/OpenIddictServerConstants.cs deleted file mode 100644 index 2a53578c..00000000 --- a/src/OpenIddict.Server/OpenIddictServerConstants.cs +++ /dev/null @@ -1,16 +0,0 @@ -/* - * 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. - */ - -namespace OpenIddict.Server -{ - public static class OpenIddictServerConstants - { - public static class Properties - { - public const string ReferenceTokenIdentifier = ".reference_token_identifier"; - } - } -} diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Protection.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Protection.cs new file mode 100644 index 00000000..0781c1aa --- /dev/null +++ b/src/OpenIddict.Server/OpenIddictServerEvents.Protection.cs @@ -0,0 +1,129 @@ +/* + * 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; +using System.Collections.Generic; +using System.Security.Claims; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Tokens; +using OpenIddict.Abstractions; + +namespace OpenIddict.Server +{ + public static partial class OpenIddictServerEvents + { + /// + /// Represents an event called when generating a token. + /// + public class GenerateTokenContext : BaseValidatingContext + { + /// + /// Creates a new instance of the class. + /// + public GenerateTokenContext(OpenIddictServerTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets or sets the request. + /// + public OpenIddictRequest Request + { + get => Transaction.Request!; + set => Transaction.Request = value; + } + + /// + /// Gets or sets the security principal used to create the token. + /// + public ClaimsPrincipal Principal { get; set; } = default!; + + /// + /// Gets or sets the encryption credentials used to encrypt the token. + /// + public EncryptingCredentials? EncryptionCredentials { get; set; } + + /// + /// Gets or sets the signing credentials used to sign the token. + /// + public SigningCredentials? SigningCredentials { get; set; } + + /// + /// Gets or sets the security token handler used to serialize the security principal. + /// + public JsonWebTokenHandler SecurityTokenHandler { get; set; } = default!; + + /// + /// Gets or sets the token returned to the client application. + /// + public string? Token { get; set; } + + /// + /// Gets or sets the type of the token to create. + /// + public string TokenType { get; set; } = default!; + } + + /// + /// Represents an event called when validating a token. + /// + public class ValidateTokenContext : BaseValidatingContext + { + /// + /// Creates a new instance of the class. + /// + public ValidateTokenContext(OpenIddictServerTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets or sets the request. + /// + public OpenIddictRequest Request + { + get => Transaction.Request!; + set => Transaction.Request = value; + } + + /// + /// Gets or sets the security token handler used to validate the token. + /// + public JsonWebTokenHandler SecurityTokenHandler { get; set; } = default!; + + /// + /// Gets or sets the validation parameters used to verify the authenticity of tokens. + /// + public TokenValidationParameters TokenValidationParameters { get; set; } = default!; + + /// + /// Gets or sets the token to validate. + /// + public string Token { get; set; } = default!; + + /// + /// Gets or sets the token type hint specified by the client, if applicable. + /// + public string? TokenTypeHint { get; set; } = default!; + + /// + /// Gets or sets the token entry identifier associated with the token, if applicable. + /// + public string? TokenId { get; set; } + + /// + /// Gets or sets the security principal resolved from the token. + /// + public ClaimsPrincipal? Principal { get; set; } + + /// + /// Gets the token types that are considered valid. + /// + public HashSet ValidTokenTypes { get; } = new(StringComparer.OrdinalIgnoreCase); + } + } +} diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.cs b/src/OpenIddict.Server/OpenIddictServerEvents.cs index e8664787..2a0d167d 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.cs +++ b/src/OpenIddict.Server/OpenIddictServerEvents.cs @@ -272,19 +272,191 @@ namespace OpenIddict.Server } /// - /// Gets or sets the security principal. + /// Gets or sets a boolean indicating whether an access token + /// must be resolved for the authentication to considered valid. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. /// - public ClaimsPrincipal? Principal { get; set; } + public bool RequireAccessToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether an authorization code + /// must be resolved for the authentication to considered valid. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool RequireAuthorizationCode { get; set; } + + /// + /// Gets or sets a boolean indicating whether a device code + /// must be resolved for the authentication to considered valid. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool RequireDeviceCode { get; set; } + + /// + /// Gets or sets a boolean indicating whether a generic token + /// must be resolved for the authentication to considered valid. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool RequireGenericToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether an identity token + /// must be resolved for the authentication to considered valid. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool RequireIdentityToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether a refresh token + /// must be resolved for the authentication to considered valid. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool RequireRefreshToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether a user code + /// must be resolved for the authentication to considered valid. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool RequireUserCode { get; set; } + + /// + /// Gets or sets a boolean indicating whether an access token + /// should be extracted from the current context and validated. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool ValidateAccessToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether an authorization code + /// should be extracted from the current context and validated. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool ValidateAuthorizationCode { get; set; } + + /// + /// Gets or sets a boolean indicating whether a device code + /// should be extracted from the current context and validated. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool ValidateDeviceCode { get; set; } + + /// + /// Gets or sets a boolean indicating whether a generic token + /// should be extracted from the current context and validated. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool ValidateGenericToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether an identity token + /// should be extracted from the current context and validated. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool ValidateIdentityToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether a refresh token + /// should be extracted from the current context and validated. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool ValidateRefreshToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether a user code + /// should be extracted from the current context and validated. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool ValidateUserCode { get; set; } + + /// + /// Gets or sets the access token to validate, if applicable. + /// + public string? AccessToken { get; set; } + + /// + /// Gets or sets the principal extracted from the access token, if applicable. + /// + public ClaimsPrincipal? AccessTokenPrincipal { get; set; } + + /// + /// Gets or sets the authorization code to validate, if applicable. + /// + public string? AuthorizationCode { get; set; } + + /// + /// Gets or sets the principal extracted from the authorization code, if applicable. + /// + public ClaimsPrincipal? AuthorizationCodePrincipal { get; set; } + + /// + /// Gets or sets the device code to validate, if applicable. + /// + public string? DeviceCode { get; set; } + + /// + /// Gets or sets the principal extracted from the device code, if applicable. + /// + public ClaimsPrincipal? DeviceCodePrincipal { get; set; } + + /// + /// Gets or sets the generic token to validate, if applicable. + /// + public string? GenericToken { get; set; } /// - /// Gets or sets the token to validate. + /// Gets or sets the optional hint indicating the type of the generic token, if applicable. /// - public string? Token { get; set; } + public string? GenericTokenTypeHint { get; set; } /// - /// Gets or sets the expected type of the token. + /// Gets or sets the principal extracted from the generic token, if applicable. /// - public string? TokenType { get; set; } + public ClaimsPrincipal? GenericTokenPrincipal { get; set; } + + /// + /// Gets or sets the identity token to validate, if applicable. + /// + public string? IdentityToken { get; set; } + + /// + /// Gets or sets the principal extracted from the identity token, if applicable. + /// + public ClaimsPrincipal? IdentityTokenPrincipal { get; set; } + + /// + /// Gets or sets the refresh token to validate, if applicable. + /// + public string? RefreshToken { get; set; } + + /// + /// Gets or sets the principal extracted from the refresh token, if applicable. + /// + public ClaimsPrincipal? RefreshTokenPrincipal { get; set; } + + /// + /// Gets or sets the user code to validate, if applicable. + /// + public string? UserCode { get; set; } + + /// + /// Gets or sets the principal extracted from the user code, if applicable. + /// + public ClaimsPrincipal? UserCodePrincipal { get; set; } } /// diff --git a/src/OpenIddict.Server/OpenIddictServerExtensions.cs b/src/OpenIddict.Server/OpenIddictServerExtensions.cs index db8cb5bb..b135c8b4 100644 --- a/src/OpenIddict.Server/OpenIddictServerExtensions.cs +++ b/src/OpenIddict.Server/OpenIddictServerExtensions.cs @@ -45,7 +45,9 @@ namespace Microsoft.Extensions.DependencyInjection // Register the built-in filters used by the default OpenIddict server event handlers. builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); @@ -53,16 +55,20 @@ namespace Microsoft.Extensions.DependencyInjection builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); @@ -71,6 +77,7 @@ namespace Microsoft.Extensions.DependencyInjection builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); diff --git a/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs b/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs index 35276797..4166aadc 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs @@ -30,6 +30,22 @@ namespace OpenIddict.Server } } + /// + /// Represents a filter that excludes the associated handlers if no access token is validated. + /// + public class RequireAccessTokenValidated : IOpenIddictServerHandlerFilter + { + public ValueTask IsActiveAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new ValueTask(context.ValidateAccessToken); + } + } + /// /// Represents a filter that excludes the associated handlers if no authorization code is generated. /// @@ -46,6 +62,22 @@ namespace OpenIddict.Server } } + /// + /// Represents a filter that excludes the associated handlers if no authorization code is validated. + /// + public class RequireAuthorizationCodeValidated : IOpenIddictServerHandlerFilter + { + public ValueTask IsActiveAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new ValueTask(context.ValidateAuthorizationCode); + } + } + /// /// Represents a filter that excludes the associated handlers if the request is not an authorization request. /// @@ -158,6 +190,22 @@ namespace OpenIddict.Server } } + /// + /// Represents a filter that excludes the associated handlers if no device code is validated. + /// + public class RequireDeviceCodeValidated : IOpenIddictServerHandlerFilter + { + public ValueTask IsActiveAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new ValueTask(context.ValidateDeviceCode); + } + } + /// /// Represents a filter that excludes the associated handlers if the request is not a device request. /// @@ -190,6 +238,22 @@ namespace OpenIddict.Server } } + /// + /// Represents a filter that excludes the associated handlers if no generic token is validated. + /// + public class RequireGenericTokenValidated : IOpenIddictServerHandlerFilter + { + public ValueTask IsActiveAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new ValueTask(context.ValidateGenericToken); + } + } + /// /// Represents a filter that excludes the associated handlers if grant type permissions were disabled. /// @@ -222,6 +286,22 @@ namespace OpenIddict.Server } } + /// + /// Represents a filter that excludes the associated handlers if no identity token is validated. + /// + public class RequireIdentityTokenValidated : IOpenIddictServerHandlerFilter + { + public ValueTask IsActiveAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new ValueTask(context.ValidateIdentityToken); + } + } + /// /// Represents a filter that excludes the associated handlers if the request is not an introspection request. /// @@ -318,6 +398,22 @@ namespace OpenIddict.Server } } + /// + /// Represents a filter that excludes the associated handlers if no refresh token is validated. + /// + public class RequireRefreshTokenValidated : IOpenIddictServerHandlerFilter + { + public ValueTask IsActiveAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new ValueTask(context.ValidateRefreshToken); + } + } + /// /// Represents a filter that excludes the associated handlers if response type permissions were disabled. /// @@ -446,6 +542,22 @@ namespace OpenIddict.Server } } + /// + /// Represents a filter that excludes the associated handlers if no user code is validated. + /// + public class RequireUserCodeValidated : IOpenIddictServerHandlerFilter + { + public ValueTask IsActiveAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new ValueTask(context.ValidateUserCode); + } + } + /// /// Represents a filter that excludes the associated handlers if the request is not a userinfo request. /// diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs index a9b91fd4..b183298f 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs @@ -1190,7 +1190,7 @@ namespace OpenIddict.Server } // Attach the security principal extracted from the token to the validation context. - context.Principal = notification.Principal; + context.Principal = notification.UserCodePrincipal; } } } diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs index 2ff73e61..326448df 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs @@ -1318,7 +1318,9 @@ namespace OpenIddict.Server } // Attach the security principal extracted from the token to the validation context. - context.Principal = notification.Principal; + context.Principal = context.Request.IsAuthorizationCodeGrantType() ? notification.AuthorizationCodePrincipal : + context.Request.IsDeviceCodeGrantType() ? notification.DeviceCodePrincipal : + context.Request.IsRefreshTokenGrantType() ? notification.RefreshTokenPrincipal : null; } } diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs index f9504d30..4db4bb66 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs @@ -700,6 +700,10 @@ namespace OpenIddict.Server var notification = new ProcessAuthenticationContext(context.Transaction); await _dispatcher.DispatchAsync(notification); + // Store the context object in the transaction so it can be later retrieved by handlers + // that want to access the authentication result without triggering a new authentication flow. + context.Transaction.SetProperty(typeof(ProcessAuthenticationContext).FullName!, notification); + if (notification.IsRequestHandled) { context.HandleRequest(); @@ -722,7 +726,7 @@ namespace OpenIddict.Server } // Attach the security principal extracted from the token to the validation context. - context.Principal = notification.Principal; + context.Principal = notification.GenericTokenPrincipal; } } diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs new file mode 100644 index 00000000..1590da3d --- /dev/null +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs @@ -0,0 +1,1543 @@ +/* + * 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; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Tokens; +using OpenIddict.Abstractions; +using static OpenIddict.Abstractions.OpenIddictConstants; +using static OpenIddict.Server.OpenIddictServerEvents; +using static OpenIddict.Server.OpenIddictServerHandlerFilters; +using SR = OpenIddict.Abstractions.OpenIddictResources; + +namespace OpenIddict.Server +{ + public static partial class OpenIddictServerHandlers + { + public static class Protection + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Token validation: + */ + ResolveTokenValidationParameters.Descriptor, + ValidateReferenceTokenIdentifier.Descriptor, + ValidateIdentityModelToken.Descriptor, + NormalizeScopeClaims.Descriptor, + MapInternalClaims.Descriptor, + RestoreReferenceTokenProperties.Descriptor, + ValidatePrincipal.Descriptor, + ValidateTokenEntry.Descriptor, + ValidateAuthorizationEntry.Descriptor, + ValidateExpirationDate.Descriptor, + + /* + * Token generation: + */ + AttachSecurityCredentials.Descriptor, + CreateTokenEntry.Descriptor, + GenerateIdentityModelToken.Descriptor, + ConvertReferenceToken.Descriptor, + BeautifyToken.Descriptor); + + /// + /// Contains the logic responsible of resolving the validation parameters used to validate tokens. + /// + public class ResolveTokenValidationParameters : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .SetType(OpenIddictServerHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ValidateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + var parameters = context.Options.TokenValidationParameters.Clone(); + parameters.ValidIssuer ??= context.Issuer?.AbsoluteUri; + parameters.ValidateIssuer = !string.IsNullOrEmpty(parameters.ValidIssuer); + + parameters.ValidTypes = context.ValidTokenTypes.Count switch + { + // If no specific token type is expected, accept all token types at this stage. + // Additional filtering can be made based on the resolved/actual token type. + 0 => null, + + // Otherwise, map the token types to their JWT public or internal representation. + _ => context.ValidTokenTypes.SelectMany(type => type switch + { + // For access tokens, both "at+jwt" and "application/at+jwt" are valid. + TokenTypeHints.AccessToken => new[] + { + JsonWebTokenTypes.AccessToken, + JsonWebTokenTypes.Prefixes.Application + JsonWebTokenTypes.AccessToken + }, + + // For identity tokens, both "JWT" and "application/jwt" are valid. + TokenTypeHints.IdToken => new[] + { + JsonWebTokenTypes.IdentityToken, + JsonWebTokenTypes.Prefixes.Application + JsonWebTokenTypes.IdentityToken + }, + + // For authorization codes, only the short "oi_auc+jwt" form is valid. + TokenTypeHints.AuthorizationCode => new[] { JsonWebTokenTypes.Private.AuthorizationCode }, + + // For device codes, only the short "oi_dvc+jwt" form is valid. + TokenTypeHints.DeviceCode => new[] { JsonWebTokenTypes.Private.DeviceCode }, + + // For refresh tokens, only the short "oi_reft+jwt" form is valid. + TokenTypeHints.RefreshToken => new[] { JsonWebTokenTypes.Private.RefreshToken }, + + // For user codes, only the short "oi_usrc+jwt" form is valid. + TokenTypeHints.UserCode => new[] { JsonWebTokenTypes.Private.UserCode }, + + _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003)) + }) + }; + + context.SecurityTokenHandler = context.Options.JsonWebTokenHandler; + context.TokenValidationParameters = parameters; + + return default; + } + } + + /// + /// Contains the logic responsible of validating reference token identifiers. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class ValidateReferenceTokenIdentifier : IOpenIddictServerHandler + { + private readonly IOpenIddictTokenManager _tokenManager; + + public ValidateReferenceTokenIdentifier() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); + + public ValidateReferenceTokenIdentifier(IOpenIddictTokenManager tokenManager) + => _tokenManager = tokenManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(ResolveTokenValidationParameters.Descriptor.Order + 1_000) + .SetType(OpenIddictServerHandlerType.BuiltIn) + .Build(); + + public async ValueTask HandleAsync(ValidateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + var token = context.Token.Length switch + { + // 12 may correspond to a normalized user code and 43 to any + // other base64url-encoded 256-bit reference token identifier. + 12 or 43 => await _tokenManager.FindByReferenceIdAsync(context.Token), + + // A value higher than 12 (but lower than 50) may correspond to a user code + // containing dashes or any other non-digit character added by the end user. + // In this case, normalize the reference identifier before making the database lookup. + > 12 and < 50 => await _tokenManager.FindByReferenceIdAsync(NormalizeUserCode(context.Token)), + + // If the token length differs, the token cannot be a reference token. + _ => null + }; + + // If the reference token cannot be found, don't return an error to allow another handler to validate it. + if (token is null) + { + return; + } + + // If the type associated with the token entry doesn't match one of the expected types, return an error. + if (context.ValidTokenTypes.Count > 0 && + !await _tokenManager.HasTypeAsync(token, context.ValidTokenTypes.ToImmutableArray())) + { + context.Reject( + error: context.EndpointType switch + { + OpenIddictServerEndpointType.Token => Errors.InvalidGrant, + _ => Errors.InvalidToken + }, + description: context.EndpointType switch + { + OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() + => SR.GetResourceString(SR.ID2001), + OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() + => SR.GetResourceString(SR.ID2002), + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() + => SR.GetResourceString(SR.ID2003), + + _ => SR.GetResourceString(SR.ID2004) + }, + uri: context.EndpointType switch + { + OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() + => SR.FormatID8000(SR.ID2001), + OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() + => SR.FormatID8000(SR.ID2002), + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() + => SR.FormatID8000(SR.ID2003), + + _ => SR.FormatID8000(SR.ID2004), + }); + + return; + } + + var payload = await _tokenManager.GetPayloadAsync(token); + if (string.IsNullOrEmpty(payload)) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0026)); + } + + // Replace the token parameter by the payload resolved from the token entry + // and store the identifier of the reference token so it can be later + // used to restore the properties associated with the token. + context.Token = payload; + context.TokenId = await _tokenManager.GetIdAsync(token); + + // Note: unlike other tokens, user codes may be potentially entered manually by users in a web form. + // To make that easier, user codes are generally "beautified" by adding intermediate dashes to + // make them easier to read and type. Since these additional characters are not part of the original + // user codes, non-digit characters are filtered from the reference identifier using this local method. + static string NormalizeUserCode(string token) + { + var builder = new StringBuilder(token); + for (var index = builder.Length - 1; index >= 0; index--) + { + var character = builder[index]; + if (character < '0' || character > '9') + { + builder.Remove(index, 1); + } + } + + return builder.ToString(); + } + } + } + + /// + /// Contains the logic responsible of validating tokens generated using IdentityModel. + /// + public class ValidateIdentityModelToken : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateReferenceTokenIdentifier.Descriptor.Order + 1_000) + .SetType(OpenIddictServerHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ValidateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // If a principal was already attached, don't overwrite it. + if (context.Principal is not null) + { + return default; + } + + // If the token cannot be read, don't return an error to allow another handler to validate it. + if (!context.SecurityTokenHandler.CanReadToken(context.Token)) + { + return default; + } + + // Special endpoints like introspection or revocation use a single parameter to convey + // multiple types of tokens (typically but not limited to access and refresh tokens). + // + // To speed up the token resolution process, the client can send a "token_type_hint" + // containing the type of the token: if the parameter doesn't match the actual type, + // the authorization server MUST use a fallback mechanism to determine whether the + // token can be introspected or revoked even if it's of a different type. + // + // This logic is not used by OpenIddict for IdentityModel tokens, as processing + // tokens of different type doesn't require re-parsing and re-validating them + // multiple times. As such, the "token_type_hint" parameter is only used in the + // Data Protection integration package and is ignored for IdentityModel tokens. + // + // For more information, see https://datatracker.ietf.org/doc/html/rfc7009#section-2.1 + // and https://datatracker.ietf.org/doc/html/rfc7662#section-2.1. + + var result = context.SecurityTokenHandler.ValidateToken(context.Token, context.TokenValidationParameters); + if (!result.IsValid) + { + context.Logger.LogTrace(result.Exception, SR.GetResourceString(SR.ID6000), context.Token); + + context.Reject( + error: context.EndpointType switch + { + OpenIddictServerEndpointType.Token => Errors.InvalidGrant, + _ => Errors.InvalidToken + }, + description: result.Exception switch + { + SecurityTokenInvalidTypeException => context.EndpointType switch + { + OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() + => SR.GetResourceString(SR.ID2005), + + OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() + => SR.GetResourceString(SR.ID2006), + + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() + => SR.GetResourceString(SR.ID2007), + + OpenIddictServerEndpointType.Userinfo => SR.GetResourceString(SR.ID2008), + + _ => SR.GetResourceString(SR.ID2089) + }, + + SecurityTokenInvalidIssuerException => SR.GetResourceString(SR.ID2088), + SecurityTokenSignatureKeyNotFoundException => SR.GetResourceString(SR.ID2090), + SecurityTokenInvalidSignatureException => SR.GetResourceString(SR.ID2091), + + _ => SR.GetResourceString(SR.ID2004) + }, + uri: result.Exception switch + { + SecurityTokenInvalidTypeException => context.EndpointType switch + { + OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() + => SR.FormatID8000(SR.ID2005), + + OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() + => SR.FormatID8000(SR.ID2006), + + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() + => SR.FormatID8000(SR.ID2007), + + OpenIddictServerEndpointType.Userinfo => SR.FormatID8000(SR.ID2008), + + _ => SR.FormatID8000(SR.ID2089) + }, + + SecurityTokenInvalidIssuerException => SR.FormatID8000(SR.ID2088), + SecurityTokenSignatureKeyNotFoundException => SR.FormatID8000(SR.ID2090), + SecurityTokenInvalidSignatureException => SR.FormatID8000(SR.ID2091), + + _ => SR.FormatID8000(SR.ID2004) + }); + + return default; + } + + // Get the JWT token. If the token is encrypted using JWE, retrieve the inner token. + var token = (JsonWebToken) result.SecurityToken; + if (token.InnerToken is not null) + { + token = token.InnerToken; + } + + // Attach the principal extracted from the token to the parent event context and store + // the token type (resolved from "typ" or "token_usage") as a special private claim. + context.Principal = new ClaimsPrincipal(result.ClaimsIdentity).SetTokenType(result.TokenType switch + { + null or { Length: 0 } => throw new InvalidOperationException(SR.GetResourceString(SR.ID0025)), + + // Both at+jwt and application/at+jwt are supported for access tokens. + JsonWebTokenTypes.AccessToken or JsonWebTokenTypes.Prefixes.Application + JsonWebTokenTypes.AccessToken + => TokenTypeHints.AccessToken, + + // Both JWT and application/JWT are supported for identity tokens. + JsonWebTokenTypes.IdentityToken or JsonWebTokenTypes.Prefixes.Application + JsonWebTokenTypes.IdentityToken + => TokenTypeHints.IdToken, + + JsonWebTokenTypes.Private.AuthorizationCode => TokenTypeHints.AuthorizationCode, + JsonWebTokenTypes.Private.DeviceCode => TokenTypeHints.DeviceCode, + JsonWebTokenTypes.Private.RefreshToken => TokenTypeHints.RefreshToken, + JsonWebTokenTypes.Private.UserCode => TokenTypeHints.UserCode, + + _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003)) + }); + + // Restore the claim destinations from the special oi_cl_dstn claim (represented as a dictionary/JSON object). + if (token.TryGetPayloadValue(Claims.Private.ClaimDestinationsMap, out ImmutableDictionary destinations)) + { + context.Principal.SetDestinations(destinations); + } + + context.Logger.LogTrace(SR.GetResourceString(SR.ID6001), context.Token, context.Principal.Claims); + + return default; + } + } + + /// + /// Contains the logic responsible of normalizing the scope claims stored in the tokens. + /// + public class NormalizeScopeClaims : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateIdentityModelToken.Descriptor.Order + 1_000) + .SetType(OpenIddictServerHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ValidateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.Principal is null) + { + return default; + } + + // Note: in previous OpenIddict versions, scopes were represented as a JSON array + // and deserialized as multiple claims. In OpenIddict 3.0, the public "scope" claim + // is formatted as a unique space-separated string containing all the granted scopes. + // To ensure access tokens generated by previous versions are still correctly handled, + // both formats (unique space-separated string or multiple scope claims) must be supported. + // To achieve that, all the "scope" claims are combined into a single one containg all the values. + // Visit https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-04 for more information. + var scopes = context.Principal.GetClaims(Claims.Scope); + if (scopes.Length > 1) + { + context.Principal.SetClaim(Claims.Scope, string.Join(" ", scopes)); + } + + return default; + } + } + + /// + /// Contains the logic responsible of mapping internal claims used by OpenIddict. + /// + public class MapInternalClaims : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(NormalizeScopeClaims.Descriptor.Order + 1_000) + .SetType(OpenIddictServerHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ValidateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.Principal is null) + { + return default; + } + + // To reduce the size of tokens, some of the private claims used by OpenIddict + // are mapped to their standard equivalent before being removed from the token. + // This handler is responsible of adding back the private claims to the principal + // when receiving the token (e.g "oi_prst" is resolved from the "scope" claim). + + // In OpenIddict 3.0, the creation date of a token is stored in "oi_crt_dt". + // If the claim doesn't exist, try to infer it from the standard "iat" JWT claim. + if (!context.Principal.HasClaim(Claims.Private.CreationDate)) + { + var date = context.Principal.GetClaim(Claims.IssuedAt); + if (!string.IsNullOrEmpty(date) && + long.TryParse(date, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) + { + context.Principal.SetCreationDate(DateTimeOffset.FromUnixTimeSeconds(value)); + } + } + + // In OpenIddict 3.0, the expiration date of a token is stored in "oi_exp_dt". + // If the claim doesn't exist, try to infer it from the standard "exp" JWT claim. + if (!context.Principal.HasClaim(Claims.Private.ExpirationDate)) + { + var date = context.Principal.GetClaim(Claims.ExpiresAt); + if (!string.IsNullOrEmpty(date) && + long.TryParse(date, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) + { + context.Principal.SetExpirationDate(DateTimeOffset.FromUnixTimeSeconds(value)); + } + } + + // In OpenIddict 3.0, the audiences allowed to receive a token are stored in "oi_aud". + // If no such claim exists, try to infer them from the standard "aud" JWT claims. + if (!context.Principal.HasClaim(Claims.Private.Audience)) + { + var audiences = context.Principal.GetClaims(Claims.Audience); + if (audiences.Any()) + { + context.Principal.SetAudiences(audiences); + } + } + + // In OpenIddict 3.0, the presenters allowed to use a token are stored in "oi_prst". + // If no such claim exists, try to infer them from the standard "azp" and "client_id" JWT claims. + // + // Note: in previous OpenIddict versions, the presenters were represented in JWT tokens + // using the "azp" claim (defined by OpenID Connect), for which a single value could be + // specified. To ensure presenters stored in JWT tokens created by OpenIddict 1.x/2.x + // can still be read with OpenIddict 3.0, the presenter is automatically inferred from + // the "azp" or "client_id" claim if no "oi_prst" claim was found in the principal. + if (!context.Principal.HasClaim(Claims.Private.Presenter)) + { + var presenter = context.Principal.GetClaim(Claims.AuthorizedParty) ?? + context.Principal.GetClaim(Claims.ClientId); + + if (!string.IsNullOrEmpty(presenter)) + { + context.Principal.SetPresenters(presenter); + } + } + + // In OpenIddict 3.0, the scopes granted to an application are stored in "oi_scp". + // If no such claim exists, try to infer them from the standard "scope" JWT claim, + // which is guaranteed to be a unique space-separated claim containing all the values. + if (!context.Principal.HasClaim(Claims.Private.Scope)) + { + var scope = context.Principal.GetClaim(Claims.Scope); + if (!string.IsNullOrEmpty(scope)) + { + context.Principal.SetScopes(scope.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries)); + } + } + + return default; + } + } + + /// + /// Contains the logic responsible of restoring the properties associated with a reference token entry. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class RestoreReferenceTokenProperties : IOpenIddictServerHandler + { + private readonly IOpenIddictTokenManager _tokenManager; + + public RestoreReferenceTokenProperties() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); + + public RestoreReferenceTokenProperties(IOpenIddictTokenManager tokenManager) + => _tokenManager = tokenManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(MapInternalClaims.Descriptor.Order + 1_000) + .SetType(OpenIddictServerHandlerType.BuiltIn) + .Build(); + + public async ValueTask HandleAsync(ValidateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.Principal is null || string.IsNullOrEmpty(context.TokenId)) + { + return; + } + + var token = await _tokenManager.FindByIdAsync(context.TokenId); + if (token is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0021)); + } + + // Restore the creation/expiration dates/identifiers from the token entry metadata. + context.Principal.SetCreationDate(await _tokenManager.GetCreationDateAsync(token)) + .SetExpirationDate(await _tokenManager.GetExpirationDateAsync(token)) + .SetAuthorizationId(await _tokenManager.GetAuthorizationIdAsync(token)) + .SetTokenId(await _tokenManager.GetIdAsync(token)) + .SetTokenType(await _tokenManager.GetTypeAsync(token)); + } + } + + /// + /// Contains the logic responsible of rejecting authentication demands for which no valid principal was resolved. + /// + public class ValidatePrincipal : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(RestoreReferenceTokenProperties.Descriptor.Order + 1_000) + .SetType(OpenIddictServerHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ValidateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.Principal is null) + { + context.Reject( + error: context.EndpointType switch + { + OpenIddictServerEndpointType.Token => Errors.InvalidGrant, + _ => Errors.InvalidToken + }, + description: context.EndpointType switch + { + OpenIddictServerEndpointType.Authorization or OpenIddictServerEndpointType.Logout + => SR.GetResourceString(SR.ID2009), + + OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() + => SR.GetResourceString(SR.ID2001), + OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() + => SR.GetResourceString(SR.ID2002), + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() + => SR.GetResourceString(SR.ID2003), + + _ => SR.GetResourceString(SR.ID2004) + }, + uri: context.EndpointType switch + { + OpenIddictServerEndpointType.Authorization or OpenIddictServerEndpointType.Logout + => SR.FormatID8000(SR.ID2009), + + OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() + => SR.FormatID8000(SR.ID2001), + OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() + => SR.FormatID8000(SR.ID2002), + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() + => SR.FormatID8000(SR.ID2003), + + _ => SR.FormatID8000(SR.ID2004) + }); + + + return default; + } + + // When using JWT or Data Protection tokens, the correct token type is always enforced by IdentityModel + // (using the "typ" header) or by ASP.NET Core Data Protection (using per-token-type purposes strings). + // To ensure tokens deserialized using a custom routine are of the expected type, a manual check is used, + // which requires that a special claim containing the token type be present in the security principal. + if (context.ValidTokenTypes.Count > 0) + { + var type = context.Principal.GetTokenType(); + if (string.IsNullOrEmpty(type)) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0004)); + } + + if (!context.ValidTokenTypes.Contains(type)) + { + throw new InvalidOperationException(SR.FormatID0005(type, string.Join(", ", context.ValidTokenTypes))); + } + } + + return default; + } + } + + /// + /// Contains the logic responsible of rejecting authentication demands that + /// use a token whose entry is no longer valid (e.g was revoked). + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class ValidateTokenEntry : IOpenIddictServerHandler + { + private readonly IOpenIddictTokenManager _tokenManager; + + public ValidateTokenEntry() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); + + public ValidateTokenEntry(IOpenIddictTokenManager tokenManager) + => _tokenManager = tokenManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidatePrincipal.Descriptor.Order + 1_000) + .SetType(OpenIddictServerHandlerType.BuiltIn) + .Build(); + + public async ValueTask HandleAsync(ValidateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + + // Extract the token identifier from the authentication principal. + // If no token identifier can be found, this indicates that the token + // has no backing database entry (e.g an access token or an identity token). + var identifier = context.Principal.GetTokenId(); + if (string.IsNullOrEmpty(identifier)) + { + return; + } + + // If the token entry cannot be found, return a generic error. + var token = await _tokenManager.FindByIdAsync(identifier); + if (token is null) + { + context.Reject( + error: context.EndpointType switch + { + OpenIddictServerEndpointType.Token => Errors.InvalidGrant, + _ => Errors.InvalidToken + }, + description: context.EndpointType switch + { + OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() + => SR.GetResourceString(SR.ID2001), + OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() + => SR.GetResourceString(SR.ID2002), + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() + => SR.GetResourceString(SR.ID2003), + + _ => SR.GetResourceString(SR.ID2004) + }, + uri: context.EndpointType switch + { + OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() + => SR.FormatID8000(SR.ID2001), + OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() + => SR.FormatID8000(SR.ID2002), + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() + => SR.FormatID8000(SR.ID2003), + + _ => SR.FormatID8000(SR.ID2004) + }); + + return; + } + + if (context.EndpointType == OpenIddictServerEndpointType.Token && (context.Request.IsAuthorizationCodeGrantType() || + context.Request.IsDeviceCodeGrantType() || + context.Request.IsRefreshTokenGrantType())) + { + // If the authorization code/device code/refresh token is already marked as redeemed, this may indicate + // that it was compromised. In this case, revoke the entire chain of tokens associated with the authorization. + // Special logic is used to avoid revoking refresh tokens already marked as redeemed to allow for a small leeway. + // Note: the authorization itself is not revoked to allow the legitimate client to start a new flow. + // See https://tools.ietf.org/html/rfc6749#section-10.5 for more information. + if (await _tokenManager.HasStatusAsync(token, Statuses.Redeemed)) + { + if (!context.Request.IsRefreshTokenGrantType() || !await IsReusableAsync(token)) + { + context.Logger.LogInformation(SR.GetResourceString(SR.ID6002), identifier); + + context.Reject( + error: context.EndpointType switch + { + OpenIddictServerEndpointType.Token => Errors.InvalidGrant, + + _ => Errors.InvalidToken + }, + description: context.EndpointType switch + { + OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() + => SR.GetResourceString(SR.ID2010), + OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() + => SR.GetResourceString(SR.ID2011), + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() + => SR.GetResourceString(SR.ID2012), + + _ => SR.GetResourceString(SR.ID2013) + }, + uri: context.EndpointType switch + { + OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() + => SR.FormatID8000(SR.ID2010), + OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() + => SR.FormatID8000(SR.ID2011), + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() + => SR.FormatID8000(SR.ID2012), + + _ => SR.FormatID8000(SR.ID2013) + }); + + // Revoke all the token entries associated with the authorization. + await TryRevokeChainAsync(await _tokenManager.GetAuthorizationIdAsync(token)); + + return; + } + + return; + } + + if (context.Request.IsDeviceCodeGrantType()) + { + // If the device code is not marked as valid yet, return an authorization_pending error. + if (await _tokenManager.HasStatusAsync(token, Statuses.Inactive)) + { + context.Logger.LogInformation(SR.GetResourceString(SR.ID6003), identifier); + + context.Reject( + error: Errors.AuthorizationPending, + description: SR.GetResourceString(SR.ID2014), + uri: SR.FormatID8000(SR.ID2014)); + + return; + } + + // If the device code is marked as rejected, return an access_denied error. + if (await _tokenManager.HasStatusAsync(token, Statuses.Rejected)) + { + context.Logger.LogInformation(SR.GetResourceString(SR.ID6004), identifier); + + context.Reject( + error: Errors.AccessDenied, + description: SR.GetResourceString(SR.ID2015), + uri: SR.FormatID8000(SR.ID2015)); + + return; + } + } + } + + if (!await _tokenManager.HasStatusAsync(token, Statuses.Valid)) + { + context.Logger.LogInformation(SR.GetResourceString(SR.ID6005), identifier); + + context.Reject( + error: context.EndpointType switch + { + OpenIddictServerEndpointType.Token => Errors.InvalidGrant, + _ => Errors.InvalidToken + }, + description: context.EndpointType switch + { + OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() + => SR.GetResourceString(SR.ID2016), + OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() + => SR.GetResourceString(SR.ID2017), + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() + => SR.GetResourceString(SR.ID2018), + + _ => SR.GetResourceString(SR.ID2019) + }, + uri: context.EndpointType switch + { + OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() + => SR.FormatID8000(SR.ID2016), + OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() + => SR.FormatID8000(SR.ID2017), + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() + => SR.FormatID8000(SR.ID2018), + + _ => SR.FormatID8000(SR.ID2019) + }); + + return; + } + + // Restore the creation/expiration dates/identifiers from the token entry metadata. + context.Principal.SetCreationDate(await _tokenManager.GetCreationDateAsync(token)) + .SetExpirationDate(await _tokenManager.GetExpirationDateAsync(token)) + .SetAuthorizationId(await _tokenManager.GetAuthorizationIdAsync(token)) + .SetTokenId(await _tokenManager.GetIdAsync(token)) + .SetTokenType(await _tokenManager.GetTypeAsync(token)); + + async ValueTask IsReusableAsync(object token) + { + // If the reuse leeway was set to null, return false to indicate + // that the refresh token is already redeemed and cannot be reused. + if (context.Options.RefreshTokenReuseLeeway is null) + { + return false; + } + + var date = await _tokenManager.GetRedemptionDateAsync(token); + if (date is null || DateTimeOffset.UtcNow < date + context.Options.RefreshTokenReuseLeeway) + { + return true; + } + + return false; + } + + async ValueTask TryRevokeChainAsync(string? identifier) + { + if (string.IsNullOrEmpty(identifier)) + { + return; + } + + // Revoke all the token entries associated with the authorization, + // including the redeemed token that was used in the token request. + await foreach (var token in _tokenManager.FindByAuthorizationIdAsync(identifier)) + { + await _tokenManager.TryRevokeAsync(token); + } + } + } + } + + /// + /// Contains the logic responsible of authentication demands a token whose + /// associated authorization entry is no longer valid (e.g was revoked). + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class ValidateAuthorizationEntry : IOpenIddictServerHandler + { + private readonly IOpenIddictAuthorizationManager _authorizationManager; + + public ValidateAuthorizationEntry() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); + + public ValidateAuthorizationEntry(IOpenIddictAuthorizationManager authorizationManager) + => _authorizationManager = authorizationManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateTokenEntry.Descriptor.Order + 1_000) + .SetType(OpenIddictServerHandlerType.BuiltIn) + .Build(); + + public async ValueTask HandleAsync(ValidateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + + var identifier = context.Principal.GetAuthorizationId(); + if (string.IsNullOrEmpty(identifier)) + { + return; + } + + var authorization = await _authorizationManager.FindByIdAsync(identifier); + if (authorization is null || !await _authorizationManager.HasStatusAsync(authorization, Statuses.Valid)) + { + context.Logger.LogInformation(SR.GetResourceString(SR.ID6006), identifier); + + context.Reject( + error: context.EndpointType switch + { + OpenIddictServerEndpointType.Token => Errors.InvalidGrant, + _ => Errors.InvalidToken + }, + description: context.EndpointType switch + { + OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() + => SR.GetResourceString(SR.ID2020), + OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() + => SR.GetResourceString(SR.ID2021), + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() + => SR.GetResourceString(SR.ID2022), + + _ => SR.GetResourceString(SR.ID2023) + }, + uri: context.EndpointType switch + { + OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() + => SR.FormatID8000(SR.ID2020), + OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() + => SR.FormatID8000(SR.ID2021), + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() + => SR.FormatID8000(SR.ID2022), + + _ => SR.FormatID8000(SR.ID2023) + }); + + return; + } + } + } + + /// + /// Contains the logic responsible of rejecting authentication demands that use an expired token. + /// + public class ValidateExpirationDate : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateTokenEntry.Descriptor.Order + 1_000) + .SetType(OpenIddictServerHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ValidateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + + // Don't validate the lifetime of id_tokens used as id_token_hints. + if (context.ValidTokenTypes.Count is 1 && context.ValidTokenTypes.ElementAt(0) is TokenTypeHints.IdToken && + context.EndpointType is OpenIddictServerEndpointType.Authorization or OpenIddictServerEndpointType.Logout) + { + return default; + } + + var date = context.Principal.GetExpirationDate(); + if (date.HasValue && date.Value < DateTimeOffset.UtcNow) + { + context.Reject( + error: context.EndpointType switch + { + OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() + => Errors.ExpiredToken, + + OpenIddictServerEndpointType.Token => Errors.InvalidGrant, + + _ => Errors.InvalidToken + }, + description: context.EndpointType switch + { + OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() + => SR.GetResourceString(SR.ID2016), + OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() + => SR.GetResourceString(SR.ID2017), + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() + => SR.GetResourceString(SR.ID2018), + + _ => SR.GetResourceString(SR.ID2019) + }, + uri: context.EndpointType switch + { + OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() + => SR.FormatID8000(SR.ID2016), + OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() + => SR.FormatID8000(SR.ID2017), + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() + => SR.FormatID8000(SR.ID2018), + + _ => SR.FormatID8000(SR.ID2019) + }); + + return default; + } + + return default; + } + } + + /// + /// Contains the logic responsible of resolving the signing and encryption credentials used to protect tokens. + /// + public class AttachSecurityCredentials : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .SetType(OpenIddictServerHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(GenerateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + context.SecurityTokenHandler = context.Options.JsonWebTokenHandler; + + context.EncryptionCredentials = context.TokenType switch + { + // Note: unlike other tokens, encryption can be disabled for access tokens. + TokenTypeHints.AccessToken when context.Options.DisableAccessTokenEncryption => null, + TokenTypeHints.IdToken => null, + + _ => context.Options.EncryptionCredentials.First() + }; + + context.SigningCredentials = context.TokenType switch + { + // Note: unlike other tokens, identity tokens can only be signed using an asymmetric key + // as they are meant to be validated by clients using the public keys exposed by the server. + TokenTypeHints.IdToken => context.Options.SigningCredentials.First(credentials => + credentials.Key is AsymmetricSecurityKey), + + _ => context.Options.SigningCredentials.First() + }; + + return default; + } + } + + /// + /// Contains the logic responsible of creating a token entry. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class CreateTokenEntry : IOpenIddictServerHandler + { + private readonly IOpenIddictApplicationManager _applicationManager; + private readonly IOpenIddictTokenManager _tokenManager; + + public CreateTokenEntry() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); + + public CreateTokenEntry( + IOpenIddictApplicationManager applicationManager, + IOpenIddictTokenManager tokenManager) + { + _applicationManager = applicationManager; + _tokenManager = tokenManager; + } + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(AttachSecurityCredentials.Descriptor.Order + 1_000) + .SetType(OpenIddictServerHandlerType.BuiltIn) + .Build(); + + /// + public async ValueTask HandleAsync(GenerateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + var descriptor = new OpenIddictTokenDescriptor + { + AuthorizationId = context.Principal.GetAuthorizationId(), + CreationDate = context.Principal.GetCreationDate(), + ExpirationDate = context.Principal.GetExpirationDate(), + Principal = context.Principal, + Type = context.TokenType + }; + + descriptor.Status = context.TokenType switch + { + // When initially created, device codes are marked as inactive. When the user + // approves the authorization demand, the UpdateReferenceDeviceCodeEntry handler + // changes the status to "active" and attaches a new payload with the claims + // corresponding the user, which allows the client to redeem the device code. + TokenTypeHints.DeviceCode => Statuses.Inactive, + + // For all other tokens, "valid" is the default status. + _ => Statuses.Valid + }; + + descriptor.Subject = context.TokenType switch + { + // Device and user codes are not bound to a user, until authorization is granted. + TokenTypeHints.DeviceCode or TokenTypeHints.UserCode => null, + + // For all other tokens, the subject is resolved from the principal. + _ => context.Principal.GetClaim(Claims.Subject) + }; + + // If the client application is known, associate it with the token. + if (!string.IsNullOrEmpty(context.Request.ClientId)) + { + var application = await _applicationManager.FindByClientIdAsync(context.Request.ClientId); + if (application is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0017)); + } + + descriptor.ApplicationId = await _applicationManager.GetIdAsync(application); + } + + var token = await _tokenManager.CreateAsync(descriptor); + if (token is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0019)); + } + + var identifier = await _tokenManager.GetIdAsync(token); + + // Attach the token identifier to the principal so that it can be stored in the token. + context.Principal.SetTokenId(identifier); + + context.Logger.LogTrace(SR.GetResourceString(SR.ID6012), context.TokenType, identifier); + } + } + + /// + /// Contains the logic responsible of generating a token using IdentityModel. + /// + public class GenerateIdentityModelToken : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(CreateTokenEntry.Descriptor.Order + 1_000) + .SetType(OpenIddictServerHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(GenerateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // If a token was already attached by another handler, don't overwrite it. + if (!string.IsNullOrEmpty(context.Token)) + { + return default; + } + + if (context.Principal is null or { Identity: not ClaimsIdentity }) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0022)); + } + + // Clone the principal and exclude the private claims mapped to standard JWT claims. + var principal = context.Principal.Clone(claim => claim.Type switch + { + Claims.Private.CreationDate or Claims.Private.ExpirationDate or Claims.Private.TokenType => false, + + Claims.Private.Audience + when context.TokenType is TokenTypeHints.AccessToken or TokenTypeHints.IdToken => false, + + Claims.Private.Scope when context.TokenType is TokenTypeHints.AccessToken => false, + + _ => true + }); + + if (principal is null or { Identity: not ClaimsIdentity }) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0020)); + } + + var claims = new Dictionary(StringComparer.Ordinal); + + // For access and identity tokens, set the public audience claims + // using the private audience claims from the security principal. + if (context.TokenType is TokenTypeHints.AccessToken or TokenTypeHints.IdToken) + { + var audiences = context.Principal.GetAudiences(); + if (audiences.Any()) + { + claims.Add(Claims.Audience, audiences.Length switch + { + 1 => audiences.ElementAt(0), + _ => audiences + }); + } + } + + // For access tokens, set the public scope claim using the private scope claims from the principal. + // Note: scopes are deliberately formatted as a single space-separated + // string to respect the usual representation of the standard scope claim. + // See https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-04. + if (context.TokenType is TokenTypeHints.AccessToken) + { + var scopes = context.Principal.GetScopes(); + if (scopes.Any()) + { + claims.Add(Claims.Scope, string.Join(" ", scopes)); + } + } + + // For authorization/device/user codes and refresh tokens, + // attach claims destinations to the JWT claims collection. + if (context.TokenType is TokenTypeHints.AuthorizationCode or TokenTypeHints.DeviceCode or + TokenTypeHints.RefreshToken or TokenTypeHints.UserCode) + { + var destinations = principal.GetDestinations(); + if (destinations.Count != 0) + { + claims.Add(Claims.Private.ClaimDestinationsMap, destinations); + } + } + + var descriptor = new SecurityTokenDescriptor + { + Claims = claims, + EncryptingCredentials = context.EncryptionCredentials, + Expires = context.Principal.GetExpirationDate()?.UtcDateTime, + IssuedAt = context.Principal.GetCreationDate()?.UtcDateTime, + Issuer = context.Issuer?.AbsoluteUri, + SigningCredentials = context.SigningCredentials, + Subject = (ClaimsIdentity) principal.Identity, + TokenType = context.TokenType switch + { + null or { Length: 0 } => throw new InvalidOperationException(SR.GetResourceString(SR.ID0025)), + + TokenTypeHints.AccessToken => JsonWebTokenTypes.AccessToken, + TokenTypeHints.IdToken => JsonWebTokenTypes.IdentityToken, + TokenTypeHints.AuthorizationCode => JsonWebTokenTypes.Private.AuthorizationCode, + TokenTypeHints.DeviceCode => JsonWebTokenTypes.Private.DeviceCode, + TokenTypeHints.RefreshToken => JsonWebTokenTypes.Private.RefreshToken, + TokenTypeHints.UserCode => JsonWebTokenTypes.Private.UserCode, + + _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003)) + } + }; + + context.Token = context.SecurityTokenHandler.CreateToken(descriptor); + + context.Logger.LogTrace(SR.GetResourceString(SR.ID6013), context.TokenType, context.Token, principal.Claims); + + return default; + } + } + + /// + /// Contains the logic responsible of converting the token to a reference token. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class ConvertReferenceToken : IOpenIddictServerHandler + { + private readonly IOpenIddictTokenManager _tokenManager; + + public ConvertReferenceToken() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); + + public ConvertReferenceToken(IOpenIddictTokenManager tokenManager) + => _tokenManager = tokenManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(GenerateIdentityModelToken.Descriptor.Order + 1_000) + .SetType(OpenIddictServerHandlerType.BuiltIn) + .Build(); + + /// + public async ValueTask HandleAsync(GenerateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (!(context.TokenType switch + { + // Access and refresh tokens can be converted to reference tokens + // if the corresponding option was enabled in the server options. + TokenTypeHints.AccessToken => context.Options.UseReferenceAccessTokens, + TokenTypeHints.RefreshToken => context.Options.UseReferenceRefreshTokens, + + // By default, authorization/user codes are always converted to reference tokens. + TokenTypeHints.AuthorizationCode or TokenTypeHints.UserCode => true, + + // Device codes are only converted to reference tokens if they are not generated + // as part of a device code swap made by the user code verification endpoint. + TokenTypeHints.DeviceCode => context.EndpointType is not OpenIddictServerEndpointType.Verification, + + // Identity tokens cannot be converted to reference tokens. + TokenTypeHints.IdToken => false, + + _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003)) + })) + { + return; + } + + var identifier = context.Principal.GetTokenId(); + if (string.IsNullOrEmpty(identifier)) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0009)); + } + + var token = await _tokenManager.FindByIdAsync(identifier); + if (token is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0021)); + } + + var descriptor = new OpenIddictTokenDescriptor(); + await _tokenManager.PopulateAsync(descriptor, token); + + // Attach the generated token to the token entry. + descriptor.Payload = context.Token; + descriptor.Principal = context.Principal; + + // Note: unlike other reference tokens, user codes are meant to be used by humans, + // who may have to enter it in a web form. To ensure it remains easy enough to type + // even by users with non-Latin keyboards, user codes generated by OpenIddict are + // only compound of 12 digits, generated using a crypto-secure random number generator. + // In this case, the resulting user code is estimated to have at most ~40 bits of entropy. + if (context.TokenType is TokenTypeHints.UserCode) + { + do + { + var data = new byte[12]; +#if SUPPORTS_STATIC_RANDOM_NUMBER_GENERATOR_METHODS + RandomNumberGenerator.Fill(data); +#else + using var generator = RandomNumberGenerator.Create(); + generator.GetBytes(data); +#endif + var builder = new StringBuilder(data.Length); + + for (var index = 0; index < data.Length; index += 4) + { + builder.AppendFormat(CultureInfo.InvariantCulture, "{0:D4}", BitConverter.ToUInt32(data, index) % 10000); + } + + descriptor.ReferenceId = builder.ToString(); + } + + // User codes are relatively short. To help reduce the risks of collisions with + // existing entries, a database check is performed here before updating the entry. + while (await _tokenManager.FindByReferenceIdAsync(descriptor.ReferenceId) is not null); + } + + // For other tokens, generate a base64url-encoded 256-bit random identifier. + else + { + var data = new byte[256 / 8]; +#if SUPPORTS_STATIC_RANDOM_NUMBER_GENERATOR_METHODS + RandomNumberGenerator.Fill(data); +#else + using var generator = RandomNumberGenerator.Create(); + generator.GetBytes(data); +#endif + + descriptor.ReferenceId = Base64UrlEncoder.Encode(data); + } + + await _tokenManager.UpdateAsync(token, descriptor); + + // Replace the returned token by the reference identifier. + context.Token = descriptor.ReferenceId; + + context.Logger.LogTrace(SR.GetResourceString(SR.ID6014), context.TokenType, identifier, descriptor.ReferenceId); + } + } + + /// + /// Contains the logic responsible of beautifying user-typed tokens. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class BeautifyToken : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + // Technically, this handler doesn't require that the degraded mode be disabled + // but the default CreateReferenceEntry handler that creates the user code + // reference identifiers only works when the degraded mode is disabled. + .AddFilter() + .UseSingletonHandler() + .SetOrder(ConvertReferenceToken.Descriptor.Order + 1_000) + .SetType(OpenIddictServerHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(GenerateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // To make user codes easier to read and type by humans, a dash is automatically + // appended before each new block of 4 integers. These dashes are expected to be + // stripped from the user codes when receiving them at the verification endpoint. + if (context.TokenType is TokenTypeHints.UserCode) + { + var builder = new StringBuilder(context.Token); + if (builder.Length % 4 != 0) + { + return default; + } + + for (var index = builder.Length; index >= 0; index -= 4) + { + if (index != 0 && index != builder.Length) + { + builder.Insert(index, Separators.Dash[0]); + } + } + + context.Token = builder.ToString(); + } + + return default; + } + } + } + } +} diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs index 366b0ddb..ba7eff0d 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs @@ -643,6 +643,10 @@ namespace OpenIddict.Server var notification = new ProcessAuthenticationContext(context.Transaction); await _dispatcher.DispatchAsync(notification); + // Store the context object in the transaction so it can be later retrieved by handlers + // that want to access the authentication result without triggering a new authentication flow. + context.Transaction.SetProperty(typeof(ProcessAuthenticationContext).FullName!, notification); + if (notification.IsRequestHandled) { context.HandleRequest(); @@ -665,7 +669,7 @@ namespace OpenIddict.Server } // Attach the security principal extracted from the token to the validation context. - context.Principal = notification.Principal; + context.Principal = notification.GenericTokenPrincipal; } } diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs index e97b547f..c91b9022 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs @@ -380,6 +380,10 @@ namespace OpenIddict.Server var notification = new ProcessAuthenticationContext(context.Transaction); await _dispatcher.DispatchAsync(notification); + // Store the context object in the transaction so it can be later retrieved by handlers + // that want to access the authentication result without triggering a new authentication flow. + context.Transaction.SetProperty(typeof(ProcessAuthenticationContext).FullName!, notification); + if (notification.IsRequestHandled) { context.HandleRequest(); @@ -402,7 +406,7 @@ namespace OpenIddict.Server } // Attach the security principal extracted from the token to the validation context. - context.Principal = notification.Principal; + context.Principal = notification.AccessTokenPrincipal; } } diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs index 1e2238ad..6b425d35 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs @@ -9,20 +9,17 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel; using System.Diagnostics; -using System.Globalization; using System.Linq; using System.Security.Claims; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; using OpenIddict.Abstractions; using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Server.OpenIddictServerEvents; using static OpenIddict.Server.OpenIddictServerHandlerFilters; -using Properties = OpenIddict.Server.OpenIddictServerConstants.Properties; using SR = OpenIddict.Abstractions.OpenIddictResources; namespace OpenIddict.Server @@ -35,17 +32,15 @@ namespace OpenIddict.Server * Authentication processing: */ ValidateAuthenticationDemand.Descriptor, - ValidateTokenParameter.Descriptor, - NormalizeUserCode.Descriptor, - ValidateReferenceTokenIdentifier.Descriptor, - ValidateIdentityModelToken.Descriptor, - NormalizeScopeClaims.Descriptor, - MapInternalClaims.Descriptor, - RestoreReferenceTokenProperties.Descriptor, - ValidatePrincipal.Descriptor, - ValidateTokenEntry.Descriptor, - ValidateAuthorizationEntry.Descriptor, - ValidateExpirationDate.Descriptor, + EvaluateValidatedTokens.Descriptor, + ResolveValidatedTokens.Descriptor, + ValidateAccessToken.Descriptor, + ValidateAuthorizationCode.Descriptor, + ValidateDeviceCode.Descriptor, + ValidateGenericToken.Descriptor, + ValidateIdentityToken.Descriptor, + ValidateRefreshToken.Descriptor, + ValidateUserCode.Descriptor, /* * Challenge processing: @@ -63,7 +58,7 @@ namespace OpenIddict.Server AttachDefaultScopes.Descriptor, AttachDefaultPresenters.Descriptor, InferResources.Descriptor, - EvaluateTokenTypes.Descriptor, + EvaluateGeneratedTokens.Descriptor, AttachAuthorization.Descriptor, PrepareAccessTokenPrincipal.Descriptor, @@ -75,33 +70,18 @@ namespace OpenIddict.Server RedeemTokenEntry.Descriptor, - CreateAccessTokenEntry.Descriptor, - GenerateIdentityModelAccessToken.Descriptor, - ConvertReferenceAccessToken.Descriptor, + GenerateAccessToken.Descriptor, + GenerateAuthorizationCode.Descriptor, + GenerateDeviceCode.Descriptor, + GenerateRefreshToken.Descriptor, - CreateAuthorizationCodeEntry.Descriptor, - GenerateIdentityModelAuthorizationCode.Descriptor, - ConvertReferenceAuthorizationCode.Descriptor, - - CreateDeviceCodeEntry.Descriptor, - GenerateIdentityModelDeviceCode.Descriptor, - ConvertReferenceDeviceCode.Descriptor, - UpdateReferenceDeviceCodeEntry.Descriptor, - - CreateRefreshTokenEntry.Descriptor, - GenerateIdentityModelRefreshToken.Descriptor, - ConvertReferenceRefreshToken.Descriptor, - - CreateUserCodeEntry.Descriptor, AttachDeviceCodeIdentifier.Descriptor, - GenerateIdentityModelUserCode.Descriptor, - ConvertReferenceUserCode.Descriptor, - + UpdateReferenceDeviceCodeEntry.Descriptor, AttachTokenDigests.Descriptor, - CreateIdentityTokenEntry.Descriptor, - GenerateIdentityModelIdentityToken.Descriptor, - BeautifyUserCode.Descriptor, + GenerateUserCode.Descriptor, + GenerateIdentityToken.Descriptor, + AttachTokenParameters.Descriptor, /* @@ -114,6 +94,7 @@ namespace OpenIddict.Server .AddRange(Discovery.DefaultHandlers) .AddRange(Exchange.DefaultHandlers) .AddRange(Introspection.DefaultHandlers) + .AddRange(Protection.DefaultHandlers) .AddRange(Revocation.DefaultHandlers) .AddRange(Session.DefaultHandlers) .AddRange(Userinfo.DefaultHandlers); @@ -163,16 +144,16 @@ namespace OpenIddict.Server } /// - /// Contains the logic responsible of resolving the token from the incoming request. + /// Contains the logic responsible of selecting the token types that should be validated. /// - public class ValidateTokenParameter : IOpenIddictServerHandler + public class EvaluateValidatedTokens : IOpenIddictServerHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() + .UseSingletonHandler() .SetOrder(ValidateAuthenticationDemand.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -185,64 +166,81 @@ namespace OpenIddict.Server throw new ArgumentNullException(nameof(context)); } - var (token, type) = context.EndpointType switch + (context.ValidateAccessToken, context.RequireAccessToken) = context.EndpointType switch + { + // The userinfo endpoint requires sending a valid access token. + OpenIddictServerEndpointType.Userinfo => (true, true), + + _ => (false, false) + }; + + (context.ValidateAuthorizationCode, context.RequireAuthorizationCode) = context.EndpointType switch + { + // The authorization code grant requires sending a valid authorization code. + OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() => (true, true), + + _ => (false, false) + }; + + (context.ValidateDeviceCode, context.RequireDeviceCode) = context.EndpointType switch { - OpenIddictServerEndpointType.Authorization or OpenIddictServerEndpointType.Logout - => (context.Request.IdTokenHint, TokenTypeHints.IdToken), + // The device code grant requires sending a valid device code. + OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() => (true, true), + + _ => (false, false) + }; + (context.ValidateGenericToken, context.RequireGenericToken) = context.EndpointType switch + { // Tokens received by the introspection and revocation endpoints can be of any type. // Additional token type filtering is made by the endpoint themselves, if needed. - OpenIddictServerEndpointType.Introspection or OpenIddictServerEndpointType.Revocation - => (context.Request.Token, null), - - OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() - => (context.Request.Code, TokenTypeHints.AuthorizationCode), - OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() - => (context.Request.DeviceCode, TokenTypeHints.DeviceCode), - OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() - => (context.Request.RefreshToken, TokenTypeHints.RefreshToken), + OpenIddictServerEndpointType.Introspection or OpenIddictServerEndpointType.Revocation => (true, true), - OpenIddictServerEndpointType.Userinfo => (context.Request.AccessToken, TokenTypeHints.AccessToken), + _ => (false, false) + }; - OpenIddictServerEndpointType.Verification => (context.Request.UserCode, TokenTypeHints.UserCode), + (context.ValidateIdentityToken, context.RequireIdentityToken) = context.EndpointType switch + { + // The identity token received by the authorization and logout + // endpoints are not required and serve as optional hints. + OpenIddictServerEndpointType.Authorization or OpenIddictServerEndpointType.Logout => (true, false), - _ => (null, null) + _ => (false, false) }; - if (string.IsNullOrEmpty(token)) + (context.ValidateRefreshToken, context.RequireRefreshToken) = context.EndpointType switch { - context.Reject( - error: Errors.InvalidRequest, - description: SR.GetResourceString(SR.ID2000), - uri: SR.FormatID8000(SR.ID2000)); + // The refresh token grant requires sending a valid refresh token. + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() => (true, true), - return default; - } + _ => (false, false) + }; - context.Token = token; - context.TokenType = type; + (context.ValidateUserCode, context.RequireUserCode) = context.EndpointType switch + { + // Note: the verification endpoint can be accessed without specifying a + // user code (that can be later set by the user using a form, for instance). + OpenIddictServerEndpointType.Verification => (true, false), + + _ => (false, false) + }; return default; } } /// - /// Contains the logic responsible of normalizing user codes. - /// Note: this handler is not used when the degraded mode is enabled. + /// Contains the logic responsible of resolving the token from the incoming request. /// - public class NormalizeUserCode : IOpenIddictServerHandler + public class ResolveValidatedTokens : IOpenIddictServerHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - // Technically, this handler doesn't require that the degraded mode be disabled - // but the default CreateReferenceUserCodeEntry that creates the user code - // reference identifiers only works when the degraded mode is disabled. - .AddFilter() - .UseSingletonHandler() - .SetOrder(ValidateTokenParameter.Descriptor.Order + 1_000) + .UseSingletonHandler() + .SetOrder(EvaluateValidatedTokens.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -254,57 +252,85 @@ namespace OpenIddict.Server throw new ArgumentNullException(nameof(context)); } - if (!string.Equals(context.TokenType, TokenTypeHints.UserCode, StringComparison.OrdinalIgnoreCase)) + context.AccessToken = context.EndpointType switch { - return default; - } + OpenIddictServerEndpointType.Userinfo when context.ValidateAccessToken => context.Request.AccessToken, + + _ => null + }; + + context.AuthorizationCode = context.EndpointType switch + { + OpenIddictServerEndpointType.Token when context.ValidateAuthorizationCode => context.Request.Code, - // Note: unlike other tokens, user codes may be potentially entered manually by users in a web form. - // To make that easier, user codes are generally "beautified" by adding intermediate dashes to - // make them easier to read and type. Since these additional characters are not part of the original - // user codes, non-digit characters are automatically filtered from the reference identifier. + _ => null + }; - var builder = new StringBuilder(context.Token); - for (var index = builder.Length - 1; index >= 0; index--) + context.DeviceCode = context.EndpointType switch { - var character = builder[index]; - if (character < '0' || character > '9') - { - builder.Remove(index, 1); - } - } + OpenIddictServerEndpointType.Token when context.ValidateDeviceCode => context.Request.DeviceCode, + + _ => null + }; + + (context.GenericToken, context.GenericTokenTypeHint) = context.EndpointType switch + { + OpenIddictServerEndpointType.Introspection or + OpenIddictServerEndpointType.Revocation + when context.ValidateGenericToken => (context.Request.Token, context.Request.TokenTypeHint), + + _ => (null, null) + }; - context.Token = builder.ToString(); + context.IdentityToken = context.EndpointType switch + { + OpenIddictServerEndpointType.Authorization or + OpenIddictServerEndpointType.Logout + when context.ValidateIdentityToken => context.Request.IdTokenHint, + + _ => null + }; + + context.RefreshToken = context.EndpointType switch + { + OpenIddictServerEndpointType.Token when context.ValidateRefreshToken => context.Request.RefreshToken, + + _ => null + }; + + context.UserCode = context.EndpointType switch + { + OpenIddictServerEndpointType.Verification when context.ValidateUserCode => context.Request.UserCode, + + _ => null + }; return default; } } /// - /// Contains the logic responsible of validating reference token identifiers. - /// Note: this handler is not used when the degraded mode is enabled. + /// Contains the logic responsible of validating the access token resolved from the context. /// - public class ValidateReferenceTokenIdentifier : IOpenIddictServerHandler + public class ValidateAccessToken : IOpenIddictServerHandler { - private readonly IOpenIddictTokenManager _tokenManager; + private readonly IOpenIddictServerDispatcher _dispatcher; - public ValidateReferenceTokenIdentifier() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); - - public ValidateReferenceTokenIdentifier(IOpenIddictTokenManager tokenManager) - => _tokenManager = tokenManager; + public ValidateAccessToken(IOpenIddictServerDispatcher dispatcher) + => _dispatcher = dispatcher; /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .UseScopedHandler() - .SetOrder(NormalizeUserCode.Descriptor.Order + 1_000) + .AddFilter() + .UseScopedHandler() + .SetOrder(ResolveValidatedTokens.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); + /// public async ValueTask HandleAsync(ProcessAuthenticationContext context) { if (context is null) @@ -312,423 +338,334 @@ namespace OpenIddict.Server throw new ArgumentNullException(nameof(context)); } - // Reference tokens are base64url-encoded payloads of exactly 256 bits, - // except reference user codes, whose length is exactly 12 characters. - // If the token length differs, the token cannot be a reference token. - if (string.IsNullOrEmpty(context.Token) || (context.Token.Length != 12 && context.Token.Length != 43)) + if (context.AccessTokenPrincipal is not null) { return; } - // If the reference token cannot be found, don't return an error to allow another handler to validate it. - var token = await _tokenManager.FindByReferenceIdAsync(context.Token); - if (token is null) + if (string.IsNullOrEmpty(context.AccessToken)) { + if (context.RequireAccessToken) + { + context.Reject( + error: Errors.MissingToken, + description: SR.GetResourceString(SR.ID2000), + uri: SR.FormatID8000(SR.ID2000)); + + return; + } + return; } - // If the type associated with the token entry doesn't match the expected type, return an error. - if (!string.IsNullOrEmpty(context.TokenType) && !await _tokenManager.HasTypeAsync(token, context.TokenType)) + var notification = new ValidateTokenContext(context.Transaction) { - context.Reject( - error: context.EndpointType switch - { - OpenIddictServerEndpointType.Token => Errors.InvalidGrant, - _ => Errors.InvalidToken - }, - description: context.EndpointType switch - { - OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() - => SR.GetResourceString(SR.ID2001), - OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() - => SR.GetResourceString(SR.ID2002), - OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() - => SR.GetResourceString(SR.ID2003), - - _ => SR.GetResourceString(SR.ID2004) - }, - uri: context.EndpointType switch - { - OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() - => SR.FormatID8000(SR.ID2001), - OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() - => SR.FormatID8000(SR.ID2002), - OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() - => SR.FormatID8000(SR.ID2003), + Token = context.AccessToken, + ValidTokenTypes = { TokenTypeHints.AccessToken } + }; - _ => SR.FormatID8000(SR.ID2004), - }); + await _dispatcher.DispatchAsync(notification); + if (notification.IsRequestHandled) + { + context.HandleRequest(); return; } - var payload = await _tokenManager.GetPayloadAsync(token); - if (string.IsNullOrEmpty(payload)) + else if (notification.IsRequestSkipped) { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0026)); + context.SkipRequest(); + return; } - // Replace the token parameter by the payload resolved from the token entry. - context.Token = payload; + else if (notification.IsRejected) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } - // Store the identifier of the reference token in the transaction properties - // so it can be later used to restore the properties associated with the token. - context.Transaction.Properties[Properties.ReferenceTokenIdentifier] = await _tokenManager.GetIdAsync(token); + context.AccessTokenPrincipal = notification.Principal; } } /// - /// Contains the logic responsible of validating tokens generated using IdentityModel. + /// Contains the logic responsible of validating the authorization code resolved from the context. /// - public class ValidateIdentityModelToken : IOpenIddictServerHandler + public class ValidateAuthorizationCode : IOpenIddictServerHandler { + private readonly IOpenIddictServerDispatcher _dispatcher; + + public ValidateAuthorizationCode(IOpenIddictServerDispatcher dispatcher) + => _dispatcher = dispatcher; + /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(ValidateReferenceTokenIdentifier.Descriptor.Order + 1_000) + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateAccessToken.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); /// - public ValueTask HandleAsync(ProcessAuthenticationContext context) + public async ValueTask HandleAsync(ProcessAuthenticationContext context) { if (context is null) { throw new ArgumentNullException(nameof(context)); } - // If a principal was already attached, don't overwrite it. - if (context.Principal is not null) - { - return default; - } - - // If the token cannot be read, don't return an error to allow another handler to validate it. - if (!context.Options.JsonWebTokenHandler.CanReadToken(context.Token)) + if (context.AuthorizationCodePrincipal is not null) { - return default; + return; } - var parameters = context.Options.TokenValidationParameters.Clone(); - parameters.ValidIssuer ??= context.Issuer?.AbsoluteUri; - parameters.ValidateIssuer = !string.IsNullOrEmpty(parameters.ValidIssuer); - parameters.ValidTypes = context.TokenType switch + if (string.IsNullOrEmpty(context.AuthorizationCode)) { - // If no specific token type is expected, accept all token types at this stage. - // Additional filtering can be made based on the resolved/actual token type. - null or { Length: 0 } => null, - - // For access tokens, both "at+jwt" and "application/at+jwt" are valid. - TokenTypeHints.AccessToken => new[] - { - JsonWebTokenTypes.AccessToken, - JsonWebTokenTypes.Prefixes.Application + JsonWebTokenTypes.AccessToken - }, - - // For identity tokens, both "JWT" and "application/jwt" are valid. - TokenTypeHints.IdToken => new[] + if (context.RequireAuthorizationCode) { - JsonWebTokenTypes.IdentityToken, - JsonWebTokenTypes.Prefixes.Application + JsonWebTokenTypes.IdentityToken - }, - - // For authorization codes, only the short "oi_auc+jwt" form is valid. - TokenTypeHints.AuthorizationCode => new[] { JsonWebTokenTypes.Private.AuthorizationCode }, - - // For device codes, only the short "oi_dvc+jwt" form is valid. - TokenTypeHints.DeviceCode => new[] { JsonWebTokenTypes.Private.DeviceCode }, - - // For refresh tokens, only the short "oi_reft+jwt" form is valid. - TokenTypeHints.RefreshToken => new[] { JsonWebTokenTypes.Private.RefreshToken }, + context.Reject( + error: Errors.MissingToken, + description: SR.GetResourceString(SR.ID2000), + uri: SR.FormatID8000(SR.ID2000)); - // For user codes, only the short "oi_usrc+jwt" form is valid. - TokenTypeHints.UserCode => new[] { JsonWebTokenTypes.Private.UserCode }, + return; + } - _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003)) - }; + return; + } - var result = context.Options.JsonWebTokenHandler.ValidateToken(context.Token, parameters); - if (!result.IsValid) + var notification = new ValidateTokenContext(context.Transaction) { - context.Logger.LogTrace(result.Exception, SR.GetResourceString(SR.ID6000), context.Token); - - context.Reject( - error: context.EndpointType switch - { - OpenIddictServerEndpointType.Token => Errors.InvalidGrant, - _ => Errors.InvalidToken - }, - description: result.Exception switch - { - SecurityTokenInvalidTypeException => context.EndpointType switch - { - OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() - => SR.GetResourceString(SR.ID2005), - - OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() - => SR.GetResourceString(SR.ID2006), - - OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() - => SR.GetResourceString(SR.ID2007), - - OpenIddictServerEndpointType.Userinfo => SR.GetResourceString(SR.ID2008), - - _ => SR.GetResourceString(SR.ID2089) - }, - - SecurityTokenInvalidIssuerException => SR.GetResourceString(SR.ID2088), - SecurityTokenSignatureKeyNotFoundException => SR.GetResourceString(SR.ID2090), - SecurityTokenInvalidSignatureException => SR.GetResourceString(SR.ID2091), - - _ => SR.GetResourceString(SR.ID2004) - }, - uri: result.Exception switch - { - SecurityTokenInvalidTypeException => context.EndpointType switch - { - OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() - => SR.FormatID8000(SR.ID2005), - - OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() - => SR.FormatID8000(SR.ID2006), - - OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() - => SR.FormatID8000(SR.ID2007), - - OpenIddictServerEndpointType.Userinfo => SR.FormatID8000(SR.ID2008), - - _ => SR.FormatID8000(SR.ID2089) - }, - - SecurityTokenInvalidIssuerException => SR.FormatID8000(SR.ID2088), - SecurityTokenSignatureKeyNotFoundException => SR.FormatID8000(SR.ID2090), - SecurityTokenInvalidSignatureException => SR.FormatID8000(SR.ID2091), - - _ => SR.FormatID8000(SR.ID2004) - }); + Token = context.AuthorizationCode, + ValidTokenTypes = { TokenTypeHints.AuthorizationCode } + }; - return default; - } + await _dispatcher.DispatchAsync(notification); - // Get the JWT token. If the token is encrypted using JWE, retrieve the inner token. - var token = (JsonWebToken) result.SecurityToken; - if (token.InnerToken is not null) + if (notification.IsRequestHandled) { - token = token.InnerToken; + context.HandleRequest(); + return; } - // Attach the principal extracted from the token to the parent event context. - context.Principal = new ClaimsPrincipal(result.ClaimsIdentity); - - // Store the token type (resolved from "typ" or "token_usage") as a special private claim. - context.Principal.SetTokenType(result.TokenType switch + else if (notification.IsRequestSkipped) { - null or { Length: 0 } => throw new InvalidOperationException(SR.GetResourceString(SR.ID0025)), - - // Both at+jwt and application/at+jwt are supported for access tokens. - JsonWebTokenTypes.AccessToken or JsonWebTokenTypes.Prefixes.Application + JsonWebTokenTypes.AccessToken - => TokenTypeHints.AccessToken, - - // Both JWT and application/JWT are supported for identity tokens. - JsonWebTokenTypes.IdentityToken or JsonWebTokenTypes.Prefixes.Application + JsonWebTokenTypes.IdentityToken - => TokenTypeHints.IdToken, - - JsonWebTokenTypes.Private.AuthorizationCode => TokenTypeHints.AuthorizationCode, - JsonWebTokenTypes.Private.DeviceCode => TokenTypeHints.DeviceCode, - JsonWebTokenTypes.Private.RefreshToken => TokenTypeHints.RefreshToken, - JsonWebTokenTypes.Private.UserCode => TokenTypeHints.UserCode, - - _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003)) - }); + context.SkipRequest(); + return; + } - // Restore the claim destinations from the special oi_cl_dstn claim (represented as a dictionary/JSON object). - if (token.TryGetPayloadValue(Claims.Private.ClaimDestinationsMap, out ImmutableDictionary destinations)) + else if (notification.IsRejected) { - context.Principal.SetDestinations(destinations); + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; } - context.Logger.LogTrace(SR.GetResourceString(SR.ID6001), context.Token, context.Principal.Claims); - - return default; + context.AuthorizationCodePrincipal = notification.Principal; } } /// - /// Contains the logic responsible of normalizing the scope claims stored in the tokens. + /// Contains the logic responsible of validating the device code resolved from the context. /// - public class NormalizeScopeClaims : IOpenIddictServerHandler + public class ValidateDeviceCode : IOpenIddictServerHandler { + private readonly IOpenIddictServerDispatcher _dispatcher; + + public ValidateDeviceCode(IOpenIddictServerDispatcher dispatcher) + => _dispatcher = dispatcher; + /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(ValidateIdentityModelToken.Descriptor.Order + 1_000) + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateAuthorizationCode.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); /// - public ValueTask HandleAsync(ProcessAuthenticationContext context) + public async ValueTask HandleAsync(ProcessAuthenticationContext context) { if (context is null) { throw new ArgumentNullException(nameof(context)); } - if (context.Principal is null) + if (context.DeviceCodePrincipal is not null) { - return default; + return; } - // Note: in previous OpenIddict versions, scopes were represented as a JSON array - // and deserialized as multiple claims. In OpenIddict 3.0, the public "scope" claim - // is formatted as a unique space-separated string containing all the granted scopes. - // To ensure access tokens generated by previous versions are still correctly handled, - // both formats (unique space-separated string or multiple scope claims) must be supported. - // To achieve that, all the "scope" claims are combined into a single one containg all the values. - // Visit https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-04 for more information. - var scopes = context.Principal.GetClaims(Claims.Scope); - if (scopes.Length > 1) + if (string.IsNullOrEmpty(context.DeviceCode)) { - context.Principal.SetClaim(Claims.Scope, string.Join(" ", scopes)); + if (context.RequireDeviceCode) + { + context.Reject( + error: Errors.MissingToken, + description: SR.GetResourceString(SR.ID2000), + uri: SR.FormatID8000(SR.ID2000)); + + return; + } + + return; } - return default; + var notification = new ValidateTokenContext(context.Transaction) + { + Token = context.DeviceCode, + ValidTokenTypes = { TokenTypeHints.DeviceCode } + }; + + await _dispatcher.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.DeviceCodePrincipal = notification.Principal; } } /// - /// Contains the logic responsible of mapping internal claims used by OpenIddict. + /// Contains the logic responsible of validating tokens of unknown types resolved from the context. /// - public class MapInternalClaims : IOpenIddictServerHandler + public class ValidateGenericToken : IOpenIddictServerHandler { + private readonly IOpenIddictServerDispatcher _dispatcher; + + public ValidateGenericToken(IOpenIddictServerDispatcher dispatcher) + => _dispatcher = dispatcher; + /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(NormalizeScopeClaims.Descriptor.Order + 1_000) + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateDeviceCode.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); /// - public ValueTask HandleAsync(ProcessAuthenticationContext context) + public async ValueTask HandleAsync(ProcessAuthenticationContext context) { if (context is null) { throw new ArgumentNullException(nameof(context)); } - if (context.Principal is null) + if (context.GenericTokenPrincipal is not null) { - return default; + return; } - // To reduce the size of tokens, some of the private claims used by OpenIddict - // are mapped to their standard equivalent before being removed from the token. - // This handler is responsible of adding back the private claims to the principal - // when receiving the token (e.g "oi_prst" is resolved from the "scope" claim). - - // In OpenIddict 3.0, the creation date of a token is stored in "oi_crt_dt". - // If the claim doesn't exist, try to infer it from the standard "iat" JWT claim. - if (!context.Principal.HasClaim(Claims.Private.CreationDate)) + if (string.IsNullOrEmpty(context.GenericToken)) { - var date = context.Principal.GetClaim(Claims.IssuedAt); - if (!string.IsNullOrEmpty(date) && - long.TryParse(date, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) + if (context.RequireGenericToken) { - context.Principal.SetCreationDate(DateTimeOffset.FromUnixTimeSeconds(value)); + context.Reject( + error: Errors.MissingToken, + description: SR.GetResourceString(SR.ID2000), + uri: SR.FormatID8000(SR.ID2000)); + + return; } + + return; } - // In OpenIddict 3.0, the expiration date of a token is stored in "oi_exp_dt". - // If the claim doesn't exist, try to infer it from the standard "exp" JWT claim. - if (!context.Principal.HasClaim(Claims.Private.ExpirationDate)) + var notification = new ValidateTokenContext(context.Transaction) { - var date = context.Principal.GetClaim(Claims.ExpiresAt); - if (!string.IsNullOrEmpty(date) && - long.TryParse(date, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) - { - context.Principal.SetExpirationDate(DateTimeOffset.FromUnixTimeSeconds(value)); - } - } + Token = context.GenericToken, + TokenTypeHint = context.GenericTokenTypeHint, + + // By default, only access tokens and refresh tokens can be introspected/revoked but + // tokens received by the introspection and revocation endpoints can be of any type. + // + // Additional token type filtering is made by the endpoint themselves, if needed. + // As such, the valid token types list is deliberately left empty in this case. + ValidTokenTypes = { } + }; - // In OpenIddict 3.0, the audiences allowed to receive a token are stored in "oi_aud". - // If no such claim exists, try to infer them from the standard "aud" JWT claims. - if (!context.Principal.HasClaim(Claims.Private.Audience)) + await _dispatcher.DispatchAsync(notification); + + if (notification.IsRequestHandled) { - var audiences = context.Principal.GetClaims(Claims.Audience); - if (audiences.Any()) - { - context.Principal.SetAudiences(audiences); - } + context.HandleRequest(); + return; } - // In OpenIddict 3.0, the presenters allowed to use a token are stored in "oi_prst". - // If no such claim exists, try to infer them from the standard "azp" and "client_id" JWT claims. - // - // Note: in previous OpenIddict versions, the presenters were represented in JWT tokens - // using the "azp" claim (defined by OpenID Connect), for which a single value could be - // specified. To ensure presenters stored in JWT tokens created by OpenIddict 1.x/2.x - // can still be read with OpenIddict 3.0, the presenter is automatically inferred from - // the "azp" or "client_id" claim if no "oi_prst" claim was found in the principal. - if (!context.Principal.HasClaim(Claims.Private.Presenter)) + else if (notification.IsRequestSkipped) { - var presenter = context.Principal.GetClaim(Claims.AuthorizedParty) ?? - context.Principal.GetClaim(Claims.ClientId); - - if (!string.IsNullOrEmpty(presenter)) - { - context.Principal.SetPresenters(presenter); - } + context.SkipRequest(); + return; } - // In OpenIddict 3.0, the scopes granted to an application are stored in "oi_scp". - // If no such claim exists, try to infer them from the standard "scope" JWT claim, - // which is guaranteed to be a unique space-separated claim containing all the values. - if (!context.Principal.HasClaim(Claims.Private.Scope)) + else if (notification.IsRejected) { - var scope = context.Principal.GetClaim(Claims.Scope); - if (!string.IsNullOrEmpty(scope)) - { - context.Principal.SetScopes(scope.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries)); - } + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; } - return default; + context.GenericTokenPrincipal = notification.Principal; } } /// - /// Contains the logic responsible of restoring the properties associated with a reference token entry. - /// Note: this handler is not used when the degraded mode is enabled. + /// Contains the logic responsible of validating the identity token resolved from the context. /// - public class RestoreReferenceTokenProperties : IOpenIddictServerHandler + public class ValidateIdentityToken : IOpenIddictServerHandler { - private readonly IOpenIddictTokenManager _tokenManager; - - public RestoreReferenceTokenProperties() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); + private readonly IOpenIddictServerDispatcher _dispatcher; - public RestoreReferenceTokenProperties(IOpenIddictTokenManager tokenManager) - => _tokenManager = tokenManager; + public ValidateIdentityToken(IOpenIddictServerDispatcher dispatcher) + => _dispatcher = dispatcher; /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .UseScopedHandler() - .SetOrder(MapInternalClaims.Descriptor.Order + 1_000) + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateGenericToken.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); + /// public async ValueTask HandleAsync(ProcessAuthenticationContext context) { if (context is null) @@ -736,387 +673,163 @@ namespace OpenIddict.Server throw new ArgumentNullException(nameof(context)); } - if (context.Principal is null) + if (context.IdentityTokenPrincipal is not null) { return; } - var identifier = context.Transaction.GetProperty(Properties.ReferenceTokenIdentifier); - if (string.IsNullOrEmpty(identifier)) + if (string.IsNullOrEmpty(context.IdentityToken)) { + if (context.RequireIdentityToken) + { + context.Reject( + error: Errors.MissingToken, + description: SR.GetResourceString(SR.ID2000), + uri: SR.FormatID8000(SR.ID2000)); + + return; + } + return; } - var token = await _tokenManager.FindByIdAsync(identifier); - if (token is null) + var notification = new ValidateTokenContext(context.Transaction) + { + Token = context.IdentityToken, + ValidTokenTypes = { TokenTypeHints.IdToken } + }; + + await _dispatcher.DispatchAsync(notification); + + if (notification.IsRequestHandled) { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0021)); + 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; } - // Restore the creation/expiration dates/identifiers from the token entry metadata. - context.Principal = context.Principal - .SetCreationDate(await _tokenManager.GetCreationDateAsync(token)) - .SetExpirationDate(await _tokenManager.GetExpirationDateAsync(token)) - .SetAuthorizationId(await _tokenManager.GetAuthorizationIdAsync(token)) - .SetTokenId(await _tokenManager.GetIdAsync(token)) - .SetTokenType(await _tokenManager.GetTypeAsync(token)); + context.IdentityTokenPrincipal = notification.Principal; } } /// - /// Contains the logic responsible of rejecting authentication demands for which no valid principal was resolved. + /// Contains the logic responsible of validating the refresh token resolved from the context. /// - public class ValidatePrincipal : IOpenIddictServerHandler + public class ValidateRefreshToken : IOpenIddictServerHandler { + private readonly IOpenIddictServerDispatcher _dispatcher; + + public ValidateRefreshToken(IOpenIddictServerDispatcher dispatcher) + => _dispatcher = dispatcher; + /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(RestoreReferenceTokenProperties.Descriptor.Order + 1_000) + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateIdentityToken.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); /// - public ValueTask HandleAsync(ProcessAuthenticationContext context) + public async ValueTask HandleAsync(ProcessAuthenticationContext context) { if (context is null) { throw new ArgumentNullException(nameof(context)); } - if (context.Principal is null) + if (context.RefreshTokenPrincipal is not null) { - context.Reject( - error: context.EndpointType switch - { - OpenIddictServerEndpointType.Token => Errors.InvalidGrant, - _ => Errors.InvalidToken - }, - description: context.EndpointType switch - { - OpenIddictServerEndpointType.Authorization or OpenIddictServerEndpointType.Logout - => SR.GetResourceString(SR.ID2009), - - OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() - => SR.GetResourceString(SR.ID2001), - OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() - => SR.GetResourceString(SR.ID2002), - OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() - => SR.GetResourceString(SR.ID2003), - - _ => SR.GetResourceString(SR.ID2004) - }, - uri: context.EndpointType switch - { - OpenIddictServerEndpointType.Authorization or OpenIddictServerEndpointType.Logout - => SR.FormatID8000(SR.ID2009), - - OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() - => SR.FormatID8000(SR.ID2001), - OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() - => SR.FormatID8000(SR.ID2002), - OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() - => SR.FormatID8000(SR.ID2003), - - _ => SR.FormatID8000(SR.ID2004) - }); - - - return default; + return; } - // When using JWT or Data Protection tokens, the correct token type is always enforced by IdentityModel - // (using the "typ" header) or by ASP.NET Core Data Protection (using per-token-type purposes strings). - // To ensure tokens deserialized using a custom routine are of the expected type, a manual check is used, - // which requires that a special claim containing the token type be present in the security principal. - if (!string.IsNullOrEmpty(context.TokenType)) + if (string.IsNullOrEmpty(context.RefreshToken)) { - var type = context.Principal.GetTokenType(); - if (string.IsNullOrEmpty(type)) + if (context.RequireRefreshToken) { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0004)); - } + context.Reject( + error: Errors.MissingToken, + description: SR.GetResourceString(SR.ID2000), + uri: SR.FormatID8000(SR.ID2000)); - if (!string.Equals(type, context.TokenType, StringComparison.OrdinalIgnoreCase)) - { - throw new InvalidOperationException(SR.FormatID0005(type, context.TokenType)); + return; } - } - - return default; - } - } - - /// - /// Contains the logic responsible of rejecting authentication demands that - /// use a token whose entry is no longer valid (e.g was revoked). - /// Note: this handler is not used when the degraded mode is enabled. - /// - public class ValidateTokenEntry : IOpenIddictServerHandler - { - private readonly IOpenIddictTokenManager _tokenManager; - - public ValidateTokenEntry() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); - - public ValidateTokenEntry(IOpenIddictTokenManager tokenManager) - => _tokenManager = tokenManager; - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .UseScopedHandler() - .SetOrder(ValidatePrincipal.Descriptor.Order + 1_000) - .SetType(OpenIddictServerHandlerType.BuiltIn) - .Build(); - - public async ValueTask HandleAsync(ProcessAuthenticationContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); - // Extract the token identifier from the authentication principal. - // If no token identifier can be found, this indicates that the token - // has no backing database entry (e.g an access token or an identity token). - var identifier = context.Principal.GetTokenId(); - if (string.IsNullOrEmpty(identifier)) - { return; } - // If the token entry cannot be found, return a generic error. - var token = await _tokenManager.FindByIdAsync(identifier); - if (token is null) + var notification = new ValidateTokenContext(context.Transaction) { - context.Reject( - error: context.EndpointType switch - { - OpenIddictServerEndpointType.Token => Errors.InvalidGrant, - _ => Errors.InvalidToken - }, - description: context.EndpointType switch - { - OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() - => SR.GetResourceString(SR.ID2001), - OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() - => SR.GetResourceString(SR.ID2002), - OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() - => SR.GetResourceString(SR.ID2003), - - _ => SR.GetResourceString(SR.ID2004) - }, - uri: context.EndpointType switch - { - OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() - => SR.FormatID8000(SR.ID2001), - OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() - => SR.FormatID8000(SR.ID2002), - OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() - => SR.FormatID8000(SR.ID2003), + Token = context.RefreshToken, + ValidTokenTypes = { TokenTypeHints.RefreshToken } + }; - _ => SR.FormatID8000(SR.ID2004) - }); + await _dispatcher.DispatchAsync(notification); + if (notification.IsRequestHandled) + { + context.HandleRequest(); return; } - if (context.EndpointType == OpenIddictServerEndpointType.Token && (context.Request.IsAuthorizationCodeGrantType() || - context.Request.IsDeviceCodeGrantType() || - context.Request.IsRefreshTokenGrantType())) + else if (notification.IsRequestSkipped) { - // If the authorization code/device code/refresh token is already marked as redeemed, this may indicate - // that it was compromised. In this case, revoke the entire chain of tokens associated with the authorization. - // Special logic is used to avoid revoking refresh tokens already marked as redeemed to allow for a small leeway. - // Note: the authorization itself is not revoked to allow the legitimate client to start a new flow. - // See https://tools.ietf.org/html/rfc6749#section-10.5 for more information. - if (await _tokenManager.HasStatusAsync(token, Statuses.Redeemed)) - { - if (!context.Request.IsRefreshTokenGrantType() || !await IsReusableAsync(token)) - { - context.Logger.LogInformation(SR.GetResourceString(SR.ID6002), identifier); - - context.Reject( - error: context.EndpointType switch - { - OpenIddictServerEndpointType.Token => Errors.InvalidGrant, - - _ => Errors.InvalidToken - }, - description: context.EndpointType switch - { - OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() - => SR.GetResourceString(SR.ID2010), - OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() - => SR.GetResourceString(SR.ID2011), - OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() - => SR.GetResourceString(SR.ID2012), - - _ => SR.GetResourceString(SR.ID2013) - }, - uri: context.EndpointType switch - { - OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() - => SR.FormatID8000(SR.ID2010), - OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() - => SR.FormatID8000(SR.ID2011), - OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() - => SR.FormatID8000(SR.ID2012), - - _ => SR.FormatID8000(SR.ID2013) - }); - - // Revoke all the token entries associated with the authorization. - await TryRevokeChainAsync(await _tokenManager.GetAuthorizationIdAsync(token)); - - return; - } - - return; - } - - if (context.Request.IsDeviceCodeGrantType()) - { - // If the device code is not marked as valid yet, return an authorization_pending error. - if (await _tokenManager.HasStatusAsync(token, Statuses.Inactive)) - { - context.Logger.LogInformation(SR.GetResourceString(SR.ID6003), identifier); - - context.Reject( - error: Errors.AuthorizationPending, - description: SR.GetResourceString(SR.ID2014), - uri: SR.FormatID8000(SR.ID2014)); - - return; - } - - // If the device code is marked as rejected, return an access_denied error. - if (await _tokenManager.HasStatusAsync(token, Statuses.Rejected)) - { - context.Logger.LogInformation(SR.GetResourceString(SR.ID6004), identifier); - - context.Reject( - error: Errors.AccessDenied, - description: SR.GetResourceString(SR.ID2015), - uri: SR.FormatID8000(SR.ID2015)); - - return; - } - } + context.SkipRequest(); + return; } - if (!await _tokenManager.HasStatusAsync(token, Statuses.Valid)) + else if (notification.IsRejected) { - context.Logger.LogInformation(SR.GetResourceString(SR.ID6005), identifier); - context.Reject( - error: context.EndpointType switch - { - OpenIddictServerEndpointType.Token => Errors.InvalidGrant, - _ => Errors.InvalidToken - }, - description: context.EndpointType switch - { - OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() - => SR.GetResourceString(SR.ID2016), - OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() - => SR.GetResourceString(SR.ID2017), - OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() - => SR.GetResourceString(SR.ID2018), - - _ => SR.GetResourceString(SR.ID2019) - }, - uri: context.EndpointType switch - { - OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() - => SR.FormatID8000(SR.ID2016), - OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() - => SR.FormatID8000(SR.ID2017), - OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() - => SR.FormatID8000(SR.ID2018), - - _ => SR.FormatID8000(SR.ID2019) - }); - + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); return; } - // Restore the creation/expiration dates/identifiers from the token entry metadata. - context.Principal.SetCreationDate(await _tokenManager.GetCreationDateAsync(token)) - .SetExpirationDate(await _tokenManager.GetExpirationDateAsync(token)) - .SetAuthorizationId(await _tokenManager.GetAuthorizationIdAsync(token)) - .SetTokenId(await _tokenManager.GetIdAsync(token)) - .SetTokenType(await _tokenManager.GetTypeAsync(token)); - - async ValueTask IsReusableAsync(object token) - { - // If the reuse leeway was set to null, return false to indicate - // that the refresh token is already redeemed and cannot be reused. - if (context.Options.RefreshTokenReuseLeeway is null) - { - return false; - } - - var date = await _tokenManager.GetRedemptionDateAsync(token); - if (date is null || DateTimeOffset.UtcNow < date + context.Options.RefreshTokenReuseLeeway) - { - return true; - } - - return false; - } - - async ValueTask TryRevokeChainAsync(string? identifier) - { - if (string.IsNullOrEmpty(identifier)) - { - return; - } - - // Revoke all the token entries associated with the authorization, - // including the redeemed token that was used in the token request. - await foreach (var token in _tokenManager.FindByAuthorizationIdAsync(identifier)) - { - await _tokenManager.TryRevokeAsync(token); - } - } + context.RefreshTokenPrincipal = notification.Principal; } } /// - /// Contains the logic responsible of authentication demands a token whose - /// associated authorization entry is no longer valid (e.g was revoked). - /// Note: this handler is not used when the degraded mode is enabled. + /// Contains the logic responsible of validating the user code resolved from the context. /// - public class ValidateAuthorizationEntry : IOpenIddictServerHandler + public class ValidateUserCode : IOpenIddictServerHandler { - private readonly IOpenIddictAuthorizationManager _authorizationManager; + private readonly IOpenIddictServerDispatcher _dispatcher; - public ValidateAuthorizationEntry() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); - - public ValidateAuthorizationEntry(IOpenIddictAuthorizationManager authorizationManager) - => _authorizationManager = authorizationManager; + public ValidateUserCode(IOpenIddictServerDispatcher dispatcher) + => _dispatcher = dispatcher; /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .UseScopedHandler() - .SetOrder(ValidateTokenEntry.Descriptor.Order + 1_000) + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateRefreshToken.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); + /// public async ValueTask HandleAsync(ProcessAuthenticationContext context) { if (context is null) @@ -1124,124 +837,56 @@ namespace OpenIddict.Server throw new ArgumentNullException(nameof(context)); } - Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); - - var identifier = context.Principal.GetAuthorizationId(); - if (string.IsNullOrEmpty(identifier)) + if (context.UserCodePrincipal is not null) { return; } - var authorization = await _authorizationManager.FindByIdAsync(identifier); - if (authorization is null || !await _authorizationManager.HasStatusAsync(authorization, Statuses.Valid)) + if (string.IsNullOrEmpty(context.UserCode)) { - context.Logger.LogInformation(SR.GetResourceString(SR.ID6006), identifier); - - context.Reject( - error: context.EndpointType switch - { - OpenIddictServerEndpointType.Token => Errors.InvalidGrant, - _ => Errors.InvalidToken - }, - description: context.EndpointType switch - { - OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() - => SR.GetResourceString(SR.ID2020), - OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() - => SR.GetResourceString(SR.ID2021), - OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() - => SR.GetResourceString(SR.ID2022), - - _ => SR.GetResourceString(SR.ID2023) - }, - uri: context.EndpointType switch - { - OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() - => SR.FormatID8000(SR.ID2020), - OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() - => SR.FormatID8000(SR.ID2021), - OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() - => SR.FormatID8000(SR.ID2022), + if (context.RequireUserCode) + { + context.Reject( + error: Errors.MissingToken, + description: SR.GetResourceString(SR.ID2000), + uri: SR.FormatID8000(SR.ID2000)); - _ => SR.FormatID8000(SR.ID2023) - }); + return; + } return; } - } - } - /// - /// Contains the logic responsible of rejecting authentication demands that use an expired token. - /// - public class ValidateExpirationDate : IOpenIddictServerHandler - { - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(ValidateTokenEntry.Descriptor.Order + 1_000) - .SetType(OpenIddictServerHandlerType.BuiltIn) - .Build(); + var notification = new ValidateTokenContext(context.Transaction) + { + Token = context.UserCode, + ValidTokenTypes = { TokenTypeHints.UserCode } + }; - /// - public ValueTask HandleAsync(ProcessAuthenticationContext context) - { - if (context is null) + await _dispatcher.DispatchAsync(notification); + + if (notification.IsRequestHandled) { - throw new ArgumentNullException(nameof(context)); + context.HandleRequest(); + return; } - Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); - - // Don't validate the lifetime of id_tokens used as id_token_hints. - if (context.EndpointType is OpenIddictServerEndpointType.Authorization or OpenIddictServerEndpointType.Logout) + else if (notification.IsRequestSkipped) { - return default; + context.SkipRequest(); + return; } - var date = context.Principal.GetExpirationDate(); - if (date.HasValue && date.Value < DateTimeOffset.UtcNow) + else if (notification.IsRejected) { context.Reject( - error: context.EndpointType switch - { - OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() - => Errors.ExpiredToken, - - OpenIddictServerEndpointType.Token => Errors.InvalidGrant, - - _ => Errors.InvalidToken - }, - description: context.EndpointType switch - { - OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() - => SR.GetResourceString(SR.ID2016), - OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() - => SR.GetResourceString(SR.ID2017), - OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() - => SR.GetResourceString(SR.ID2018), - - _ => SR.GetResourceString(SR.ID2019) - }, - uri: context.EndpointType switch - { - OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() - => SR.FormatID8000(SR.ID2016), - OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() - => SR.FormatID8000(SR.ID2017), - OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() - => SR.FormatID8000(SR.ID2018), - - _ => SR.FormatID8000(SR.ID2019) - }); - - return default; + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; } - return default; + context.UserCodePrincipal = notification.Principal; } } @@ -1382,10 +1027,10 @@ namespace OpenIddict.Server typeof(ProcessAuthenticationContext).FullName!) ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0007)); - Debug.Assert(notification.Principal is not null, SR.GetResourceString(SR.ID4006)); + Debug.Assert(notification.UserCodePrincipal is not null, SR.GetResourceString(SR.ID4006)); // Extract the device code identifier from the user code principal. - var identifier = notification.Principal.GetClaim(Claims.Private.DeviceCodeId); + var identifier = notification.UserCodePrincipal.GetClaim(Claims.Private.DeviceCodeId); if (string.IsNullOrEmpty(identifier)) { throw new InvalidOperationException(SR.GetResourceString(SR.ID0008)); @@ -1441,10 +1086,10 @@ namespace OpenIddict.Server typeof(ProcessAuthenticationContext).FullName!) ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0007)); - Debug.Assert(notification.Principal is not null, SR.GetResourceString(SR.ID4006)); + Debug.Assert(notification.UserCodePrincipal is not null, SR.GetResourceString(SR.ID4006)); // Extract the device code identifier from the authentication principal. - var identifier = notification.Principal.GetTokenId(); + var identifier = notification.UserCodePrincipal.GetTokenId(); if (string.IsNullOrEmpty(identifier)) { throw new InvalidOperationException(SR.GetResourceString(SR.ID0009)); @@ -1570,13 +1215,29 @@ namespace OpenIddict.Server typeof(ProcessAuthenticationContext).FullName!) ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0007)); - if (notification.Principal is null) + var principal = context.EndpointType switch + { + OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() + => notification.AuthorizationCodePrincipal, + + OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() + => notification.DeviceCodePrincipal, + + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() + => notification.RefreshTokenPrincipal, + + OpenIddictServerEndpointType.Verification => notification.UserCodePrincipal, + + _ => null + }; + + if (principal is null) { return default; } - // Restore the internal claims resolved from the authorization code/refresh token. - foreach (var claims in notification.Principal.Claims + // Restore the internal claims resolved from the token. + foreach (var claims in principal.Claims .Where(claim => claim.Type.StartsWith(Claims.Prefixes.Private, StringComparison.OrdinalIgnoreCase)) .GroupBy(claim => claim.Type)) { @@ -1716,14 +1377,14 @@ namespace OpenIddict.Server /// Contains the logic responsible of selecting the token types that /// should be generated and optionally returned in the response. /// - public class EvaluateTokenTypes : IOpenIddictServerHandler + public class EvaluateGeneratedTokens : IOpenIddictServerHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() + .UseSingletonHandler() .SetOrder(InferResources.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -1838,7 +1499,7 @@ namespace OpenIddict.Server .AddFilter() .AddFilter() .UseScopedHandler() - .SetOrder(EvaluateTokenTypes.Descriptor.Order + 1_000) + .SetOrder(EvaluateGeneratedTokens.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -1994,7 +1655,7 @@ namespace OpenIddict.Server // Remove the destinations from the claim properties. foreach (var claim in principal.Claims) { - claim.Properties.Remove(OpenIddictConstants.Properties.Destinations); + claim.Properties.Remove(Properties.Destinations); } principal.SetCreationDate(DateTimeOffset.UtcNow); @@ -2258,9 +1919,9 @@ namespace OpenIddict.Server typeof(ProcessAuthenticationContext).FullName!) ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0007)); - Debug.Assert(notification.Principal is not null, SR.GetResourceString(SR.ID4006)); + Debug.Assert(notification.RefreshTokenPrincipal is not null, SR.GetResourceString(SR.ID4006)); - principal.SetExpirationDate(notification.Principal.GetExpirationDate()); + principal.SetExpirationDate(notification.RefreshTokenPrincipal.GetExpirationDate()); } else @@ -2354,7 +2015,7 @@ namespace OpenIddict.Server // Remove the destinations from the claim properties. foreach (var claim in principal.Claims) { - claim.Properties.Remove(OpenIddictConstants.Properties.Destinations); + claim.Properties.Remove(Properties.Destinations); } principal.SetCreationDate(DateTimeOffset.UtcNow); @@ -2380,7 +2041,7 @@ namespace OpenIddict.Server principal.SetClaim(Claims.Nonce, context.EndpointType switch { OpenIddictServerEndpointType.Authorization => context.Request.Nonce, - OpenIddictServerEndpointType.Token => context.Principal.GetClaim(Claims.Private.Nonce), + OpenIddictServerEndpointType.Token => context.Principal.GetClaim(Claims.Private.Nonce), _ => null }); @@ -2527,33 +2188,22 @@ namespace OpenIddict.Server } /// - /// Contains the logic responsible of creating an access token entry. - /// Note: this handler is not used when the degraded mode is enabled. + /// Contains the logic responsible of generating an access token for the current sign-in operation. /// - public class CreateAccessTokenEntry : IOpenIddictServerHandler + public class GenerateAccessToken : IOpenIddictServerHandler { - private readonly IOpenIddictApplicationManager _applicationManager; - private readonly IOpenIddictTokenManager _tokenManager; + private readonly IOpenIddictServerDispatcher _dispatcher; - public CreateAccessTokenEntry() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); - - public CreateAccessTokenEntry( - IOpenIddictApplicationManager applicationManager, - IOpenIddictTokenManager tokenManager) - { - _applicationManager = applicationManager; - _tokenManager = tokenManager; - } + public GenerateAccessToken(IOpenIddictServerDispatcher dispatcher) + => _dispatcher = dispatcher; /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() .AddFilter() - .UseScopedHandler() + .UseScopedHandler() .SetOrder(RedeemTokenEntry.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -2563,989 +2213,60 @@ namespace OpenIddict.Server { if (context is null) { - throw new ArgumentNullException(nameof(context)); - } - - var principal = context.AccessTokenPrincipal; - if (principal is null) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0022)); - } - - var descriptor = new OpenIddictTokenDescriptor - { - AuthorizationId = principal.GetAuthorizationId(), - CreationDate = principal.GetCreationDate(), - ExpirationDate = principal.GetExpirationDate(), - Principal = principal, - Status = Statuses.Valid, - Subject = principal.GetClaim(Claims.Subject), - Type = TokenTypeHints.AccessToken - }; - - // If the client application is known, associate it with the token. - if (!string.IsNullOrEmpty(context.Request.ClientId)) - { - var application = await _applicationManager.FindByClientIdAsync(context.Request.ClientId); - if (application is null) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0017)); - } - - descriptor.ApplicationId = await _applicationManager.GetIdAsync(application); - } - - var token = await _tokenManager.CreateAsync(descriptor); - if (token is null) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0019)); - } - - var identifier = await _tokenManager.GetIdAsync(token); - - // Attach the token identifier to the principal so that it can be stored in the token. - principal.SetTokenId(identifier); - - context.Logger.LogTrace(SR.GetResourceString(SR.ID6012), identifier); - } - } - - /// - /// Contains the logic responsible of generating an access token using IdentityModel. - /// - public class GenerateIdentityModelAccessToken : IOpenIddictServerHandler - { - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .UseSingletonHandler() - .SetOrder(CreateAccessTokenEntry.Descriptor.Order + 1_000) - .SetType(OpenIddictServerHandlerType.BuiltIn) - .Build(); - - /// - public ValueTask HandleAsync(ProcessSignInContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - // If an access token was already attached by another handler, don't overwrite it. - if (!string.IsNullOrEmpty(context.AccessToken)) - { - return default; - } - - if (context.AccessTokenPrincipal is null) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0022)); - } - - // Clone the principal and exclude the private claims mapped to standard JWT claims. - var principal = context.AccessTokenPrincipal.Clone(claim => claim.Type is not ( - Claims.Private.Audience or - Claims.Private.CreationDate or - Claims.Private.ExpirationDate or - Claims.Private.Scope or - Claims.Private.TokenType)); - - if (principal is null or { Identity: not ClaimsIdentity }) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0020)); - } - - var claims = new Dictionary(StringComparer.Ordinal); - - // Set the public audience claims using the private audience claims from the principal. - // Note: when there's a single audience, represent it as a unique string claim. - var audiences = context.AccessTokenPrincipal.GetAudiences(); - if (audiences.Any()) - { - claims.Add(Claims.Audience, audiences.Length switch - { - 1 => audiences.ElementAt(0), - _ => audiences - }); - } - - // Set the public scope claim using the private scope claims from the principal. - // Note: scopes are deliberately formatted as a single space-separated - // string to respect the usual representation of the standard scope claim. - // See https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-04. - var scopes = context.AccessTokenPrincipal.GetScopes(); - if (scopes.Any()) - { - claims.Add(Claims.Scope, string.Join(" ", scopes)); - } - - var descriptor = new SecurityTokenDescriptor - { - Claims = claims, - // Note: unlike other tokens, encryption can be disabled for access tokens. - EncryptingCredentials = !context.Options.DisableAccessTokenEncryption ? - context.Options.EncryptionCredentials.First() : null, - Expires = context.AccessTokenPrincipal.GetExpirationDate()?.UtcDateTime, - IssuedAt = context.AccessTokenPrincipal.GetCreationDate()?.UtcDateTime, - Issuer = context.Issuer?.AbsoluteUri, - SigningCredentials = context.Options.SigningCredentials.First(), - Subject = (ClaimsIdentity) principal.Identity, - TokenType = JsonWebTokenTypes.AccessToken - }; - - context.AccessToken = context.Options.JsonWebTokenHandler.CreateToken(descriptor); - - context.Logger.LogTrace(SR.GetResourceString(SR.ID6013), context.AccessToken, principal.Claims); - - return default; - } - } - - /// - /// Contains the logic responsible of converting the access token to a reference token. - /// Note: this handler is not used when the degraded mode is enabled. - /// - public class ConvertReferenceAccessToken : IOpenIddictServerHandler - { - private readonly IOpenIddictTokenManager _tokenManager; - - public ConvertReferenceAccessToken() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); - - public ConvertReferenceAccessToken(IOpenIddictTokenManager tokenManager) - => _tokenManager = tokenManager; - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .AddFilter() - .AddFilter() - .UseScopedHandler() - .SetOrder(GenerateIdentityModelAccessToken.Descriptor.Order + 1_000) - .SetType(OpenIddictServerHandlerType.BuiltIn) - .Build(); - - /// - public async ValueTask HandleAsync(ProcessSignInContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (string.IsNullOrEmpty(context.AccessToken)) - { - return; - } - - var principal = context.AccessTokenPrincipal; - if (principal is null) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0020)); - } - - var identifier = principal.GetTokenId(); - if (string.IsNullOrEmpty(identifier)) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0009)); - } - - var token = await _tokenManager.FindByIdAsync(identifier); - if (token is null) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0021)); - } - - // Generate a new crypto-secure random identifier that will be substituted to the token. - var data = new byte[256 / 8]; -#if SUPPORTS_STATIC_RANDOM_NUMBER_GENERATOR_METHODS - RandomNumberGenerator.Fill(data); -#else - using var generator = RandomNumberGenerator.Create(); - generator.GetBytes(data); -#endif - var descriptor = new OpenIddictTokenDescriptor(); - await _tokenManager.PopulateAsync(descriptor, token); - - // Attach the generated token to the token entry, persist the change - // and replace the returned token by the reference identifier. - descriptor.Payload = context.AccessToken; - descriptor.Principal = principal; - descriptor.ReferenceId = Base64UrlEncoder.Encode(data); - - await _tokenManager.UpdateAsync(token, descriptor); - - context.AccessToken = descriptor.ReferenceId; - - context.Logger.LogTrace(SR.GetResourceString(SR.ID6014), identifier, descriptor.ReferenceId); - } - } - - /// - /// Contains the logic responsible of creating an authorization code entry. - /// Note: this handler is not used when the degraded mode is enabled. - /// - public class CreateAuthorizationCodeEntry : IOpenIddictServerHandler - { - private readonly IOpenIddictApplicationManager _applicationManager; - private readonly IOpenIddictTokenManager _tokenManager; - - public CreateAuthorizationCodeEntry() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); - - public CreateAuthorizationCodeEntry( - IOpenIddictApplicationManager applicationManager, - IOpenIddictTokenManager tokenManager) - { - _applicationManager = applicationManager; - _tokenManager = tokenManager; - } - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .AddFilter() - .UseScopedHandler() - .SetOrder(ConvertReferenceAccessToken.Descriptor.Order + 1_000) - .SetType(OpenIddictServerHandlerType.BuiltIn) - .Build(); - - /// - public async ValueTask HandleAsync(ProcessSignInContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - var principal = context.AuthorizationCodePrincipal; - if (principal is null) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0020)); - } - - var descriptor = new OpenIddictTokenDescriptor - { - AuthorizationId = principal.GetAuthorizationId(), - CreationDate = principal.GetCreationDate(), - ExpirationDate = principal.GetExpirationDate(), - Principal = principal, - Status = Statuses.Valid, - Subject = principal.GetClaim(Claims.Subject), - Type = TokenTypeHints.AuthorizationCode - }; - - // If the client application is known, associate it with the token. - if (!string.IsNullOrEmpty(context.Request.ClientId)) - { - var application = await _applicationManager.FindByClientIdAsync(context.Request.ClientId); - if (application is null) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0017)); - } - - descriptor.ApplicationId = await _applicationManager.GetIdAsync(application); - } - - var token = await _tokenManager.CreateAsync(descriptor); - if (token is null) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0019)); - } - - var identifier = await _tokenManager.GetIdAsync(token); - - // Attach the token identifier to the principal so that it can be stored in the token. - principal.SetTokenId(identifier); - - context.Logger.LogTrace(SR.GetResourceString(SR.ID6015), identifier); - } - } - - /// - /// Contains the logic responsible of generating an authorization code using IdentityModel. - /// - public class GenerateIdentityModelAuthorizationCode : IOpenIddictServerHandler - { - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .UseSingletonHandler() - .SetOrder(CreateAuthorizationCodeEntry.Descriptor.Order + 1_000) - .SetType(OpenIddictServerHandlerType.BuiltIn) - .Build(); - - /// - public ValueTask HandleAsync(ProcessSignInContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - // If an authorization code was already attached by another handler, don't overwrite it. - if (!string.IsNullOrEmpty(context.AuthorizationCode)) - { - return default; - } - - if (context.AuthorizationCodePrincipal is null) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0022)); - } - - // Clone the principal and exclude the claim mapped to standard JWT claims. - var principal = context.AuthorizationCodePrincipal.Clone(claim => claim.Type is not ( - Claims.Private.CreationDate or - Claims.Private.ExpirationDate or - Claims.Private.TokenType)); - - if (principal is null or { Identity: not ClaimsIdentity }) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0022)); - } - - var descriptor = new SecurityTokenDescriptor - { - EncryptingCredentials = context.Options.EncryptionCredentials.First(), - Expires = context.AuthorizationCodePrincipal.GetExpirationDate()?.UtcDateTime, - IssuedAt = context.AuthorizationCodePrincipal.GetCreationDate()?.UtcDateTime, - Issuer = context.Issuer?.AbsoluteUri, - SigningCredentials = context.Options.SigningCredentials.First(), - Subject = (ClaimsIdentity) principal.Identity, - TokenType = JsonWebTokenTypes.Private.AuthorizationCode - }; - - // Attach claims destinations to the JWT claims collection. - var destinations = principal.GetDestinations(); - if (destinations.Count != 0) - { - descriptor.Claims = new Dictionary(StringComparer.Ordinal) - { - [Claims.Private.ClaimDestinationsMap] = destinations - }; - } - - context.AuthorizationCode = context.Options.JsonWebTokenHandler.CreateToken(descriptor); - - context.Logger.LogTrace(SR.GetResourceString(SR.ID6016), context.AuthorizationCode, principal.Claims); - - return default; - } - } - - /// - /// Contains the logic responsible of converting the authorization code to a reference token. - /// Note: this handler is not used when the degraded mode is enabled. - /// - public class ConvertReferenceAuthorizationCode : IOpenIddictServerHandler - { - private readonly IOpenIddictTokenManager _tokenManager; - - public ConvertReferenceAuthorizationCode() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); - - public ConvertReferenceAuthorizationCode(IOpenIddictTokenManager tokenManager) - => _tokenManager = tokenManager; - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .AddFilter() - .UseScopedHandler() - .SetOrder(GenerateIdentityModelAuthorizationCode.Descriptor.Order + 1_000) - .SetType(OpenIddictServerHandlerType.BuiltIn) - .Build(); - - /// - public async ValueTask HandleAsync(ProcessSignInContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (string.IsNullOrEmpty(context.AuthorizationCode)) - { - return; - } - - var principal = context.AuthorizationCodePrincipal; - if (principal is null) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0020)); - } - - var identifier = principal.GetTokenId(); - if (string.IsNullOrEmpty(identifier)) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0009)); - } - - var token = await _tokenManager.FindByIdAsync(identifier); - if (token is null) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0021)); - } - - // Generate a new crypto-secure random identifier that will be substituted to the token. - var data = new byte[256 / 8]; -#if SUPPORTS_STATIC_RANDOM_NUMBER_GENERATOR_METHODS - RandomNumberGenerator.Fill(data); -#else - using var generator = RandomNumberGenerator.Create(); - generator.GetBytes(data); -#endif - var descriptor = new OpenIddictTokenDescriptor(); - await _tokenManager.PopulateAsync(descriptor, token); - - // Attach the generated token to the token entry, persist the change - // and replace the returned token by the reference identifier. - descriptor.Payload = context.AuthorizationCode; - descriptor.Principal = principal; - descriptor.ReferenceId = Base64UrlEncoder.Encode(data); - - await _tokenManager.UpdateAsync(token, descriptor); - - context.AuthorizationCode = descriptor.ReferenceId; - - context.Logger.LogTrace(SR.GetResourceString(SR.ID6017), identifier, descriptor.ReferenceId); - } - } - - /// - /// Contains the logic responsible of creating a device code entry. - /// Note: this handler is not used when the degraded mode is enabled. - /// - public class CreateDeviceCodeEntry : IOpenIddictServerHandler - { - private readonly IOpenIddictApplicationManager _applicationManager; - private readonly IOpenIddictTokenManager _tokenManager; - - public CreateDeviceCodeEntry() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); - - public CreateDeviceCodeEntry( - IOpenIddictApplicationManager applicationManager, - IOpenIddictTokenManager tokenManager) - { - _applicationManager = applicationManager; - _tokenManager = tokenManager; - } - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .AddFilter() - .UseScopedHandler() - .SetOrder(ConvertReferenceAuthorizationCode.Descriptor.Order + 1_000) - .SetType(OpenIddictServerHandlerType.BuiltIn) - .Build(); - - /// - public async ValueTask HandleAsync(ProcessSignInContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (context.EndpointType == OpenIddictServerEndpointType.Verification) - { - return; - } - - var principal = context.DeviceCodePrincipal; - if (principal is null) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0020)); - } - - var descriptor = new OpenIddictTokenDescriptor - { - AuthorizationId = principal.GetAuthorizationId(), - CreationDate = principal.GetCreationDate(), - ExpirationDate = principal.GetExpirationDate(), - Principal = principal, - Status = Statuses.Inactive, - Subject = null, // Device codes are not bound to a user, which is not known until the user code is populated. - Type = TokenTypeHints.DeviceCode - }; - - // If the client application is known, associate it with the token. - if (!string.IsNullOrEmpty(context.Request.ClientId)) - { - var application = await _applicationManager.FindByClientIdAsync(context.Request.ClientId); - if (application is null) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0017)); - } - - descriptor.ApplicationId = await _applicationManager.GetIdAsync(application); - } - - var token = await _tokenManager.CreateAsync(descriptor); - if (token is null) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0019)); - } - - var identifier = await _tokenManager.GetIdAsync(token); - - // Attach the token identifier to the principal so that it can be stored in the token. - principal.SetTokenId(identifier); - - context.Logger.LogTrace(SR.GetResourceString(SR.ID6018), identifier); - } - } - - /// - /// Contains the logic responsible of generating a device code using IdentityModel. - /// - public class GenerateIdentityModelDeviceCode : IOpenIddictServerHandler - { - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .UseSingletonHandler() - .SetOrder(CreateDeviceCodeEntry.Descriptor.Order + 1_000) - .SetType(OpenIddictServerHandlerType.BuiltIn) - .Build(); - - /// - public ValueTask HandleAsync(ProcessSignInContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - // If a device code was already attached by another handler, don't overwrite it. - if (!string.IsNullOrEmpty(context.DeviceCode)) - { - return default; - } - - if (context.DeviceCodePrincipal is null) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0022)); - } - - // Clone the principal and exclude the claim mapped to standard JWT claims. - var principal = context.DeviceCodePrincipal.Clone(claim => claim.Type is not ( - Claims.Private.CreationDate or - Claims.Private.ExpirationDate or - Claims.Private.TokenType)); - - if (principal is null or { Identity: not ClaimsIdentity }) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0022)); - } - - var descriptor = new SecurityTokenDescriptor - { - EncryptingCredentials = context.Options.EncryptionCredentials.First(), - Expires = context.DeviceCodePrincipal.GetExpirationDate()?.UtcDateTime, - IssuedAt = context.DeviceCodePrincipal.GetCreationDate()?.UtcDateTime, - Issuer = context.Issuer?.AbsoluteUri, - SigningCredentials = context.Options.SigningCredentials.First(), - Subject = (ClaimsIdentity) principal.Identity, - TokenType = JsonWebTokenTypes.Private.DeviceCode - }; - - // Attach claims destinations to the JWT claims collection. - var destinations = principal.GetDestinations(); - if (destinations.Count != 0) - { - descriptor.Claims = new Dictionary(StringComparer.Ordinal) - { - [Claims.Private.ClaimDestinationsMap] = destinations - }; - } - - context.DeviceCode = context.Options.JsonWebTokenHandler.CreateToken(descriptor); - - context.Logger.LogTrace(SR.GetResourceString(SR.ID6019), context.DeviceCode, principal.Claims); - - return default; - } - } - - /// - /// Contains the logic responsible of creating a reference device code entry. - /// Note: this handler is not used when the degraded mode is enabled. - /// - public class ConvertReferenceDeviceCode : IOpenIddictServerHandler - { - private readonly IOpenIddictTokenManager _tokenManager; - - public ConvertReferenceDeviceCode() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); - - public ConvertReferenceDeviceCode(IOpenIddictTokenManager tokenManager) - => _tokenManager = tokenManager; - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - // Note: device codes are always reference tokens. - .AddFilter() - .UseScopedHandler() - .SetOrder(GenerateIdentityModelDeviceCode.Descriptor.Order + 1_000) - .SetType(OpenIddictServerHandlerType.BuiltIn) - .Build(); - - /// - public async ValueTask HandleAsync(ProcessSignInContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (string.IsNullOrEmpty(context.DeviceCode)) - { - return; - } - - if (context.EndpointType == OpenIddictServerEndpointType.Verification) - { - return; - } - - var principal = context.DeviceCodePrincipal; - if (principal is null) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0020)); - } - - var identifier = principal.GetTokenId(); - if (string.IsNullOrEmpty(identifier)) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0009)); - } - - var token = await _tokenManager.FindByIdAsync(identifier); - if (token is null) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0021)); - } - - // Generate a new crypto-secure random identifier that will be substituted to the token. - var data = new byte[256 / 8]; -#if SUPPORTS_STATIC_RANDOM_NUMBER_GENERATOR_METHODS - RandomNumberGenerator.Fill(data); -#else - using var generator = RandomNumberGenerator.Create(); - generator.GetBytes(data); -#endif - var descriptor = new OpenIddictTokenDescriptor(); - await _tokenManager.PopulateAsync(descriptor, token); - - // Attach the generated token to the token entry, persist the change - // and replace the returned token by the reference identifier. - descriptor.Payload = context.DeviceCode; - descriptor.Principal = principal; - descriptor.ReferenceId = Base64UrlEncoder.Encode(data); - - await _tokenManager.UpdateAsync(token, descriptor); - - context.DeviceCode = descriptor.ReferenceId; - - context.Logger.LogTrace(SR.GetResourceString(SR.ID6020), identifier, descriptor.ReferenceId); - } - } - - /// - /// Contains the logic responsible of updating the existing reference device code entry. - /// Note: this handler is not used when the degraded mode is enabled. - /// - public class UpdateReferenceDeviceCodeEntry : IOpenIddictServerHandler - { - private readonly IOpenIddictTokenManager _tokenManager; - - public UpdateReferenceDeviceCodeEntry() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); - - public UpdateReferenceDeviceCodeEntry(IOpenIddictTokenManager tokenManager) - => _tokenManager = tokenManager; - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .AddFilter() - .UseScopedHandler() - .SetOrder(ConvertReferenceDeviceCode.Descriptor.Order + 1_000) - .SetType(OpenIddictServerHandlerType.BuiltIn) - .Build(); - - /// - public async ValueTask HandleAsync(ProcessSignInContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (string.IsNullOrEmpty(context.DeviceCode)) - { - return; - } - - if (context.EndpointType != OpenIddictServerEndpointType.Verification) - { - return; - } - - Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); - - var principal = context.DeviceCodePrincipal; - if (principal is null) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0020)); - } - - // Extract the token identifier from the authentication principal. - var identifier = context.Principal.GetClaim(Claims.Private.DeviceCodeId); - if (string.IsNullOrEmpty(identifier)) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0008)); - } - - var token = await _tokenManager.FindByIdAsync(identifier); - if (token is null) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0265)); - } - - // Replace the device code details by the payload derived from the new device code principal, - // that includes all the user claims populated by the application after authenticating the user. - var descriptor = new OpenIddictTokenDescriptor(); - await _tokenManager.PopulateAsync(descriptor, token); - - // Note: the lifetime is deliberately extended to give more time to the client to redeem the code. - descriptor.ExpirationDate = principal.GetExpirationDate(); - descriptor.Payload = context.DeviceCode; - descriptor.Principal = principal; - descriptor.Status = Statuses.Valid; - descriptor.Subject = principal.GetClaim(Claims.Subject); - - await _tokenManager.UpdateAsync(token, descriptor); - - context.Logger.LogTrace(SR.GetResourceString(SR.ID6021), await _tokenManager.GetIdAsync(token)); - } - } - - /// - /// Contains the logic responsible of creating a refresh token entry. - /// Note: this handler is not used when the degraded mode is enabled. - /// - public class CreateRefreshTokenEntry : IOpenIddictServerHandler - { - private readonly IOpenIddictApplicationManager _applicationManager; - private readonly IOpenIddictTokenManager _tokenManager; - - public CreateRefreshTokenEntry() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); - - public CreateRefreshTokenEntry( - IOpenIddictApplicationManager applicationManager, - IOpenIddictTokenManager tokenManager) - { - _applicationManager = applicationManager; - _tokenManager = tokenManager; - } - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .AddFilter() - .UseScopedHandler() - .SetOrder(UpdateReferenceDeviceCodeEntry.Descriptor.Order + 1_000) - .SetType(OpenIddictServerHandlerType.BuiltIn) - .Build(); - - /// - public async ValueTask HandleAsync(ProcessSignInContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - var principal = context.RefreshTokenPrincipal; - if (principal is null) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0020)); - } - - var descriptor = new OpenIddictTokenDescriptor - { - AuthorizationId = principal.GetAuthorizationId(), - CreationDate = principal.GetCreationDate(), - ExpirationDate = principal.GetExpirationDate(), - Principal = principal, - Status = Statuses.Valid, - Subject = principal.GetClaim(Claims.Subject), - Type = TokenTypeHints.RefreshToken - }; - - // If the client application is known, associate it with the token. - if (!string.IsNullOrEmpty(context.Request.ClientId)) - { - var application = await _applicationManager.FindByClientIdAsync(context.Request.ClientId); - if (application is null) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0017)); - } - - descriptor.ApplicationId = await _applicationManager.GetIdAsync(application); - } - - var token = await _tokenManager.CreateAsync(descriptor); - if (token is null) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0019)); - } - - var identifier = await _tokenManager.GetIdAsync(token); - - // Attach the token identifier to the principal so that it can be stored in the token. - principal.SetTokenId(identifier); - - context.Logger.LogTrace(SR.GetResourceString(SR.ID6022), identifier); - } - } - - /// - /// Contains the logic responsible of generating a refresh token using IdentityModel. - /// - public class GenerateIdentityModelRefreshToken : IOpenIddictServerHandler - { - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .UseSingletonHandler() - .SetOrder(CreateRefreshTokenEntry.Descriptor.Order + 1_000) - .SetType(OpenIddictServerHandlerType.BuiltIn) - .Build(); - - /// - public ValueTask HandleAsync(ProcessSignInContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - // If a refresh token was already attached by another handler, don't overwrite it. - if (!string.IsNullOrEmpty(context.RefreshToken)) - { - return default; + throw new ArgumentNullException(nameof(context)); } - if (context.RefreshTokenPrincipal is null) + var notification = new GenerateTokenContext(context.Transaction) { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0022)); - } + Principal = context.AccessTokenPrincipal!, + TokenType = TokenTypeHints.AccessToken + }; - // Clone the principal and exclude the claim mapped to standard JWT claims. - var principal = context.RefreshTokenPrincipal.Clone(claim => claim.Type is not ( - Claims.Private.CreationDate or - Claims.Private.ExpirationDate or - Claims.Private.TokenType)); + await _dispatcher.DispatchAsync(notification); - if (principal is null or { Identity: not ClaimsIdentity }) + if (notification.IsRequestHandled) { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0022)); + context.HandleRequest(); + return; } - var descriptor = new SecurityTokenDescriptor + else if (notification.IsRequestSkipped) { - EncryptingCredentials = context.Options.EncryptionCredentials.First(), - Expires = context.RefreshTokenPrincipal.GetExpirationDate()?.UtcDateTime, - IssuedAt = context.RefreshTokenPrincipal.GetCreationDate()?.UtcDateTime, - Issuer = context.Issuer?.AbsoluteUri, - SigningCredentials = context.Options.SigningCredentials.First(), - Subject = (ClaimsIdentity) principal.Identity, - TokenType = JsonWebTokenTypes.Private.RefreshToken - }; + context.SkipRequest(); + return; + } - // Attach claims destinations to the JWT claims collection. - var destinations = principal.GetDestinations(); - if (destinations.Count != 0) + else if (notification.IsRejected) { - descriptor.Claims = new Dictionary(StringComparer.Ordinal) - { - [Claims.Private.ClaimDestinationsMap] = destinations - }; + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; } - context.RefreshToken = context.Options.JsonWebTokenHandler.CreateToken(descriptor); - - context.Logger.LogTrace(SR.GetResourceString(SR.ID6023), context.RefreshToken, principal.Claims); - - return default; + context.AccessToken = notification.Token; } } /// - /// Contains the logic responsible of converting the refresh token to a reference token. - /// Note: this handler is not used when the degraded mode is enabled. + /// Contains the logic responsible of generating an authorization code for the current sign-in operation. /// - public class ConvertReferenceRefreshToken : IOpenIddictServerHandler + public class GenerateAuthorizationCode : IOpenIddictServerHandler { - private readonly IOpenIddictTokenManager _tokenManager; - - public ConvertReferenceRefreshToken() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); + private readonly IOpenIddictServerDispatcher _dispatcher; - public ConvertReferenceRefreshToken(IOpenIddictTokenManager tokenManager) - => _tokenManager = tokenManager; + public GenerateAuthorizationCode(IOpenIddictServerDispatcher dispatcher) + => _dispatcher = dispatcher; /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .AddFilter() - .AddFilter() - .UseScopedHandler() - .SetOrder(GenerateIdentityModelRefreshToken.Descriptor.Order + 1_000) + .AddFilter() + .UseScopedHandler() + .SetOrder(GenerateAccessToken.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -3557,124 +2278,119 @@ namespace OpenIddict.Server throw new ArgumentNullException(nameof(context)); } - if (string.IsNullOrEmpty(context.RefreshToken)) + var notification = new GenerateTokenContext(context.Transaction) { - return; - } + Principal = context.AuthorizationCodePrincipal!, + TokenType = TokenTypeHints.AuthorizationCode + }; - var principal = context.RefreshTokenPrincipal; - if (principal is null) + await _dispatcher.DispatchAsync(notification); + + if (notification.IsRequestHandled) { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0020)); + context.HandleRequest(); + return; } - var identifier = principal.GetTokenId(); - if (string.IsNullOrEmpty(identifier)) + else if (notification.IsRequestSkipped) { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0009)); + context.SkipRequest(); + return; } - var token = await _tokenManager.FindByIdAsync(identifier); - if (token is null) + else if (notification.IsRejected) { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0021)); + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; } - // Generate a new crypto-secure random identifier that will be substituted to the token. - var data = new byte[256 / 8]; -#if SUPPORTS_STATIC_RANDOM_NUMBER_GENERATOR_METHODS - RandomNumberGenerator.Fill(data); -#else - using var generator = RandomNumberGenerator.Create(); - generator.GetBytes(data); -#endif - var descriptor = new OpenIddictTokenDescriptor(); - await _tokenManager.PopulateAsync(descriptor, token); - - // Attach the generated token to the token entry, persist the change - // and replace the returned token by the reference identifier. - descriptor.Payload = context.RefreshToken; - descriptor.Principal = principal; - descriptor.ReferenceId = Base64UrlEncoder.Encode(data); - - await _tokenManager.UpdateAsync(token, descriptor); - - context.RefreshToken = descriptor.ReferenceId; - - context.Logger.LogTrace(SR.GetResourceString(SR.ID6024), identifier, descriptor.ReferenceId); + context.AuthorizationCode = notification.Token; } } /// - /// Contains the logic responsible of generating and attaching the device code identifier to the user code principal. + /// Contains the logic responsible of generating a device code for the current sign-in operation. /// - public class AttachDeviceCodeIdentifier : IOpenIddictServerHandler + public class GenerateDeviceCode : IOpenIddictServerHandler { + private readonly IOpenIddictServerDispatcher _dispatcher; + + public GenerateDeviceCode(IOpenIddictServerDispatcher dispatcher) + => _dispatcher = dispatcher; + /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() - .AddFilter() - .UseSingletonHandler() - .SetOrder(ConvertReferenceRefreshToken.Descriptor.Order + 1_000) + .UseScopedHandler() + .SetOrder(GenerateAuthorizationCode.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); /// - public ValueTask HandleAsync(ProcessSignInContext context) + public async ValueTask HandleAsync(ProcessSignInContext context) { if (context is null) { throw new ArgumentNullException(nameof(context)); } - var principal = context.UserCodePrincipal; - if (principal is null) + var notification = new GenerateTokenContext(context.Transaction) { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0020)); + Principal = context.DeviceCodePrincipal!, + TokenType = TokenTypeHints.DeviceCode + }; + + await _dispatcher.DispatchAsync(notification); + + if (notification.IsRequestHandled) + { + context.HandleRequest(); + return; } - var identifier = context.DeviceCodePrincipal?.GetTokenId(); - if (!string.IsNullOrEmpty(identifier)) + else if (notification.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + + else if (notification.IsRejected) { - principal.SetClaim(Claims.Private.DeviceCodeId, identifier); + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; } - return default; + context.DeviceCode = notification.Token; } } /// - /// Contains the logic responsible of creating a user code entry. - /// Note: this handler is not used when the degraded mode is enabled. + /// Contains the logic responsible of generating a refresh token for the current sign-in operation. /// - public class CreateUserCodeEntry : IOpenIddictServerHandler + public class GenerateRefreshToken : IOpenIddictServerHandler { - private readonly IOpenIddictApplicationManager _applicationManager; - private readonly IOpenIddictTokenManager _tokenManager; - - public CreateUserCodeEntry() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); + private readonly IOpenIddictServerDispatcher _dispatcher; - public CreateUserCodeEntry( - IOpenIddictApplicationManager applicationManager, - IOpenIddictTokenManager tokenManager) - { - _applicationManager = applicationManager; - _tokenManager = tokenManager; - } + public GenerateRefreshToken(IOpenIddictServerDispatcher dispatcher) + => _dispatcher = dispatcher; /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .AddFilter() - .UseScopedHandler() - .SetOrder(AttachDeviceCodeIdentifier.Descriptor.Order + 1_000) + .AddFilter() + .UseScopedHandler() + .SetOrder(GenerateDeviceCode.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -3686,63 +2402,53 @@ namespace OpenIddict.Server throw new ArgumentNullException(nameof(context)); } - var principal = context.UserCodePrincipal; - if (principal is null) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0020)); - } - - var descriptor = new OpenIddictTokenDescriptor + var notification = new GenerateTokenContext(context.Transaction) { - AuthorizationId = principal.GetAuthorizationId(), - CreationDate = principal.GetCreationDate(), - ExpirationDate = principal.GetExpirationDate(), - Principal = principal, - Status = Statuses.Valid, - Subject = null, // User codes are not bound to a user until authorization is granted. - Type = TokenTypeHints.UserCode + Principal = context.RefreshTokenPrincipal!, + TokenType = TokenTypeHints.RefreshToken }; - // If the client application is known, associate it with the token. - if (!string.IsNullOrEmpty(context.Request.ClientId)) - { - var application = await _applicationManager.FindByClientIdAsync(context.Request.ClientId); - if (application is null) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0017)); - } + await _dispatcher.DispatchAsync(notification); - descriptor.ApplicationId = await _applicationManager.GetIdAsync(application); + if (notification.IsRequestHandled) + { + context.HandleRequest(); + return; } - var token = await _tokenManager.CreateAsync(descriptor); - if (token is null) + else if (notification.IsRequestSkipped) { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0019)); + context.SkipRequest(); + return; } - var identifier = await _tokenManager.GetIdAsync(token); - - // Attach the token identifier to the principal so that it can be stored in the token. - principal.SetTokenId(identifier); + else if (notification.IsRejected) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } - context.Logger.LogTrace(SR.GetResourceString(SR.ID6025), identifier); + context.RefreshToken = notification.Token; } } /// - /// Contains the logic responsible of generating a user code using IdentityModel. + /// Contains the logic responsible of generating and attaching the device code identifier to the user code principal. /// - public class GenerateIdentityModelUserCode : IOpenIddictServerHandler + public class AttachDeviceCodeIdentifier : IOpenIddictServerHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() .AddFilter() - .UseSingletonHandler() - .SetOrder(CreateUserCodeEntry.Descriptor.Order + 1_000) + .UseSingletonHandler() + .SetOrder(GenerateRefreshToken.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -3754,58 +2460,32 @@ namespace OpenIddict.Server throw new ArgumentNullException(nameof(context)); } - // If a user code was already attached by another handler, don't overwrite it. - if (!string.IsNullOrEmpty(context.UserCode)) - { - return default; - } - if (context.UserCodePrincipal is null) { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0022)); + throw new InvalidOperationException(SR.GetResourceString(SR.ID0020)); } - // Clone the principal and exclude the claim mapped to standard JWT claims. - var principal = context.UserCodePrincipal.Clone(claim => claim.Type is not ( - Claims.Private.CreationDate or - Claims.Private.ExpirationDate or - Claims.Private.TokenType)); - - if (principal is null or { Identity: not ClaimsIdentity }) + var identifier = context.DeviceCodePrincipal?.GetTokenId(); + if (!string.IsNullOrEmpty(identifier)) { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0022)); + context.UserCodePrincipal.SetClaim(Claims.Private.DeviceCodeId, identifier); } - var descriptor = new SecurityTokenDescriptor - { - EncryptingCredentials = context.Options.EncryptionCredentials.First(), - Expires = context.UserCodePrincipal.GetExpirationDate()?.UtcDateTime, - IssuedAt = context.UserCodePrincipal.GetCreationDate()?.UtcDateTime, - Issuer = context.Issuer?.AbsoluteUri, - SigningCredentials = context.Options.SigningCredentials.First(), - Subject = (ClaimsIdentity) principal.Identity, - TokenType = JsonWebTokenTypes.Private.UserCode - }; - - context.UserCode = context.Options.JsonWebTokenHandler.CreateToken(descriptor); - - context.Logger.LogTrace(SR.GetResourceString(SR.ID6026), context.UserCode, principal.Claims); - return default; } } /// - /// Contains the logic responsible of converting the user code to a reference token. + /// Contains the logic responsible of updating the existing reference device code entry. /// Note: this handler is not used when the degraded mode is enabled. /// - public class ConvertReferenceUserCode : IOpenIddictServerHandler + public class UpdateReferenceDeviceCodeEntry : IOpenIddictServerHandler { private readonly IOpenIddictTokenManager _tokenManager; - public ConvertReferenceUserCode() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); + public UpdateReferenceDeviceCodeEntry() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); - public ConvertReferenceUserCode(IOpenIddictTokenManager tokenManager) + public UpdateReferenceDeviceCodeEntry(IOpenIddictTokenManager tokenManager) => _tokenManager = tokenManager; /// @@ -3815,10 +2495,9 @@ namespace OpenIddict.Server = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() .AddFilter() - // Note: user codes are always reference tokens. - .AddFilter() - .UseScopedHandler() - .SetOrder(GenerateIdentityModelUserCode.Descriptor.Order + 1_000) + .AddFilter() + .UseScopedHandler() + .SetOrder(AttachDeviceCodeIdentifier.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -3830,79 +2509,46 @@ namespace OpenIddict.Server throw new ArgumentNullException(nameof(context)); } - if (string.IsNullOrEmpty(context.UserCode)) + if (context.EndpointType != OpenIddictServerEndpointType.Verification || string.IsNullOrEmpty(context.DeviceCode)) { return; } - var principal = context.UserCodePrincipal; - if (principal is null) + Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + + if (context.DeviceCodePrincipal is null) { throw new InvalidOperationException(SR.GetResourceString(SR.ID0020)); } - var identifier = principal.GetTokenId(); + // Extract the token identifier from the authentication principal. + var identifier = context.Principal.GetClaim(Claims.Private.DeviceCodeId); if (string.IsNullOrEmpty(identifier)) { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0009)); + throw new InvalidOperationException(SR.GetResourceString(SR.ID0008)); } var token = await _tokenManager.FindByIdAsync(identifier); if (token is null) { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0021)); + throw new InvalidOperationException(SR.GetResourceString(SR.ID0265)); } - // Note: unlike other reference tokens, user codes are meant to be used by humans, - // who may have to enter it in a web form. To ensure it remains easy enough to type - // even by users with non-Latin keyboards, user codes generated by OpenIddict are - // only compound of 12 digits, generated using a crypto-secure random number generator. - // In this case, the resulting user code is estimated to have at most ~40 bits of entropy. - + // Replace the device code details by the payload derived from the new device code principal, + // that includes all the user claims populated by the application after authenticating the user. var descriptor = new OpenIddictTokenDescriptor(); await _tokenManager.PopulateAsync(descriptor, token); - // Attach the generated token to the token entry, persist the change - // and replace the returned token by the reference identifier. - descriptor.Payload = context.UserCode; - descriptor.Principal = principal; - descriptor.ReferenceId = await GenerateReferenceIdentifierAsync(_tokenManager); + // Note: the lifetime is deliberately extended to give more time to the client to redeem the code. + descriptor.ExpirationDate = context.DeviceCodePrincipal.GetExpirationDate(); + descriptor.Payload = context.DeviceCode; + descriptor.Principal = context.DeviceCodePrincipal; + descriptor.Status = Statuses.Valid; + descriptor.Subject = context.DeviceCodePrincipal.GetClaim(Claims.Subject); await _tokenManager.UpdateAsync(token, descriptor); - context.UserCode = descriptor.ReferenceId; - - context.Logger.LogTrace(SR.GetResourceString(SR.ID6027), identifier, descriptor.ReferenceId); - - static async ValueTask GenerateReferenceIdentifierAsync(IOpenIddictTokenManager manager) - { - string token; - - do - { - var data = new byte[12]; -#if SUPPORTS_STATIC_RANDOM_NUMBER_GENERATOR_METHODS - RandomNumberGenerator.Fill(data); -#else - using var generator = RandomNumberGenerator.Create(); - generator.GetBytes(data); -#endif - var builder = new StringBuilder(data.Length); - - for (var index = 0; index < data.Length; index += 4) - { - builder.AppendFormat(CultureInfo.InvariantCulture, "{0:D4}", BitConverter.ToUInt32(data, index) % 10000); - } - - token = builder.ToString(); - } - - // User codes are relatively short. To help reduce the risks of collisions with - // existing entries, a database check is performed here before updating the entry. - while (await manager.FindByReferenceIdAsync(token) is not null); - - return token; - } + context.Logger.LogTrace(SR.GetResourceString(SR.ID6021), await _tokenManager.GetIdAsync(token)); } } @@ -3919,7 +2565,7 @@ namespace OpenIddict.Server = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler() - .SetOrder(ConvertReferenceUserCode.Descriptor.Order + 1_000) + .SetOrder(UpdateReferenceDeviceCodeEntry.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -3931,8 +2577,7 @@ namespace OpenIddict.Server throw new ArgumentNullException(nameof(context)); } - var principal = context.IdentityTokenPrincipal; - if (principal is null) + if (context.IdentityTokenPrincipal is null) { throw new InvalidOperationException(SR.GetResourceString(SR.ID0022)); } @@ -3961,7 +2606,7 @@ namespace OpenIddict.Server // Note: only the left-most half of the hash is used. // See http://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken - principal.SetClaim(Claims.AccessTokenHash, Base64UrlEncoder.Encode(digest, 0, digest.Length / 2)); + context.IdentityTokenPrincipal.SetClaim(Claims.AccessTokenHash, Base64UrlEncoder.Encode(digest, 0, digest.Length / 2)); } if (!string.IsNullOrEmpty(context.AuthorizationCode)) @@ -3970,7 +2615,7 @@ namespace OpenIddict.Server // Note: only the left-most half of the hash is used. // See http://openid.net/specs/openid-connect-core-1_0.html#HybridIDToken - principal.SetClaim(Claims.CodeHash, Base64UrlEncoder.Encode(digest, 0, digest.Length / 2)); + context.IdentityTokenPrincipal.SetClaim(Claims.CodeHash, Base64UrlEncoder.Encode(digest, 0, digest.Length / 2)); } return default; @@ -4036,33 +2681,22 @@ namespace OpenIddict.Server } /// - /// Contains the logic responsible of creating an identity token entry. - /// Note: this handler is not used when the degraded mode is enabled. + /// Contains the logic responsible of generating a user code for the current sign-in operation. /// - public class CreateIdentityTokenEntry : IOpenIddictServerHandler + public class GenerateUserCode : IOpenIddictServerHandler { - private readonly IOpenIddictApplicationManager _applicationManager; - private readonly IOpenIddictTokenManager _tokenManager; + private readonly IOpenIddictServerDispatcher _dispatcher; - public CreateIdentityTokenEntry() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); - - public CreateIdentityTokenEntry( - IOpenIddictApplicationManager applicationManager, - IOpenIddictTokenManager tokenManager) - { - _applicationManager = applicationManager; - _tokenManager = tokenManager; - } + public GenerateUserCode(IOpenIddictServerDispatcher dispatcher) + => _dispatcher = dispatcher; /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .AddFilter() - .UseScopedHandler() + .AddFilter() + .UseScopedHandler() .SetOrder(AttachTokenDigests.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -4075,183 +2709,98 @@ namespace OpenIddict.Server throw new ArgumentNullException(nameof(context)); } - var principal = context.IdentityTokenPrincipal; - if (principal is null) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0020)); - } - - var descriptor = new OpenIddictTokenDescriptor + var notification = new GenerateTokenContext(context.Transaction) { - AuthorizationId = principal.GetAuthorizationId(), - CreationDate = principal.GetCreationDate(), - ExpirationDate = principal.GetExpirationDate(), - Principal = principal, - Status = Statuses.Valid, - Subject = principal.GetClaim(Claims.Subject), - Type = TokenTypeHints.IdToken + Principal = context.UserCodePrincipal!, + TokenType = TokenTypeHints.UserCode }; - // If the client application is known, associate it with the token. - if (!string.IsNullOrEmpty(context.Request.ClientId)) - { - var application = await _applicationManager.FindByClientIdAsync(context.Request.ClientId); - if (application is null) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0017)); - } + await _dispatcher.DispatchAsync(notification); - descriptor.ApplicationId = await _applicationManager.GetIdAsync(application); + if (notification.IsRequestHandled) + { + context.HandleRequest(); + return; } - var token = await _tokenManager.CreateAsync(descriptor); - if (token is null) + else if (notification.IsRequestSkipped) { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0019)); + context.SkipRequest(); + return; } - var identifier = await _tokenManager.GetIdAsync(token); - - // Attach the token identifier to the principal so that it can be stored in the token. - principal.SetTokenId(identifier); + else if (notification.IsRejected) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } - context.Logger.LogTrace(SR.GetResourceString(SR.ID6028), identifier); + context.UserCode = notification.Token; } } /// - /// Contains the logic responsible of generating an identity token using IdentityModel. + /// Contains the logic responsible of generating an identity token for the current sign-in operation. /// - public class GenerateIdentityModelIdentityToken : IOpenIddictServerHandler + public class GenerateIdentityToken : IOpenIddictServerHandler { + private readonly IOpenIddictServerDispatcher _dispatcher; + + public GenerateIdentityToken(IOpenIddictServerDispatcher dispatcher) + => _dispatcher = dispatcher; + /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() - .UseSingletonHandler() - .SetOrder(CreateIdentityTokenEntry.Descriptor.Order + 1_000) + .UseScopedHandler() + .SetOrder(GenerateUserCode.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); /// - public ValueTask HandleAsync(ProcessSignInContext context) + public async ValueTask HandleAsync(ProcessSignInContext context) { if (context is null) { throw new ArgumentNullException(nameof(context)); } - // If an identity token was already attached by another handler, don't overwrite it. - if (!string.IsNullOrEmpty(context.IdentityToken)) - { - return default; - } - - if (context.IdentityTokenPrincipal is null) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0022)); - } - - // Clone the principal and exclude the claim mapped to standard JWT claims. - var principal = context.IdentityTokenPrincipal.Clone(claim => claim.Type is not ( - Claims.Private.Audience or - Claims.Private.CreationDate or - Claims.Private.ExpirationDate or - Claims.Private.TokenType)); - - if (principal is null or { Identity: not ClaimsIdentity }) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0022)); - } - - var claims = new Dictionary(StringComparer.Ordinal); - - // Set the public audience claims using the private audience claims from the principal. - // Note: when there's a single audience, represent it as a unique string claim. - var audiences = context.IdentityTokenPrincipal.GetAudiences(); - if (audiences.Any()) + var notification = new GenerateTokenContext(context.Transaction) { - claims.Add(Claims.Audience, audiences.Length switch - { - 1 => audiences.ElementAt(0), - _ => audiences - }); - } - - var descriptor = new SecurityTokenDescriptor - { - Claims = claims, - Expires = context.IdentityTokenPrincipal.GetExpirationDate()?.UtcDateTime, - IssuedAt = context.IdentityTokenPrincipal.GetCreationDate()?.UtcDateTime, - Issuer = context.Issuer?.AbsoluteUri, - // Note: unlike other tokens, identity tokens can only be signed using an asymmetric key - // as they are meant to be validated by clients using the public keys exposed by the server. - SigningCredentials = context.Options.SigningCredentials.First(credentials => - credentials.Key is AsymmetricSecurityKey), - Subject = (ClaimsIdentity) principal.Identity, - TokenType = JsonWebTokenTypes.IdentityToken + Principal = context.IdentityTokenPrincipal!, + TokenType = TokenTypeHints.IdToken }; - context.IdentityToken = context.Options.JsonWebTokenHandler.CreateToken(descriptor); - - context.Logger.LogTrace(SR.GetResourceString(SR.ID6029), context.IdentityToken, principal.Claims); - - return default; - } - } - - /// - /// Contains the logic responsible of beautifying the user code returned to the client. - /// Note: this handler is not used when the degraded mode is enabled. - /// - public class BeautifyUserCode : IOpenIddictServerHandler - { - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - // Technically, this handler doesn't require that the degraded mode be disabled - // but the default CreateReferenceUserCodeEntry that creates the user code - // reference identifiers only works when the degraded mode is disabled. - .AddFilter() - .AddFilter() - .UseSingletonHandler() - .SetOrder(GenerateIdentityModelIdentityToken.Descriptor.Order + 1_000) - .SetType(OpenIddictServerHandlerType.BuiltIn) - .Build(); + await _dispatcher.DispatchAsync(notification); - /// - public ValueTask HandleAsync(ProcessSignInContext context) - { - if (context is null) + if (notification.IsRequestHandled) { - throw new ArgumentNullException(nameof(context)); + context.HandleRequest(); + return; } - // To make user codes easier to read and type by humans, a dash is automatically - // appended before each new block of 4 integers. These dashes are expected to be - // stripped from the user codes when receiving them at the verification endpoint. - - var builder = new StringBuilder(context.UserCode); - if (builder.Length % 4 != 0) + else if (notification.IsRequestSkipped) { - return default; + context.SkipRequest(); + return; } - for (var index = builder.Length; index >= 0; index -= 4) + else if (notification.IsRejected) { - if (index != 0 && index != builder.Length) - { - builder.Insert(index, Separators.Dash[0]); - } + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; } - context.UserCode = builder.ToString(); - - return default; + context.IdentityToken = notification.Token; } } @@ -4266,7 +2815,7 @@ namespace OpenIddict.Server public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .UseSingletonHandler() - .SetOrder(BeautifyUserCode.Descriptor.Order + 1_000) + .SetOrder(GenerateIdentityToken.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); diff --git a/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreConstants.cs b/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreConstants.cs index 2cbd63ce..5cd67d27 100644 --- a/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreConstants.cs +++ b/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreConstants.cs @@ -19,6 +19,7 @@ namespace OpenIddict.Validation.AspNetCore public static class Properties { + public const string AccessTokenPrincipal = ".access_token_principal"; public const string Error = ".error"; public const string ErrorDescription = ".error_description"; public const string ErrorUri = ".error_uri"; diff --git a/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandler.cs b/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandler.cs index 262a6268..3546e282 100644 --- a/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandler.cs +++ b/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandler.cs @@ -6,8 +6,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; -using System.Security.Claims; using System.Text.Encodings.Web; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; @@ -17,6 +15,7 @@ using Microsoft.Extensions.Options; using OpenIddict.Abstractions; using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Validation.OpenIddictValidationEvents; +using Properties = OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreConstants.Properties; using SR = OpenIddict.Abstractions.OpenIddictResources; namespace OpenIddict.Validation.AspNetCore @@ -143,9 +142,9 @@ namespace OpenIddict.Validation.AspNetCore var properties = new AuthenticationProperties(new Dictionary { - [OpenIddictValidationAspNetCoreConstants.Properties.Error] = context.Error, - [OpenIddictValidationAspNetCoreConstants.Properties.ErrorDescription] = context.ErrorDescription, - [OpenIddictValidationAspNetCoreConstants.Properties.ErrorUri] = context.ErrorUri + [Properties.Error] = context.Error, + [Properties.ErrorDescription] = context.ErrorDescription, + [Properties.ErrorUri] = context.ErrorUri }); return AuthenticateResult.Fail(SR.GetResourceString(SR.ID0113), properties); @@ -153,29 +152,51 @@ namespace OpenIddict.Validation.AspNetCore else { - Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); - Debug.Assert(!string.IsNullOrEmpty(context.Principal.GetTokenType()), SR.GetResourceString(SR.ID4009)); - Debug.Assert(!string.IsNullOrEmpty(context.Token), SR.GetResourceString(SR.ID4010)); + // A single main claims-based principal instance can be attached to an authentication ticket. + // To return the most appropriate one, the principal is selected based on the endpoint type. + // Independently of the selected main principal, all principals resolved from validated tokens + // are attached to the authentication properties bag so they can be accessed from user code. + var principal = context.EndpointType switch + { + OpenIddictValidationEndpointType.Unknown => context.AccessTokenPrincipal, + + _ => null + }; + + if (principal is null) + { + return AuthenticateResult.NoResult(); + } - // Store the token to allow any ASP.NET Core component (e.g a controller) - // to retrieve it (e.g to make an API request to another application). var properties = new AuthenticationProperties { - ExpiresUtc = context.Principal.GetExpirationDate(), - IssuedUtc = context.Principal.GetCreationDate() + ExpiresUtc = principal.GetExpirationDate(), + IssuedUtc = principal.GetCreationDate() }; - properties.StoreTokens(new[] + List? tokens = null; + + // Attach the tokens to allow any ASP.NET Core component (e.g a controller) + // to retrieve them (e.g to make an API request to another application). + + if (context.AccessTokenPrincipal is not null && !string.IsNullOrEmpty(context.AccessToken)) { - new AuthenticationToken + tokens ??= new(capacity: 1); + tokens.Add(new AuthenticationToken { - Name = context.Principal.GetTokenType()!, - Value = context.Token - } - }); + Name = TokenTypeHints.AccessToken, + Value = context.AccessToken + }); + + properties.SetParameter(Properties.AccessTokenPrincipal, context.AccessTokenPrincipal); + } + + if (tokens is { Count: > 0 }) + { + properties.StoreTokens(tokens); + } - return AuthenticateResult.Success(new AuthenticationTicket( - context.Principal, properties, + return AuthenticateResult.Success(new AuthenticationTicket(principal, properties, OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)); } } diff --git a/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandlers.cs b/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandlers.cs index b47c1c45..5234e354 100644 --- a/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandlers.cs +++ b/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandlers.cs @@ -24,6 +24,8 @@ using Microsoft.Net.Http.Headers; using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandlerFilters; using static OpenIddict.Validation.OpenIddictValidationEvents; +using static OpenIddict.Validation.OpenIddictValidationHandlerFilters; +using static OpenIddict.Validation.OpenIddictValidationHandlers; using Properties = OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreConstants.Properties; using SR = OpenIddict.Abstractions.OpenIddictResources; @@ -144,8 +146,9 @@ namespace OpenIddict.Validation.AspNetCore public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() .AddFilter() + .AddFilter() .UseSingletonHandler() - .SetOrder(int.MinValue + 50_000) + .SetOrder(EvaluateValidatedTokens.Descriptor.Order + 500) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); @@ -158,7 +161,7 @@ namespace OpenIddict.Validation.AspNetCore } // If a token was already resolved, don't overwrite it. - if (!string.IsNullOrEmpty(context.Token)) + if (!string.IsNullOrEmpty(context.AccessToken)) { return default; } @@ -176,8 +179,7 @@ namespace OpenIddict.Validation.AspNetCore string header = request.Headers[HeaderNames.Authorization]; if (!string.IsNullOrEmpty(header) && header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) { - context.Token = header.Substring("Bearer ".Length); - context.TokenType = TokenTypeHints.AccessToken; + context.AccessToken = header.Substring("Bearer ".Length); return default; } @@ -198,6 +200,7 @@ namespace OpenIddict.Validation.AspNetCore public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() .AddFilter() + .AddFilter() .UseSingletonHandler() .SetOrder(ExtractAccessTokenFromAuthorizationHeader.Descriptor.Order + 1_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) @@ -212,7 +215,7 @@ namespace OpenIddict.Validation.AspNetCore } // If a token was already resolved, don't overwrite it. - if (!string.IsNullOrEmpty(context.Token)) + if (!string.IsNullOrEmpty(context.AccessToken)) { return; } @@ -236,8 +239,7 @@ namespace OpenIddict.Validation.AspNetCore var form = await request.ReadFormAsync(request.HttpContext.RequestAborted); if (form.TryGetValue(Parameters.AccessToken, out StringValues token)) { - context.Token = token; - context.TokenType = TokenTypeHints.AccessToken; + context.AccessToken = token; return; } @@ -256,6 +258,7 @@ namespace OpenIddict.Validation.AspNetCore public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() .AddFilter() + .AddFilter() .UseSingletonHandler() .SetOrder(ExtractAccessTokenFromBodyForm.Descriptor.Order + 1_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) @@ -270,7 +273,7 @@ namespace OpenIddict.Validation.AspNetCore } // If a token was already resolved, don't overwrite it. - if (!string.IsNullOrEmpty(context.Token)) + if (!string.IsNullOrEmpty(context.AccessToken)) { return default; } @@ -287,8 +290,7 @@ namespace OpenIddict.Validation.AspNetCore // See https://tools.ietf.org/html/rfc6750#section-2.3 for more information. if (request.Query.TryGetValue(Parameters.AccessToken, out StringValues token)) { - context.Token = token; - context.TokenType = TokenTypeHints.AccessToken; + context.AccessToken = token; return default; } diff --git a/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionHandlers.Protection.cs b/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionHandlers.Protection.cs new file mode 100644 index 00000000..e83506a5 --- /dev/null +++ b/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionHandlers.Protection.cs @@ -0,0 +1,130 @@ +/* + * 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; +using System.Collections.Immutable; +using System.IO; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using OpenIddict.Abstractions; +using static OpenIddict.Abstractions.OpenIddictConstants; +using static OpenIddict.Validation.DataProtection.OpenIddictValidationDataProtectionConstants.Purposes; +using static OpenIddict.Validation.OpenIddictValidationEvents; +using static OpenIddict.Validation.OpenIddictValidationHandlerFilters; +using static OpenIddict.Validation.OpenIddictValidationHandlers.Protection; +using Schemes = OpenIddict.Validation.DataProtection.OpenIddictValidationDataProtectionConstants.Purposes.Schemes; +using SR = OpenIddict.Abstractions.OpenIddictResources; + +namespace OpenIddict.Validation.DataProtection +{ + public static partial class OpenIddictValidationDataProtectionHandlers + { + public static class Protection + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Token validation: + */ + ValidateDataProtectionToken.Descriptor); + + /// + /// Contains the logic responsible of validating tokens generated using Data Protection. + /// + public class ValidateDataProtectionToken : IOpenIddictValidationHandler + { + private readonly IOptionsMonitor _options; + + public ValidateDataProtectionToken(IOptionsMonitor options) + => _options = options; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateIdentityModelToken.Descriptor.Order + 500) + .SetType(OpenIddictValidationHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ValidateTokenContext context) + { + // If a principal was already attached, don't overwrite it. + if (context.Principal is not null) + { + return default; + } + + // Note: ASP.NET Core Data Protection tokens always start with "CfDJ8", that corresponds + // to the base64 representation of the magic "09 F0 C9 F0" header identifying DP payloads. + if (!context.Token.StartsWith("CfDJ8", StringComparison.Ordinal)) + { + return default; + } + + // Note: unlike the equivalent handler in the server stack, the logic used here is + // simpler as only access tokens are currently supported by the validation stack. + var principal = context.ValidTokenTypes.Count is 0 || context.ValidTokenTypes.Contains(TokenTypeHints.AccessToken) ? + ValidateToken(context.Token, TokenTypeHints.AccessToken) : + null; + + if (principal is null) + { + context.Reject( + error: Errors.InvalidToken, + description: SR.GetResourceString(SR.ID2004), + uri: SR.FormatID8000(SR.ID2004)); + + return default; + } + + context.Principal = principal; + + context.Logger.LogTrace(SR.GetResourceString(SR.ID6152), context.Token, context.Principal.Claims); + + return default; + + ClaimsPrincipal? ValidateToken(string token, string type) + { + // Create a Data Protection protector using the provider registered in the options. + var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(type switch + { + // Note: reference tokens are encrypted using a different "purpose" string than non-reference tokens. + TokenTypeHints.AccessToken when !string.IsNullOrEmpty(context.TokenId) + => new[] { Handlers.Server, Formats.AccessToken, Features.ReferenceTokens, Schemes.Server }, + TokenTypeHints.AccessToken => new[] { Handlers.Server, Formats.AccessToken, Schemes.Server }, + + _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003)) + }); + + try + { + using var buffer = new MemoryStream(protector.Unprotect(Base64UrlEncoder.DecodeBytes(token))); + using var reader = new BinaryReader(buffer); + + // Note: since the data format relies on a data protector using different "purposes" strings + // per token type, the token processed at this stage is guaranteed to be of the expected type. + return _options.CurrentValue.Formatter.ReadToken(reader)?.SetTokenType(type); + } + + catch (Exception exception) + { + context.Logger.LogTrace(exception, SR.GetResourceString(SR.ID6153), token); + + return null; + } + } + } + } + } + } +} diff --git a/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionHandlers.cs b/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionHandlers.cs index c8a02c54..e27e278a 100644 --- a/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionHandlers.cs +++ b/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionHandlers.cs @@ -4,118 +4,15 @@ * the license and the contributors participating to this project. */ -using System; using System.Collections.Immutable; using System.ComponentModel; -using System.IO; -using System.Threading.Tasks; -using Microsoft.AspNetCore.DataProtection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.IdentityModel.Tokens; -using OpenIddict.Abstractions; -using static OpenIddict.Abstractions.OpenIddictConstants; -using static OpenIddict.Validation.DataProtection.OpenIddictValidationDataProtectionConstants.Purposes; -using static OpenIddict.Validation.OpenIddictValidationEvents; -using static OpenIddict.Validation.OpenIddictValidationHandlers; -using Properties = OpenIddict.Validation.OpenIddictValidationConstants.Properties; -using Schemes = OpenIddict.Validation.DataProtection.OpenIddictValidationDataProtectionConstants.Purposes.Schemes; -using SR = OpenIddict.Abstractions.OpenIddictResources; namespace OpenIddict.Validation.DataProtection { [EditorBrowsable(EditorBrowsableState.Never)] public static partial class OpenIddictValidationDataProtectionHandlers { - public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( - /* - * Authentication processing: - */ - ValidateDataProtectionToken.Descriptor); - - /// - /// Contains the logic responsible of validating tokens generated using Data Protection. - /// - public class ValidateDataProtectionToken : IOpenIddictValidationHandler - { - private readonly IOptionsMonitor _options; - - public ValidateDataProtectionToken(IOptionsMonitor options) - => _options = options; - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictValidationHandlerDescriptor Descriptor { get; } - = OpenIddictValidationHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(ValidateIdentityModelToken.Descriptor.Order + 500) - .SetType(OpenIddictValidationHandlerType.BuiltIn) - .Build(); - - /// - public ValueTask HandleAsync(ProcessAuthenticationContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - // If a principal was already attached, don't overwrite it. - if (context.Principal is not null) - { - return default; - } - - // Note: ASP.NET Core Data Protection tokens always start with "CfDJ8", that corresponds - // to the base64 representation of the magic "09 F0 C9 F0" header identifying DP payloads. - if (string.IsNullOrEmpty(context.Token) || !context.Token.StartsWith("CfDJ8", StringComparison.Ordinal)) - { - return default; - } - - // Create a Data Protection protector using the provider registered in the options. - var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(context.TokenType switch - { - null => throw new InvalidOperationException(SR.GetResourceString(SR.ID0167)), - - TokenTypeHints.AccessToken when context.Transaction.Properties.ContainsKey(Properties.ReferenceTokenIdentifier) - => new[] { Handlers.Server, Formats.AccessToken, Features.ReferenceTokens, Schemes.Server }, - - TokenTypeHints.AccessToken => new[] { Handlers.Server, Formats.AccessToken, Schemes.Server }, - - _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003)) - }); - - try - { - using var buffer = new MemoryStream(protector.Unprotect(Base64UrlEncoder.DecodeBytes(context.Token))); - using var reader = new BinaryReader(buffer); - - // Note: since the data format relies on a data protector using different "purposes" strings - // per token type, the token processed at this stage is guaranteed to be of the expected type. - context.Principal = _options.CurrentValue.Formatter.ReadToken(reader)?.SetTokenType(context.TokenType); - } - - catch (Exception exception) - { - context.Logger.LogTrace(exception, SR.GetResourceString(SR.ID6153), context.Token); - } - - if (context.Principal is null) - { - context.Reject( - error: Errors.InvalidToken, - description: SR.GetResourceString(SR.ID2004), - uri: SR.FormatID8000(SR.ID2004)); - - return default; - } - - context.Logger.LogTrace(SR.GetResourceString(SR.ID6152), context.Token, context.Principal.Claims); - - return default; - } - } + public static ImmutableArray DefaultHandlers { get; } + = ImmutableArray.CreateRange(Protection.DefaultHandlers); } } diff --git a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinConstants.cs b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinConstants.cs index ae00c1f9..d8b418b0 100644 --- a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinConstants.cs +++ b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinConstants.cs @@ -30,6 +30,7 @@ namespace OpenIddict.Validation.Owin public static class Properties { + public const string AccessTokenPrincipal = ".access_token_principal"; public const string Error = ".error"; public const string ErrorDescription = ".error_description"; public const string ErrorUri = ".error_uri"; diff --git a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandler.cs b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandler.cs index 497cedff..e604d9f9 100644 --- a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandler.cs +++ b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandler.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.Owin; @@ -15,6 +14,7 @@ using Microsoft.Owin.Security.Infrastructure; using OpenIddict.Abstractions; using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Validation.OpenIddictValidationEvents; +using Properties = OpenIddict.Validation.Owin.OpenIddictValidationOwinConstants.Properties; using SR = OpenIddict.Abstractions.OpenIddictResources; namespace OpenIddict.Validation.Owin @@ -152,11 +152,11 @@ namespace OpenIddict.Validation.Owin return null; } - var properties = new AuthenticationProperties(new Dictionary + var properties = new OpenIddictValidationOwinProperties(new Dictionary { - [OpenIddictValidationOwinConstants.Properties.Error] = context.Error, - [OpenIddictValidationOwinConstants.Properties.ErrorDescription] = context.ErrorDescription, - [OpenIddictValidationOwinConstants.Properties.ErrorUri] = context.ErrorUri + [Properties.Error] = context.Error, + [Properties.ErrorDescription] = context.ErrorDescription, + [Properties.ErrorUri] = context.ErrorUri }); return new AuthenticationTicket(null, properties); @@ -164,22 +164,38 @@ namespace OpenIddict.Validation.Owin else { - Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); - Debug.Assert(!string.IsNullOrEmpty(context.Principal.GetTokenType()), SR.GetResourceString(SR.ID4009)); - Debug.Assert(!string.IsNullOrEmpty(context.Token), SR.GetResourceString(SR.ID4010)); + // A single main claims-based principal instance can be attached to an authentication ticket. + // To return the most appropriate one, the principal is selected based on the endpoint type. + // Independently of the selected main principal, all principals resolved from validated tokens + // are attached to the authentication properties bag so they can be accessed from user code. + var principal = context.EndpointType switch + { + OpenIddictValidationEndpointType.Unknown => context.AccessTokenPrincipal, + + _ => null + }; - // Store the token to allow any OWIN/Katana component (e.g a controller) - // to retrieve it (e.g to make an API request to another application). - var properties = new AuthenticationProperties(new Dictionary + if (principal is null) { - [context.Principal.GetTokenType()!] = context.Token - }) + return null; + } + + var properties = new OpenIddictValidationOwinProperties { - ExpiresUtc = context.Principal.GetExpirationDate(), - IssuedUtc = context.Principal.GetCreationDate() + ExpiresUtc = principal.GetExpirationDate(), + IssuedUtc = principal.GetCreationDate() }; - return new AuthenticationTicket((ClaimsIdentity) context.Principal.Identity, properties); + // Attach the tokens to allow any OWIN/Katana component (e.g a controller) + // to retrieve them (e.g to make an API request to another application). + + if (context.AccessTokenPrincipal is not null && !string.IsNullOrEmpty(context.AccessToken)) + { + properties.Dictionary[TokenTypeHints.AccessToken] = context.AccessToken; + properties.SetParameter(Properties.AccessTokenPrincipal, context.AccessTokenPrincipal); + } + + return new AuthenticationTicket((ClaimsIdentity) principal.Identity, properties); } } @@ -202,7 +218,7 @@ namespace OpenIddict.Validation.Owin // corresponds to a challenge response, as LookupChallenge() will always return a non-null // value when active authentication is used, even if no challenge was actually triggered. var challenge = Helper.LookupChallenge(Options.AuthenticationType, Options.AuthenticationMode); - if (challenge is not null && (Response.StatusCode == 401 || Response.StatusCode == 403)) + if (challenge is not null && Response.StatusCode is 401 or 403) { var transaction = Context.Get(typeof(OpenIddictValidationTransaction).FullName) ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0166)); diff --git a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlers.cs b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlers.cs index 3c9760ee..522476e7 100644 --- a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlers.cs +++ b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandlers.cs @@ -20,6 +20,8 @@ using Microsoft.Owin.Security; using Owin; using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Validation.OpenIddictValidationEvents; +using static OpenIddict.Validation.OpenIddictValidationHandlerFilters; +using static OpenIddict.Validation.OpenIddictValidationHandlers; using static OpenIddict.Validation.Owin.OpenIddictValidationOwinConstants; using static OpenIddict.Validation.Owin.OpenIddictValidationOwinHandlerFilters; using Properties = OpenIddict.Validation.Owin.OpenIddictValidationOwinConstants.Properties; @@ -142,8 +144,9 @@ namespace OpenIddict.Validation.Owin public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() .AddFilter() + .AddFilter() .UseSingletonHandler() - .SetOrder(int.MinValue + 50_000) + .SetOrder(EvaluateValidatedTokens.Descriptor.Order + 500) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); @@ -156,7 +159,7 @@ namespace OpenIddict.Validation.Owin } // If a token was already resolved, don't overwrite it. - if (!string.IsNullOrEmpty(context.Token)) + if (!string.IsNullOrEmpty(context.AccessToken)) { return default; } @@ -174,8 +177,7 @@ namespace OpenIddict.Validation.Owin string header = request.Headers[Headers.Authorization]; if (!string.IsNullOrEmpty(header) && header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) { - context.Token = header.Substring("Bearer ".Length); - context.TokenType = TokenTypeHints.AccessToken; + context.AccessToken = header.Substring("Bearer ".Length); return default; } @@ -196,6 +198,7 @@ namespace OpenIddict.Validation.Owin public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() .AddFilter() + .AddFilter() .UseSingletonHandler() .SetOrder(ExtractAccessTokenFromAuthorizationHeader.Descriptor.Order + 1_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) @@ -210,7 +213,7 @@ namespace OpenIddict.Validation.Owin } // If a token was already resolved, don't overwrite it. - if (!string.IsNullOrEmpty(context.Token)) + if (!string.IsNullOrEmpty(context.AccessToken)) { return; } @@ -235,8 +238,7 @@ namespace OpenIddict.Validation.Owin string token = form[Parameters.AccessToken]; if (!string.IsNullOrEmpty(token)) { - context.Token = token; - context.TokenType = TokenTypeHints.AccessToken; + context.AccessToken = token; return; } @@ -255,6 +257,7 @@ namespace OpenIddict.Validation.Owin public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() .AddFilter() + .AddFilter() .UseSingletonHandler() .SetOrder(ExtractAccessTokenFromBodyForm.Descriptor.Order + 1_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) @@ -269,7 +272,7 @@ namespace OpenIddict.Validation.Owin } // If a token was already resolved, don't overwrite it. - if (!string.IsNullOrEmpty(context.Token)) + if (!string.IsNullOrEmpty(context.AccessToken)) { return default; } @@ -287,8 +290,7 @@ namespace OpenIddict.Validation.Owin string token = request.Query[Parameters.AccessToken]; if (!string.IsNullOrEmpty(token)) { - context.Token = token; - context.TokenType = TokenTypeHints.AccessToken; + context.AccessToken = token; return default; } diff --git a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinProperties.cs b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinProperties.cs new file mode 100644 index 00000000..6655081e --- /dev/null +++ b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinProperties.cs @@ -0,0 +1,89 @@ +/* + * 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; +using System.Collections.Generic; +using Microsoft.Owin.Security; +using SR = OpenIddict.Abstractions.OpenIddictResources; + +namespace OpenIddict.Validation.Owin +{ + /// + public class OpenIddictValidationOwinProperties : AuthenticationProperties + { + /// + public OpenIddictValidationOwinProperties() + : this(items: null) + { + } + + /// + public OpenIddictValidationOwinProperties(IDictionary? items) + : this(items, parameters: null) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// State values dictionary to use. + /// Parameters dictionary to use. + public OpenIddictValidationOwinProperties( + IDictionary? items, + IDictionary? parameters) + : base(items) + => Parameters = parameters ?? new Dictionary(StringComparer.Ordinal); + + /// + /// Gets the collection of parameters passed to the authentication handler. + /// + /// + /// Note: these properties are not intended for serialization or persistence, + /// only for flowing data between call sites. + /// + public IDictionary Parameters { get; } + + /// + /// Gets a parameter from the collection. + /// + /// The parameter type. + /// The parameter name. + /// The parameter value or a default value if the property is not set. + public T? GetParameter(string name) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException(SR.ID0190, nameof(name)); + } + + return Parameters.TryGetValue(name, out var parameter) && parameter is T value ? value : default; + } + + /// + /// Sets a parameter value in the collection. + /// + /// The parameter type. + /// The parameter key. + /// The value to set. + public void SetParameter(string name, T? value) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException(SR.ID0190, nameof(name)); + } + + if (value is null) + { + Parameters.Remove(name); + } + + else + { + Parameters[name] = value; + } + } + } +} diff --git a/src/OpenIddict.Validation/OpenIddictValidationConstants.cs b/src/OpenIddict.Validation/OpenIddictValidationConstants.cs deleted file mode 100644 index 5d3ef499..00000000 --- a/src/OpenIddict.Validation/OpenIddictValidationConstants.cs +++ /dev/null @@ -1,16 +0,0 @@ -/* - * 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. - */ - -namespace OpenIddict.Validation -{ - public static class OpenIddictValidationConstants - { - public static class Properties - { - public const string ReferenceTokenIdentifier = ".reference_token_identifier"; - } - } -} diff --git a/src/OpenIddict.Validation/OpenIddictValidationEvents.Discovery.cs b/src/OpenIddict.Validation/OpenIddictValidationEvents.Discovery.cs index 2a551b46..bd453e6b 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationEvents.Discovery.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationEvents.Discovery.cs @@ -94,7 +94,7 @@ namespace OpenIddict.Validation } /// - /// Represents an event called for each validated configuration response. + /// Represents an event called for each configuration response. /// public class HandleConfigurationResponseContext : BaseExternalContext { @@ -212,7 +212,7 @@ namespace OpenIddict.Validation } /// - /// Represents an event called for each validated cryptography response. + /// Represents an event called for each cryptography response. /// public class HandleCryptographyResponseContext : BaseExternalContext { diff --git a/src/OpenIddict.Validation/OpenIddictValidationEvents.Introspection.cs b/src/OpenIddict.Validation/OpenIddictValidationEvents.Introspection.cs index 6bfcbeb3..cf678280 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationEvents.Introspection.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationEvents.Introspection.cs @@ -42,7 +42,7 @@ namespace OpenIddict.Validation /// /// Gets or sets the token type sent to the introspection endpoint. /// - public string? TokenType { get; set; } + public string? TokenTypeHint { get; set; } } /// @@ -103,7 +103,7 @@ namespace OpenIddict.Validation } /// - /// Represents an event called for each validated introspection response. + /// Represents an event called for each introspection response. /// public class HandleIntrospectionResponseContext : BaseExternalContext { @@ -138,11 +138,6 @@ namespace OpenIddict.Validation /// public string? Token { get; set; } - /// - /// Gets or sets the token type sent to the introspection endpoint. - /// - public string? TokenType { get; set; } - /// /// Gets or sets the principal containing the claims resolved from the introspection response. /// diff --git a/src/OpenIddict.Validation/OpenIddictValidationEvents.Protection.cs b/src/OpenIddict.Validation/OpenIddictValidationEvents.Protection.cs new file mode 100644 index 00000000..a0b8afd0 --- /dev/null +++ b/src/OpenIddict.Validation/OpenIddictValidationEvents.Protection.cs @@ -0,0 +1,72 @@ +/* + * 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; +using System.Collections.Generic; +using System.Security.Claims; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Tokens; +using OpenIddict.Abstractions; + +namespace OpenIddict.Validation +{ + public static partial class OpenIddictValidationEvents + { + /// + /// Represents an event called when validating a token. + /// + public class ValidateTokenContext : BaseValidatingContext + { + /// + /// Creates a new instance of the class. + /// + public ValidateTokenContext(OpenIddictValidationTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets or sets the request, or null if it is not available. + /// + public OpenIddictRequest? Request + { + get => Transaction.Request; + set => Transaction.Request = value; + } + + /// + /// Gets or sets the security token handler used to validate the token. + /// + public JsonWebTokenHandler SecurityTokenHandler { get; set; } = default!; + + /// + /// Gets or sets the validation parameters used to verify the authenticity of tokens. + /// + public TokenValidationParameters TokenValidationParameters { get; set; } = default!; + + /// + /// Gets or sets the token to validate. + /// + public string Token { get; set; } = default!; + + /// + /// Gets or sets the token entry identifier associated with the token, if applicable. + /// + public string? TokenId { get; set; } + + /// + /// Gets or sets the security principal resolved from the token. + /// + public ClaimsPrincipal? Principal { get; set; } + + /// + /// Gets the token types that are considered valid. If no value is + /// explicitly specified, all supported tokens are considered valid. + /// + public HashSet ValidTokenTypes { get; } = new(StringComparer.OrdinalIgnoreCase); + } + } +} diff --git a/src/OpenIddict.Validation/OpenIddictValidationEvents.cs b/src/OpenIddict.Validation/OpenIddictValidationEvents.cs index 773cd476..c671176a 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationEvents.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationEvents.cs @@ -238,19 +238,30 @@ namespace OpenIddict.Validation } /// - /// Gets or sets the security principal. + /// Gets or sets the principal extracted from the access token, if applicable. /// - public ClaimsPrincipal? Principal { get; set; } + public ClaimsPrincipal? AccessTokenPrincipal { get; set; } /// - /// Gets or sets the token to validate. + /// Gets or sets the access token to validate, if applicable. /// - public string? Token { get; set; } + public string? AccessToken { get; set; } /// - /// Gets or sets the expected type of the token. + /// Gets or sets a boolean indicating whether an access token + /// must be resolved for the authentication to considered valid. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. /// - public string? TokenType { get; set; } + public bool RequireAccessToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether an access token + /// should be extracted from the current context and validated. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool ValidateAccessToken { get; set; } } /// diff --git a/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs b/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs index eda34cc0..259a18f0 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs @@ -45,6 +45,7 @@ namespace Microsoft.Extensions.DependencyInjection builder.Services.TryAdd(DefaultHandlers.Select(descriptor => descriptor.ServiceDescriptor)); // Register the built-in filters used by the default OpenIddict validation event handlers. + builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs index 65bb369d..5432e2f8 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs @@ -14,6 +14,22 @@ namespace OpenIddict.Validation [EditorBrowsable(EditorBrowsableState.Advanced)] public static class OpenIddictValidationHandlerFilters { + /// + /// Represents a filter that excludes the associated handlers if no access token is validated. + /// + public class RequireAccessTokenValidated : IOpenIddictValidationHandlerFilter + { + public ValueTask IsActiveAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new ValueTask(context.ValidateAccessToken); + } + } + /// /// Represents a filter that excludes the associated handlers if authorization validation was not enabled. /// diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs index 311eacd8..c992aa7d 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs @@ -36,7 +36,7 @@ namespace OpenIddict.Validation HandleInactiveResponse.Descriptor, ValidateWellKnownClaims.Descriptor, ValidateIssuer.Descriptor, - ValidateTokenType.Descriptor, + ValidateTokenUsage.Descriptor, PopulateClaims.Descriptor); /// @@ -93,7 +93,7 @@ namespace OpenIddict.Validation } context.Request.Token = context.Token; - context.Request.TokenTypeHint = context.TokenType; + context.Request.TokenTypeHint = context.TokenTypeHint; return default; } @@ -283,16 +283,16 @@ namespace OpenIddict.Validation } /// - /// Contains the logic responsible of extracting and validating the token type from the introspection response. + /// Contains the logic responsible of extracting and validating the token usage from the introspection response. /// - public class ValidateTokenType : IOpenIddictValidationHandler + public class ValidateTokenUsage : IOpenIddictValidationHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() + .UseSingletonHandler() .SetOrder(ValidateIssuer.Descriptor.Order + 1_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); @@ -308,19 +308,33 @@ namespace OpenIddict.Validation // 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. - if (!string.IsNullOrEmpty(context.TokenType)) + // 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)) { - var usage = (string?) context.Response[Claims.TokenUsage]; - if (!string.IsNullOrEmpty(usage) && - !string.Equals(usage, context.TokenType, StringComparison.OrdinalIgnoreCase)) - { - context.Reject( - error: Errors.InvalidToken, - description: SR.GetResourceString(SR.ID2110), - uri: SR.FormatID8000(SR.ID2110)); + return default; + } - return default; - } + if (!(usage switch + { + // Note: by default, OpenIddict only allows access/refresh tokens to be + // introspected but additional types can be added using the events model. + TokenTypeHints.AccessToken or TokenTypeHints.AuthorizationCode or + TokenTypeHints.IdToken or TokenTypeHints.RefreshToken or + TokenTypeHints.UserCode + => true, + + _ => false // Other token usages are not supported. + })) + { + context.Reject( + error: Errors.ServerError, + description: SR.GetResourceString(SR.ID2118), + uri: SR.FormatID8000(SR.ID2118)); + + return default; } return default; @@ -338,7 +352,7 @@ namespace OpenIddict.Validation public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() .UseSingletonHandler() - .SetOrder(ValidateTokenType.Descriptor.Order + 1_000) + .SetOrder(ValidateTokenUsage.Descriptor.Order + 1_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); @@ -378,8 +392,7 @@ namespace OpenIddict.Validation } // 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 or Claims.TokenUsage) + if (parameter.Key is Claims.Active or Claims.Issuer or Claims.NotBefore or Claims.TokenType) { continue; } diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs new file mode 100644 index 00000000..e2c461ee --- /dev/null +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs @@ -0,0 +1,864 @@ +/* + * 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; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.Tokens; +using OpenIddict.Abstractions; +using static OpenIddict.Abstractions.OpenIddictConstants; +using static OpenIddict.Validation.OpenIddictValidationEvents; +using static OpenIddict.Validation.OpenIddictValidationHandlerFilters; +using SR = OpenIddict.Abstractions.OpenIddictResources; + +namespace OpenIddict.Validation +{ + public static partial class OpenIddictValidationHandlers + { + public static class Protection + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Token validation: + */ + ResolveTokenValidationParameters.Descriptor, + ValidateReferenceTokenIdentifier.Descriptor, + ValidateIdentityModelToken.Descriptor, + IntrospectToken.Descriptor, + NormalizeScopeClaims.Descriptor, + MapInternalClaims.Descriptor, + RestoreReferenceTokenProperties.Descriptor, + ValidatePrincipal.Descriptor, + ValidateExpirationDate.Descriptor, + ValidateAudience.Descriptor, + ValidateTokenEntry.Descriptor, + ValidateAuthorizationEntry.Descriptor); + + /// + /// Contains the logic responsible of resolving the validation parameters used to validate tokens. + /// + public class ResolveTokenValidationParameters : IOpenIddictValidationHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .SetType(OpenIddictValidationHandlerType.BuiltIn) + .Build(); + + /// + public async ValueTask HandleAsync(ValidateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + var configuration = await context.Options.ConfigurationManager.GetConfigurationAsync(default) ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); + + // Clone the token validation parameters and set the issuer using the value found in the + // OpenID Connect server configuration (that can be static or retrieved using discovery). + var parameters = context.Options.TokenValidationParameters.Clone(); + parameters.ValidIssuer ??= configuration.Issuer ?? context.Issuer?.AbsoluteUri; + parameters.ValidateIssuer = !string.IsNullOrEmpty(parameters.ValidIssuer); + + // Combine the signing keys registered statically in the token validation parameters + // with the signing keys resolved from the OpenID Connect server configuration. + parameters.IssuerSigningKeys = + parameters.IssuerSigningKeys?.Concat(configuration.SigningKeys) ?? configuration.SigningKeys; + + parameters.ValidTypes = context.ValidTokenTypes.Count switch + { + // If no specific token type is expected, accept all token types at this stage. + // Additional filtering can be made based on the resolved/actual token type. + 0 => null, + + // Otherwise, map the token types to their JWT public or internal representation. + _ => context.ValidTokenTypes.SelectMany(type => type switch + { + // For access tokens, both "at+jwt" and "application/at+jwt" are valid. + TokenTypeHints.AccessToken => new[] + { + JsonWebTokenTypes.AccessToken, + JsonWebTokenTypes.Prefixes.Application + JsonWebTokenTypes.AccessToken + }, + + _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003)) + }) + }; + + context.SecurityTokenHandler = context.Options.JsonWebTokenHandler; + context.TokenValidationParameters = parameters; + } + } + + /// + /// Contains the logic responsible of validating reference token identifiers. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class ValidateReferenceTokenIdentifier : IOpenIddictValidationHandler + { + private readonly IOpenIddictTokenManager _tokenManager; + + public ValidateReferenceTokenIdentifier() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0139)); + + public ValidateReferenceTokenIdentifier(IOpenIddictTokenManager tokenManager) + => _tokenManager = tokenManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(ResolveTokenValidationParameters.Descriptor.Order + 1_000) + .SetType(OpenIddictValidationHandlerType.BuiltIn) + .Build(); + + /// + public async ValueTask HandleAsync(ValidateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Reference tokens are base64url-encoded payloads of exactly 256 bits (generated using a + // crypto-secure RNG). If the token length differs, the token cannot be a reference token. + if (context.Token.Length != 43) + { + return; + } + + // If the reference token cannot be found, don't return an error to allow another handler to validate it. + var token = await _tokenManager.FindByReferenceIdAsync(context.Token); + if (token is null) + { + return; + } + + // If the type associated with the token entry doesn't match one of the expected types, return an error. + if (context.ValidTokenTypes.Count > 0 && + !await _tokenManager.HasTypeAsync(token, context.ValidTokenTypes.ToImmutableArray())) + { + context.Reject( + error: Errors.InvalidToken, + description: SR.GetResourceString(SR.ID2004), + uri: SR.FormatID8000(SR.ID2004)); + + return; + } + + var payload = await _tokenManager.GetPayloadAsync(token); + if (string.IsNullOrEmpty(payload)) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0026)); + } + + // Replace the token parameter by the payload resolved from the token entry + // and store the identifier of the reference token so it can be later + // used to restore the properties associated with the token. + context.Token = payload; + context.TokenId = await _tokenManager.GetIdAsync(token); + } + } + + /// + /// Contains the logic responsible of validating tokens generated using IdentityModel. + /// + public class ValidateIdentityModelToken : IOpenIddictValidationHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateReferenceTokenIdentifier.Descriptor.Order + 1_000) + .SetType(OpenIddictValidationHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ValidateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // If a principal was already attached, don't overwrite it. + if (context.Principal is not null) + { + return default; + } + + // If the token cannot be read, don't return an error to allow another handler to validate it. + if (!context.SecurityTokenHandler.CanReadToken(context.Token)) + { + return default; + } + + var result = context.SecurityTokenHandler.ValidateToken(context.Token, context.TokenValidationParameters); + if (!result.IsValid) + { + // If validation failed because of an unrecognized key identifier, inform the configuration manager + // that the configuration MAY have be refreshed by sending a new discovery request to the server. + if (result.Exception is SecurityTokenSignatureKeyNotFoundException) + { + context.Options.ConfigurationManager.RequestRefresh(); + } + + context.Logger.LogTrace(result.Exception, SR.GetResourceString(SR.ID6000), context.Token); + + context.Reject( + error: Errors.InvalidToken, + description: result.Exception switch + { + SecurityTokenInvalidIssuerException => SR.GetResourceString(SR.ID2088), + SecurityTokenInvalidTypeException => SR.GetResourceString(SR.ID2089), + SecurityTokenSignatureKeyNotFoundException => SR.GetResourceString(SR.ID2090), + SecurityTokenInvalidSignatureException => SR.GetResourceString(SR.ID2091), + + _ => SR.GetResourceString(SR.ID2004) + }, + uri: result.Exception switch + { + SecurityTokenInvalidIssuerException => SR.FormatID8000(SR.ID2088), + SecurityTokenInvalidTypeException => SR.FormatID8000(SR.ID2089), + SecurityTokenSignatureKeyNotFoundException => SR.FormatID8000(SR.ID2090), + SecurityTokenInvalidSignatureException => SR.FormatID8000(SR.ID2091), + + _ => SR.FormatID8000(SR.ID2004) + }); + + return default; + } + + // Attach the principal extracted from the token to the parent event context and store + // the token type (resolved from "typ" or "token_usage") as a special private claim. + context.Principal = new ClaimsPrincipal(result.ClaimsIdentity).SetTokenType(result.TokenType switch + { + null or { Length: 0 } => throw new InvalidOperationException(SR.GetResourceString(SR.ID0025)), + + // Both at+jwt and application/at+jwt are supported for access tokens. + JsonWebTokenTypes.AccessToken or JsonWebTokenTypes.Prefixes.Application + JsonWebTokenTypes.AccessToken + => TokenTypeHints.AccessToken, + + _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003)) + }); + + context.Logger.LogTrace(SR.GetResourceString(SR.ID6001), context.Token, context.Principal.Claims); + + return default; + } + } + + /// + /// Contains the logic responsible of validating the tokens using OAuth 2.0 introspection. + /// + public class IntrospectToken : IOpenIddictValidationHandler + { + private readonly OpenIddictValidationService _service; + + public IntrospectToken(OpenIddictValidationService service) + => _service = service; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateIdentityModelToken.Descriptor.Order + 1_000) + .SetType(OpenIddictValidationHandlerType.BuiltIn) + .Build(); + + /// + public async ValueTask HandleAsync(ValidateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // If a principal was already attached, don't overwrite it. + if (context.Principal is not null) + { + return; + } + + Debug.Assert(!string.IsNullOrEmpty(context.Token), SR.GetResourceString(SR.ID4010)); + + var configuration = await context.Options.ConfigurationManager.GetConfigurationAsync(default) ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); + + if (string.IsNullOrEmpty(configuration.IntrospectionEndpoint) || + !Uri.TryCreate(configuration.IntrospectionEndpoint, UriKind.Absolute, out Uri? address) || + !address.IsWellFormedOriginalString()) + { + context.Reject( + error: Errors.ServerError, + description: SR.GetResourceString(SR.ID2092), + uri: SR.FormatID8000(SR.ID2092)); + + return; + } + + ClaimsPrincipal principal; + + try + { + principal = await _service.IntrospectTokenAsync(address, context.Token, context.ValidTokenTypes.Count switch + { + // Infer the token type hint sent to the authorization server to help speed up + // the token resolution lookup. If multiple types are accepted, no hint is sent. + 1 => context.ValidTokenTypes.ElementAt(0), + _ => null + }) ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0141)); + } + + catch (Exception exception) + { + context.Logger.LogDebug(exception, SR.GetResourceString(SR.ID6155)); + + context.Reject( + error: Errors.InvalidToken, + description: SR.GetResourceString(SR.ID2004), + uri: SR.FormatID8000(SR.ID2004)); + + return; + } + + // 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 a + // token whose type is considered acceptable based on the valid types collection. + // + // If the valid types collection is empty, all types of tokens are considered valid. + var usage = principal.GetClaim(Claims.TokenUsage); + if (!string.IsNullOrEmpty(usage) && context.ValidTokenTypes.Count > 0 && + !context.ValidTokenTypes.Contains(usage)) + { + context.Reject( + error: Errors.InvalidToken, + description: SR.GetResourceString(SR.ID2110), + uri: SR.FormatID8000(SR.ID2110)); + + return; + } + + // Note: at this point, the "token_usage" claim value is guaranteed to correspond + // to a known value as it is checked when validating the introspection response. + // + // If no value could be resolved, the token is assumed to be an access token. + context.Principal = principal.SetTokenType(usage ?? TokenTypeHints.AccessToken); + + context.Logger.LogTrace(SR.GetResourceString(SR.ID6154), context.Token, context.Principal.Claims); + } + } + + /// + /// Contains the logic responsible of normalizing the scope claims stored in the tokens. + /// + public class NormalizeScopeClaims : IOpenIddictValidationHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(IntrospectToken.Descriptor.Order + 1_000) + .SetType(OpenIddictValidationHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ValidateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.Principal is null) + { + return default; + } + + // Note: in previous OpenIddict versions, scopes were represented as a JSON array + // and deserialized as multiple claims. In OpenIddict 3.0, the public "scope" claim + // is formatted as a unique space-separated string containing all the granted scopes. + // To ensure access tokens generated by previous versions are still correctly handled, + // both formats (unique space-separated string or multiple scope claims) must be supported. + // To achieve that, all the "scope" claims are combined into a single one containg all the values. + // Visit https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-04 for more information. + var scopes = context.Principal.GetClaims(Claims.Scope); + if (scopes.Length > 1) + { + context.Principal.SetClaim(Claims.Scope, string.Join(" ", scopes)); + } + + return default; + } + } + + /// + /// Contains the logic responsible of mapping internal claims used by OpenIddict. + /// + public class MapInternalClaims : IOpenIddictValidationHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(NormalizeScopeClaims.Descriptor.Order + 1_000) + .SetType(OpenIddictValidationHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ValidateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.Principal is null) + { + return default; + } + + // To reduce the size of tokens, some of the private claims used by OpenIddict + // are mapped to their standard equivalent before being removed from the token. + // This handler is responsible of adding back the private claims to the principal + // when receiving the token (e.g "oi_prst" is resolved from the "scope" claim). + + // In OpenIddict 3.0, the creation date of a token is stored in "oi_crt_dt". + // If the claim doesn't exist, try to infer it from the standard "iat" JWT claim. + if (!context.Principal.HasClaim(Claims.Private.CreationDate)) + { + var date = context.Principal.GetClaim(Claims.IssuedAt); + if (!string.IsNullOrEmpty(date) && + long.TryParse(date, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) + { + context.Principal.SetCreationDate(DateTimeOffset.FromUnixTimeSeconds(value)); + } + } + + // In OpenIddict 3.0, the expiration date of a token is stored in "oi_exp_dt". + // If the claim doesn't exist, try to infer it from the standard "exp" JWT claim. + if (!context.Principal.HasClaim(Claims.Private.ExpirationDate)) + { + var date = context.Principal.GetClaim(Claims.ExpiresAt); + if (!string.IsNullOrEmpty(date) && + long.TryParse(date, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) + { + context.Principal.SetExpirationDate(DateTimeOffset.FromUnixTimeSeconds(value)); + } + } + + // In OpenIddict 3.0, the audiences allowed to receive a token are stored in "oi_aud". + // If no such claim exists, try to infer them from the standard "aud" JWT claims. + if (!context.Principal.HasClaim(Claims.Private.Audience)) + { + var audiences = context.Principal.GetClaims(Claims.Audience); + if (audiences.Any()) + { + context.Principal.SetAudiences(audiences); + } + } + + // In OpenIddict 3.0, the presenters allowed to use a token are stored in "oi_prst". + // If no such claim exists, try to infer them from the standard "azp" and "client_id" JWT claims. + // + // Note: in previous OpenIddict versions, the presenters were represented in JWT tokens + // using the "azp" claim (defined by OpenID Connect), for which a single value could be + // specified. To ensure presenters stored in JWT tokens created by OpenIddict 1.x/2.x + // can still be read with OpenIddict 3.0, the presenter is automatically inferred from + // the "azp" or "client_id" claim if no "oi_prst" claim was found in the principal. + if (!context.Principal.HasClaim(Claims.Private.Presenter)) + { + var presenter = context.Principal.GetClaim(Claims.AuthorizedParty) ?? + context.Principal.GetClaim(Claims.ClientId); + + if (!string.IsNullOrEmpty(presenter)) + { + context.Principal.SetPresenters(presenter); + } + } + + // In OpenIddict 3.0, the scopes granted to an application are stored in "oi_scp". + // If no such claim exists, try to infer them from the standard "scope" JWT claim, + // which is guaranteed to be a unique space-separated claim containing all the values. + if (!context.Principal.HasClaim(Claims.Private.Scope)) + { + var scope = context.Principal.GetClaim(Claims.Scope); + if (!string.IsNullOrEmpty(scope)) + { + context.Principal.SetScopes(scope.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries)); + } + } + + return default; + } + } + + /// + /// Contains the logic responsible of restoring the properties associated with a reference token entry. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class RestoreReferenceTokenProperties : IOpenIddictValidationHandler + { + private readonly IOpenIddictTokenManager _tokenManager; + + public RestoreReferenceTokenProperties() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0139)); + + public RestoreReferenceTokenProperties(IOpenIddictTokenManager tokenManager) + => _tokenManager = tokenManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(MapInternalClaims.Descriptor.Order + 1_000) + .SetType(OpenIddictValidationHandlerType.BuiltIn) + .Build(); + + /// + public async ValueTask HandleAsync(ValidateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.Principal is null || string.IsNullOrEmpty(context.TokenId)) + { + return; + } + + var token = await _tokenManager.FindByIdAsync(context.TokenId); + if (token is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0021)); + } + + // Restore the creation/expiration dates/identifiers from the token entry metadata. + context.Principal.SetCreationDate(await _tokenManager.GetCreationDateAsync(token)) + .SetExpirationDate(await _tokenManager.GetExpirationDateAsync(token)) + .SetAuthorizationId(await _tokenManager.GetAuthorizationIdAsync(token)) + .SetTokenId(await _tokenManager.GetIdAsync(token)) + .SetTokenType(await _tokenManager.GetTypeAsync(token)); + } + } + + /// + /// Contains the logic responsible of rejecting authentication demands for which no valid principal was resolved. + /// + public class ValidatePrincipal : IOpenIddictValidationHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(RestoreReferenceTokenProperties.Descriptor.Order + 1_000) + .SetType(OpenIddictValidationHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ValidateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.Principal is null) + { + context.Reject( + error: Errors.InvalidToken, + description: SR.GetResourceString(SR.ID2004), + uri: SR.FormatID8000(SR.ID2004)); + + return default; + } + + // When using JWT or Data Protection tokens, the correct token type is always enforced by IdentityModel + // (using the "typ" header) or by ASP.NET Core Data Protection (using per-token-type purposes strings). + // To ensure tokens deserialized using a custom routine are of the expected type, a manual check is used, + // which requires that a special claim containing the token type be present in the security principal. + if (context.ValidTokenTypes.Count > 0) + { + var type = context.Principal.GetTokenType(); + if (string.IsNullOrEmpty(type)) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0004)); + } + + if (!context.ValidTokenTypes.Contains(type)) + { + throw new InvalidOperationException(SR.FormatID0005(type, string.Join(", ", context.ValidTokenTypes))); + } + } + + return default; + } + } + + /// + /// Contains the logic responsible of rejecting authentication demands containing expired access tokens. + /// + public class ValidateExpirationDate : IOpenIddictValidationHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidatePrincipal.Descriptor.Order + 1_000) + .SetType(OpenIddictValidationHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ValidateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + + var date = context.Principal.GetExpirationDate(); + if (date.HasValue && date.Value < DateTimeOffset.UtcNow) + { + context.Logger.LogInformation(SR.GetResourceString(SR.ID6156)); + + context.Reject( + error: Errors.InvalidToken, + description: SR.GetResourceString(SR.ID2019), + uri: SR.FormatID8000(SR.ID2019)); + + return default; + } + + return default; + } + } + + /// + /// Contains the logic responsible of rejecting authentication demands containing + /// access tokens that were issued to be used by another audience/resource server. + /// + public class ValidateAudience : IOpenIddictValidationHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateExpirationDate.Descriptor.Order + 1_000) + .SetType(OpenIddictValidationHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ValidateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + + // If no explicit audience has been configured, + // skip the default audience validation. + if (context.Options.Audiences.Count == 0) + { + return default; + } + + // If the access token doesn't have any audience attached, return an error. + var audiences = context.Principal.GetAudiences(); + if (audiences.IsDefaultOrEmpty) + { + context.Logger.LogInformation(SR.GetResourceString(SR.ID6157)); + + context.Reject( + error: Errors.InvalidToken, + description: SR.GetResourceString(SR.ID2093), + uri: SR.FormatID8000(SR.ID2093)); + + return default; + } + + // If the access token doesn't include any registered audience, return an error. + if (!audiences.Intersect(context.Options.Audiences, StringComparer.Ordinal).Any()) + { + context.Logger.LogInformation(SR.GetResourceString(SR.ID6158)); + + context.Reject( + error: Errors.InvalidToken, + description: SR.GetResourceString(SR.ID2094), + uri: SR.FormatID8000(SR.ID2094)); + + return default; + } + + return default; + } + } + + /// + /// Contains the logic responsible of authentication demands a token whose + /// associated token entry is no longer valid (e.g was revoked). + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class ValidateTokenEntry : IOpenIddictValidationHandler + { + private readonly IOpenIddictTokenManager _tokenManager; + + public ValidateTokenEntry() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0139)); + + public ValidateTokenEntry(IOpenIddictTokenManager tokenManager) + => _tokenManager = tokenManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateAudience.Descriptor.Order + 1_000) + .SetType(OpenIddictValidationHandlerType.BuiltIn) + .Build(); + + /// + public async ValueTask HandleAsync(ValidateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + + var identifier = context.Principal.GetTokenId(); + if (string.IsNullOrEmpty(identifier)) + { + return; + } + + var token = await _tokenManager.FindByIdAsync(identifier); + if (token is null || !await _tokenManager.HasStatusAsync(token, Statuses.Valid)) + { + context.Logger.LogInformation(SR.GetResourceString(SR.ID6005), identifier); + + context.Reject( + error: Errors.InvalidToken, + description: SR.GetResourceString(SR.ID2019), + uri: SR.FormatID8000(SR.ID2019)); + + return; + } + + // Restore the creation/expiration dates/identifiers from the token entry metadata. + context.Principal.SetCreationDate(await _tokenManager.GetCreationDateAsync(token)) + .SetExpirationDate(await _tokenManager.GetExpirationDateAsync(token)) + .SetAuthorizationId(await _tokenManager.GetAuthorizationIdAsync(token)) + .SetTokenId(await _tokenManager.GetIdAsync(token)) + .SetTokenType(await _tokenManager.GetTypeAsync(token)); + } + } + + /// + /// Contains the logic responsible of authentication demands a token whose + /// associated authorization entry is no longer valid (e.g was revoked). + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class ValidateAuthorizationEntry : IOpenIddictValidationHandler + { + private readonly IOpenIddictAuthorizationManager _authorizationManager; + + public ValidateAuthorizationEntry() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0142)); + + public ValidateAuthorizationEntry(IOpenIddictAuthorizationManager authorizationManager) + => _authorizationManager = authorizationManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateTokenEntry.Descriptor.Order + 1_000) + .SetType(OpenIddictValidationHandlerType.BuiltIn) + .Build(); + + /// + public async ValueTask HandleAsync(ValidateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + + var identifier = context.Principal.GetAuthorizationId(); + if (string.IsNullOrEmpty(identifier)) + { + return; + } + + var authorization = await _authorizationManager.FindByIdAsync(identifier); + if (authorization is null || !await _authorizationManager.HasStatusAsync(authorization, Statuses.Valid)) + { + context.Logger.LogInformation(SR.GetResourceString(SR.ID6006), identifier); + + context.Reject( + error: Errors.InvalidToken, + description: SR.GetResourceString(SR.ID2023), + uri: SR.FormatID8000(SR.ID2023)); + + return; + } + } + } + } + } +} diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs index 94ec28ea..8435e8c4 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs @@ -7,18 +7,10 @@ using System; using System.Collections.Immutable; using System.ComponentModel; -using System.Diagnostics; -using System.Globalization; -using System.Linq; -using System.Security.Claims; using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.IdentityModel.Tokens; -using OpenIddict.Abstractions; using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Validation.OpenIddictValidationEvents; using static OpenIddict.Validation.OpenIddictValidationHandlerFilters; -using Properties = OpenIddict.Validation.OpenIddictValidationConstants.Properties; using SR = OpenIddict.Abstractions.OpenIddictResources; namespace OpenIddict.Validation @@ -30,18 +22,8 @@ namespace OpenIddict.Validation /* * Authentication processing: */ - ValidateToken.Descriptor, - ValidateReferenceTokenIdentifier.Descriptor, - ValidateIdentityModelToken.Descriptor, - IntrospectToken.Descriptor, - NormalizeScopeClaims.Descriptor, - MapInternalClaims.Descriptor, - RestoreReferenceTokenProperties.Descriptor, - ValidatePrincipal.Descriptor, - ValidateExpirationDate.Descriptor, - ValidateAudience.Descriptor, - ValidateTokenEntry.Descriptor, - ValidateAuthorizationEntry.Descriptor, + EvaluateValidatedTokens.Descriptor, + ValidateAccessToken.Descriptor, /* * Challenge processing: @@ -49,19 +31,20 @@ namespace OpenIddict.Validation AttachDefaultChallengeError.Descriptor) .AddRange(Discovery.DefaultHandlers) - .AddRange(Introspection.DefaultHandlers); + .AddRange(Introspection.DefaultHandlers) + .AddRange(Protection.DefaultHandlers); /// - /// Contains the logic responsible of ensuring a token was correctly resolved from the context. + /// Contains the logic responsible of selecting the token types that should be validated. /// - public class ValidateToken : IOpenIddictValidationHandler + public class EvaluateValidatedTokens : IOpenIddictValidationHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() + .UseSingletonHandler() .SetOrder(int.MinValue + 100_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); @@ -74,113 +57,46 @@ namespace OpenIddict.Validation throw new ArgumentNullException(nameof(context)); } + (context.ValidateAccessToken, context.RequireAccessToken) = context.EndpointType switch + { + // The validation handler is responsible of validating access tokens for endpoints + // it doesn't manage (typically, API endpoints using token authentication). + // + // As such, sending an access token is not mandatory: API endpoints that require + // authentication can set up an authorization policy to reject such requests later + // in the request processing pipeline (typically, via the authorization middleware). + OpenIddictValidationEndpointType.Unknown => (true, false), + + _ => (false, false) + }; + // Note: unlike the equivalent event in the server stack, authentication can be triggered for // arbitrary requests (typically, API endpoints that are not owned by the validation stack). // As such, the token is not directly resolved from the request, that may be null at this stage. // Instead, the token is expected to be populated by one or multiple handlers provided by the host. - // - // Note: this event can also be triggered by the validation service to validate an arbitrary token. - - if (string.IsNullOrEmpty(context.Token)) - { - context.Reject( - error: Errors.MissingToken, - description: SR.GetResourceString(SR.ID2000), - uri: SR.FormatID8000(SR.ID2000)); - - return default; - } return default; } } /// - /// Contains the logic responsible of validating reference token identifiers. - /// Note: this handler is not used when the degraded mode is enabled. + /// Contains the logic responsible of ensuring a token was correctly resolved from the context. /// - public class ValidateReferenceTokenIdentifier : IOpenIddictValidationHandler + public class ValidateAccessToken : IOpenIddictValidationHandler { - private readonly IOpenIddictTokenManager _tokenManager; - - public ValidateReferenceTokenIdentifier() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0139)); + private readonly IOpenIddictValidationDispatcher _dispatcher; - public ValidateReferenceTokenIdentifier(IOpenIddictTokenManager tokenManager) - => _tokenManager = tokenManager; + public ValidateAccessToken(IOpenIddictValidationDispatcher dispatcher) + => _dispatcher = dispatcher; /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .UseScopedHandler() - .SetOrder(ValidateToken.Descriptor.Order + 1_000) - .SetType(OpenIddictValidationHandlerType.BuiltIn) - .Build(); - - /// - public async ValueTask HandleAsync(ProcessAuthenticationContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - // Reference tokens are base64url-encoded payloads of exactly 256 bits (generated using a - // crypto-secure RNG). If the token length differs, the token cannot be a reference token. - if (string.IsNullOrEmpty(context.Token) || context.Token.Length != 43) - { - return; - } - - // If the reference token cannot be found, don't return an error to allow another handler to validate it. - var token = await _tokenManager.FindByReferenceIdAsync(context.Token); - if (token is null) - { - return; - } - - // If the type associated with the token entry doesn't match the expected type, return an error. - if (!string.IsNullOrEmpty(context.TokenType) && !await _tokenManager.HasTypeAsync(token, context.TokenType)) - { - context.Reject( - error: Errors.InvalidToken, - description: SR.GetResourceString(SR.ID2004), - uri: SR.FormatID8000(SR.ID2004)); - - return; - } - - var payload = await _tokenManager.GetPayloadAsync(token); - if (string.IsNullOrEmpty(payload)) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0026)); - } - - // Replace the token parameter by the payload resolved from the token entry. - context.Token = payload; - - // Store the identifier of the reference token in the transaction properties - // so it can be later used to restore the properties associated with the token. - context.Transaction.Properties[Properties.ReferenceTokenIdentifier] = await _tokenManager.GetIdAsync(token); - } - } - - /// - /// Contains the logic responsible of validating tokens generated using IdentityModel. - /// - public class ValidateIdentityModelToken : IOpenIddictValidationHandler - { - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictValidationHandlerDescriptor Descriptor { get; } - = OpenIddictValidationHandlerDescriptor.CreateBuilder() - .AddFilter() - .UseSingletonHandler() - .SetOrder(ValidateReferenceTokenIdentifier.Descriptor.Order + 1_000) + .AddFilter() + .UseScopedHandler() + .SetOrder(EvaluateValidatedTokens.Descriptor.Order + 1_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); @@ -192,670 +108,56 @@ namespace OpenIddict.Validation throw new ArgumentNullException(nameof(context)); } - // If a principal was already attached, don't overwrite it. - if (context.Principal is not null) + if (context.AccessTokenPrincipal is not null) { return; } - // If the token cannot be read, don't return an error to allow another handler to validate it. - if (!context.Options.JsonWebTokenHandler.CanReadToken(context.Token)) + if (string.IsNullOrEmpty(context.AccessToken)) { - return; - } - - var configuration = await context.Options.ConfigurationManager.GetConfigurationAsync(default) ?? - throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); - - // Clone the token validation parameters and set the issuer using the value found in the - // OpenID Connect server configuration (that can be static or retrieved using discovery). - var parameters = context.Options.TokenValidationParameters.Clone(); - parameters.ValidIssuer ??= configuration.Issuer ?? context.Issuer?.AbsoluteUri; - parameters.ValidateIssuer = !string.IsNullOrEmpty(parameters.ValidIssuer); - - // Combine the signing keys registered statically in the token validation parameters - // with the signing keys resolved from the OpenID Connect server configuration. - parameters.IssuerSigningKeys = - parameters.IssuerSigningKeys?.Concat(configuration.SigningKeys) ?? configuration.SigningKeys; - - parameters.ValidTypes = context.TokenType switch - { - // If no specific token type is expected, accept all token types at this stage. - // Additional filtering can be made based on the resolved/actual token type. - null or { Length: 0 } => null, - - // For access tokens, both "at+jwt" and "application/at+jwt" are valid. - TokenTypeHints.AccessToken => new[] + if (context.RequireAccessToken) { - JsonWebTokenTypes.AccessToken, - JsonWebTokenTypes.Prefixes.Application + JsonWebTokenTypes.AccessToken - }, + context.Reject( + error: Errors.MissingToken, + description: SR.GetResourceString(SR.ID2000), + uri: SR.FormatID8000(SR.ID2000)); - _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003)) - }; - - var result = context.Options.JsonWebTokenHandler.ValidateToken(context.Token, parameters); - if (!result.IsValid) - { - // If validation failed because of an unrecognized key identifier, inform the configuration manager - // that the configuration MAY have be refreshed by sending a new discovery request to the server. - if (result.Exception is SecurityTokenSignatureKeyNotFoundException) - { - context.Options.ConfigurationManager.RequestRefresh(); + return; } - context.Logger.LogTrace(result.Exception, SR.GetResourceString(SR.ID6000), context.Token); - - context.Reject( - error: Errors.InvalidToken, - description: result.Exception switch - { - SecurityTokenInvalidIssuerException => SR.GetResourceString(SR.ID2088), - SecurityTokenInvalidTypeException => SR.GetResourceString(SR.ID2089), - SecurityTokenSignatureKeyNotFoundException => SR.GetResourceString(SR.ID2090), - SecurityTokenInvalidSignatureException => SR.GetResourceString(SR.ID2091), - - _ => SR.GetResourceString(SR.ID2004) - }, - uri: result.Exception switch - { - SecurityTokenInvalidIssuerException => SR.FormatID8000(SR.ID2088), - SecurityTokenInvalidTypeException => SR.FormatID8000(SR.ID2089), - SecurityTokenSignatureKeyNotFoundException => SR.FormatID8000(SR.ID2090), - SecurityTokenInvalidSignatureException => SR.FormatID8000(SR.ID2091), - - _ => SR.FormatID8000(SR.ID2004) - }); - - return; - } - - // Attach the principal extracted from the token to the parent event context. - context.Principal = new ClaimsPrincipal(result.ClaimsIdentity); - - // Store the token type (resolved from "typ" or "token_usage") as a special private claim. - context.Principal.SetTokenType(result.TokenType switch - { - null or { Length: 0 } => throw new InvalidOperationException(SR.GetResourceString(SR.ID0025)), - - // Both at+jwt and application/at+jwt are supported for access tokens. - JsonWebTokenTypes.AccessToken or JsonWebTokenTypes.Prefixes.Application + JsonWebTokenTypes.AccessToken - => TokenTypeHints.AccessToken, - - _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003)) - }); - - context.Logger.LogTrace(SR.GetResourceString(SR.ID6001), context.Token, context.Principal.Claims); - } - } - - /// - /// Contains the logic responsible of validating the tokens using OAuth 2.0 introspection. - /// - public class IntrospectToken : IOpenIddictValidationHandler - { - private readonly OpenIddictValidationService _service; - - public IntrospectToken(OpenIddictValidationService service) - => _service = service; - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictValidationHandlerDescriptor Descriptor { get; } - = OpenIddictValidationHandlerDescriptor.CreateBuilder() - .AddFilter() - .UseSingletonHandler() - .SetOrder(ValidateIdentityModelToken.Descriptor.Order + 1_000) - .SetType(OpenIddictValidationHandlerType.BuiltIn) - .Build(); - - /// - public async ValueTask HandleAsync(ProcessAuthenticationContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - // If a principal was already attached, don't overwrite it. - if (context.Principal is not null) - { - return; - } - - Debug.Assert(!string.IsNullOrEmpty(context.Token), SR.GetResourceString(SR.ID4010)); - - var configuration = await context.Options.ConfigurationManager.GetConfigurationAsync(default) ?? - throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); - - if (string.IsNullOrEmpty(configuration.IntrospectionEndpoint) || - !Uri.TryCreate(configuration.IntrospectionEndpoint, UriKind.Absolute, out Uri? address) || - !address.IsWellFormedOriginalString()) - { - context.Reject( - error: Errors.ServerError, - description: SR.GetResourceString(SR.ID2092), - uri: SR.FormatID8000(SR.ID2092)); - - return; - } - - try - { - var principal = await _service.IntrospectTokenAsync(address, context.Token, context.TokenType) ?? - throw new InvalidOperationException(SR.GetResourceString(SR.ID0141)); - - // Note: tokens that are considered valid at this point are assumed to be of the given type, - // as the introspection handlers ensure the introspected token type matches the expected - // type when a "token_usage" claim was returned as part of the introspection response. - // If no token type can be inferred, the token is assumed to be an access token. - context.Principal = principal.SetTokenType(context.TokenType ?? TokenTypeHints.AccessToken); - - context.Logger.LogTrace(SR.GetResourceString(SR.ID6154), context.Token, context.Principal.Claims); - } - - catch (Exception exception) - { - context.Logger.LogDebug(exception, SR.GetResourceString(SR.ID6155)); - - context.Reject( - error: Errors.InvalidToken, - description: SR.GetResourceString(SR.ID2004), - uri: SR.FormatID8000(SR.ID2004)); - return; } - } - } - - /// - /// Contains the logic responsible of normalizing the scope claims stored in the tokens. - /// - public class NormalizeScopeClaims : IOpenIddictValidationHandler - { - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictValidationHandlerDescriptor Descriptor { get; } - = OpenIddictValidationHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(IntrospectToken.Descriptor.Order + 1_000) - .SetType(OpenIddictValidationHandlerType.BuiltIn) - .Build(); - - /// - public ValueTask HandleAsync(ProcessAuthenticationContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (context.Principal is null) - { - return default; - } - - // Note: in previous OpenIddict versions, scopes were represented as a JSON array - // and deserialized as multiple claims. In OpenIddict 3.0, the public "scope" claim - // is formatted as a unique space-separated string containing all the granted scopes. - // To ensure access tokens generated by previous versions are still correctly handled, - // both formats (unique space-separated string or multiple scope claims) must be supported. - // To achieve that, all the "scope" claims are combined into a single one containg all the values. - // Visit https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-04 for more information. - var scopes = context.Principal.GetClaims(Claims.Scope); - if (scopes.Length > 1) - { - context.Principal.SetClaim(Claims.Scope, string.Join(" ", scopes)); - } - - return default; - } - } - - /// - /// Contains the logic responsible of mapping internal claims used by OpenIddict. - /// - public class MapInternalClaims : IOpenIddictValidationHandler - { - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictValidationHandlerDescriptor Descriptor { get; } - = OpenIddictValidationHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(NormalizeScopeClaims.Descriptor.Order + 1_000) - .SetType(OpenIddictValidationHandlerType.BuiltIn) - .Build(); - - /// - public ValueTask HandleAsync(ProcessAuthenticationContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (context.Principal is null) - { - return default; - } - - // To reduce the size of tokens, some of the private claims used by OpenIddict - // are mapped to their standard equivalent before being removed from the token. - // This handler is responsible of adding back the private claims to the principal - // when receiving the token (e.g "oi_prst" is resolved from the "scope" claim). - - // In OpenIddict 3.0, the creation date of a token is stored in "oi_crt_dt". - // If the claim doesn't exist, try to infer it from the standard "iat" JWT claim. - if (!context.Principal.HasClaim(Claims.Private.CreationDate)) - { - var date = context.Principal.GetClaim(Claims.IssuedAt); - if (!string.IsNullOrEmpty(date) && - long.TryParse(date, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) - { - context.Principal.SetCreationDate(DateTimeOffset.FromUnixTimeSeconds(value)); - } - } - - // In OpenIddict 3.0, the expiration date of a token is stored in "oi_exp_dt". - // If the claim doesn't exist, try to infer it from the standard "exp" JWT claim. - if (!context.Principal.HasClaim(Claims.Private.ExpirationDate)) - { - var date = context.Principal.GetClaim(Claims.ExpiresAt); - if (!string.IsNullOrEmpty(date) && - long.TryParse(date, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) - { - context.Principal.SetExpirationDate(DateTimeOffset.FromUnixTimeSeconds(value)); - } - } - - // In OpenIddict 3.0, the audiences allowed to receive a token are stored in "oi_aud". - // If no such claim exists, try to infer them from the standard "aud" JWT claims. - if (!context.Principal.HasClaim(Claims.Private.Audience)) - { - var audiences = context.Principal.GetClaims(Claims.Audience); - if (audiences.Any()) - { - context.Principal.SetAudiences(audiences); - } - } - - // In OpenIddict 3.0, the presenters allowed to use a token are stored in "oi_prst". - // If no such claim exists, try to infer them from the standard "azp" and "client_id" JWT claims. - // - // Note: in previous OpenIddict versions, the presenters were represented in JWT tokens - // using the "azp" claim (defined by OpenID Connect), for which a single value could be - // specified. To ensure presenters stored in JWT tokens created by OpenIddict 1.x/2.x - // can still be read with OpenIddict 3.0, the presenter is automatically inferred from - // the "azp" or "client_id" claim if no "oi_prst" claim was found in the principal. - if (!context.Principal.HasClaim(Claims.Private.Presenter)) - { - var presenter = context.Principal.GetClaim(Claims.AuthorizedParty) ?? - context.Principal.GetClaim(Claims.ClientId); - - if (!string.IsNullOrEmpty(presenter)) - { - context.Principal.SetPresenters(presenter); - } - } - - // In OpenIddict 3.0, the scopes granted to an application are stored in "oi_scp". - // If no such claim exists, try to infer them from the standard "scope" JWT claim, - // which is guaranteed to be a unique space-separated claim containing all the values. - if (!context.Principal.HasClaim(Claims.Private.Scope)) - { - var scope = context.Principal.GetClaim(Claims.Scope); - if (!string.IsNullOrEmpty(scope)) - { - context.Principal.SetScopes(scope.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries)); - } - } - - return default; - } - } - - /// - /// Contains the logic responsible of restoring the properties associated with a reference token entry. - /// Note: this handler is not used when the degraded mode is enabled. - /// - public class RestoreReferenceTokenProperties : IOpenIddictValidationHandler - { - private readonly IOpenIddictTokenManager _tokenManager; - - public RestoreReferenceTokenProperties() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0139)); - - public RestoreReferenceTokenProperties(IOpenIddictTokenManager tokenManager) - => _tokenManager = tokenManager; - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictValidationHandlerDescriptor Descriptor { get; } - = OpenIddictValidationHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .UseScopedHandler() - .SetOrder(MapInternalClaims.Descriptor.Order + 1_000) - .SetType(OpenIddictValidationHandlerType.BuiltIn) - .Build(); - /// - public async ValueTask HandleAsync(ProcessAuthenticationContext context) - { - if (context is null) + var notification = new ValidateTokenContext(context.Transaction) { - throw new ArgumentNullException(nameof(context)); - } + Token = context.AccessToken, + ValidTokenTypes = { TokenTypeHints.AccessToken } + }; - if (context.Principal is null) - { - return; - } + await _dispatcher.DispatchAsync(notification); - var identifier = context.Transaction.GetProperty(Properties.ReferenceTokenIdentifier); - if (string.IsNullOrEmpty(identifier)) + if (notification.IsRequestHandled) { + context.HandleRequest(); return; } - var token = await _tokenManager.FindByIdAsync(identifier); - if (token is null) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0021)); - } - - // Restore the creation/expiration dates/identifiers from the token entry metadata. - context.Principal = context.Principal - .SetCreationDate(await _tokenManager.GetCreationDateAsync(token)) - .SetExpirationDate(await _tokenManager.GetExpirationDateAsync(token)) - .SetAuthorizationId(await _tokenManager.GetAuthorizationIdAsync(token)) - .SetTokenId(await _tokenManager.GetIdAsync(token)) - .SetTokenType(await _tokenManager.GetTypeAsync(token)); - } - } - - /// - /// Contains the logic responsible of rejecting authentication demands for which no valid principal was resolved. - /// - public class ValidatePrincipal : IOpenIddictValidationHandler - { - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictValidationHandlerDescriptor Descriptor { get; } - = OpenIddictValidationHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(RestoreReferenceTokenProperties.Descriptor.Order + 1_000) - .SetType(OpenIddictValidationHandlerType.BuiltIn) - .Build(); - - /// - public ValueTask HandleAsync(ProcessAuthenticationContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (context.Principal is null) - { - context.Reject( - error: Errors.InvalidToken, - description: SR.GetResourceString(SR.ID2004), - uri: SR.FormatID8000(SR.ID2004)); - - return default; - } - - // When using JWT or Data Protection tokens, the correct token type is always enforced by IdentityModel - // (using the "typ" header) or by ASP.NET Core Data Protection (using per-token-type purposes strings). - // To ensure tokens deserialized using a custom routine are of the expected type, a manual check is used, - // which requires that a special claim containing the token type be present in the security principal. - if (!string.IsNullOrEmpty(context.TokenType)) - { - var type = context.Principal.GetTokenType(); - if (string.IsNullOrEmpty(type)) - { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0004)); - } - - if (!string.Equals(type, context.TokenType, StringComparison.OrdinalIgnoreCase)) - { - throw new InvalidOperationException(SR.FormatID0005(type, context.TokenType)); - } - } - - return default; - } - } - - /// - /// Contains the logic responsible of rejecting authentication demands containing expired access tokens. - /// - public class ValidateExpirationDate : IOpenIddictValidationHandler - { - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictValidationHandlerDescriptor Descriptor { get; } - = OpenIddictValidationHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(ValidatePrincipal.Descriptor.Order + 1_000) - .SetType(OpenIddictValidationHandlerType.BuiltIn) - .Build(); - - /// - public ValueTask HandleAsync(ProcessAuthenticationContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); - - var date = context.Principal.GetExpirationDate(); - if (date.HasValue && date.Value < DateTimeOffset.UtcNow) - { - context.Logger.LogInformation(SR.GetResourceString(SR.ID6156)); - - context.Reject( - error: Errors.InvalidToken, - description: SR.GetResourceString(SR.ID2019), - uri: SR.FormatID8000(SR.ID2019)); - - return default; - } - - return default; - } - } - - /// - /// Contains the logic responsible of rejecting authentication demands containing - /// access tokens that were issued to be used by another audience/resource server. - /// - public class ValidateAudience : IOpenIddictValidationHandler - { - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictValidationHandlerDescriptor Descriptor { get; } - = OpenIddictValidationHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(ValidateExpirationDate.Descriptor.Order + 1_000) - .SetType(OpenIddictValidationHandlerType.BuiltIn) - .Build(); - - /// - public ValueTask HandleAsync(ProcessAuthenticationContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); - - // If no explicit audience has been configured, - // skip the default audience validation. - if (context.Options.Audiences.Count == 0) - { - return default; - } - - // If the access token doesn't have any audience attached, return an error. - var audiences = context.Principal.GetAudiences(); - if (audiences.IsDefaultOrEmpty) - { - context.Logger.LogInformation(SR.GetResourceString(SR.ID6157)); - - context.Reject( - error: Errors.InvalidToken, - description: SR.GetResourceString(SR.ID2093), - uri: SR.FormatID8000(SR.ID2093)); - - return default; - } - - // If the access token doesn't include any registered audience, return an error. - if (!audiences.Intersect(context.Options.Audiences, StringComparer.Ordinal).Any()) - { - context.Logger.LogInformation(SR.GetResourceString(SR.ID6158)); - - context.Reject( - error: Errors.InvalidToken, - description: SR.GetResourceString(SR.ID2094), - uri: SR.FormatID8000(SR.ID2094)); - - return default; - } - - return default; - } - } - - /// - /// Contains the logic responsible of authentication demands a token whose - /// associated token entry is no longer valid (e.g was revoked). - /// Note: this handler is not used when the degraded mode is enabled. - /// - public class ValidateTokenEntry : IOpenIddictValidationHandler - { - private readonly IOpenIddictTokenManager _tokenManager; - - public ValidateTokenEntry() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0139)); - - public ValidateTokenEntry(IOpenIddictTokenManager tokenManager) - => _tokenManager = tokenManager; - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictValidationHandlerDescriptor Descriptor { get; } - = OpenIddictValidationHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .UseScopedHandler() - .SetOrder(ValidateAudience.Descriptor.Order + 1_000) - .SetType(OpenIddictValidationHandlerType.BuiltIn) - .Build(); - - /// - public async ValueTask HandleAsync(ProcessAuthenticationContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); - - var identifier = context.Principal.GetTokenId(); - if (string.IsNullOrEmpty(identifier)) + else if (notification.IsRequestSkipped) { + context.SkipRequest(); return; } - var token = await _tokenManager.FindByIdAsync(identifier); - if (token is null || !await _tokenManager.HasStatusAsync(token, Statuses.Valid)) + else if (notification.IsRejected) { - context.Logger.LogInformation(SR.GetResourceString(SR.ID6005), identifier); - context.Reject( - error: Errors.InvalidToken, - description: SR.GetResourceString(SR.ID2019), - uri: SR.FormatID8000(SR.ID2019)); - + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); return; } - // Restore the creation/expiration dates/identifiers from the token entry metadata. - context.Principal.SetCreationDate(await _tokenManager.GetCreationDateAsync(token)) - .SetExpirationDate(await _tokenManager.GetExpirationDateAsync(token)) - .SetAuthorizationId(await _tokenManager.GetAuthorizationIdAsync(token)) - .SetTokenId(await _tokenManager.GetIdAsync(token)) - .SetTokenType(await _tokenManager.GetTypeAsync(token)); - } - } - - /// - /// Contains the logic responsible of authentication demands a token whose - /// associated authorization entry is no longer valid (e.g was revoked). - /// Note: this handler is not used when the degraded mode is enabled. - /// - public class ValidateAuthorizationEntry : IOpenIddictValidationHandler - { - private readonly IOpenIddictAuthorizationManager _authorizationManager; - - public ValidateAuthorizationEntry() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0142)); - - public ValidateAuthorizationEntry(IOpenIddictAuthorizationManager authorizationManager) - => _authorizationManager = authorizationManager; - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictValidationHandlerDescriptor Descriptor { get; } - = OpenIddictValidationHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .UseScopedHandler() - .SetOrder(ValidateTokenEntry.Descriptor.Order + 1_000) - .SetType(OpenIddictValidationHandlerType.BuiltIn) - .Build(); - - /// - public async ValueTask HandleAsync(ProcessAuthenticationContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); - - var identifier = context.Principal.GetAuthorizationId(); - if (string.IsNullOrEmpty(identifier)) - { - return; - } - - var authorization = await _authorizationManager.FindByIdAsync(identifier); - if (authorization is null || !await _authorizationManager.HasStatusAsync(authorization, Statuses.Valid)) - { - context.Logger.LogInformation(SR.GetResourceString(SR.ID6006), identifier); - - context.Reject( - error: Errors.InvalidToken, - description: SR.GetResourceString(SR.ID2023), - uri: SR.FormatID8000(SR.ID2023)); - - return; - } + context.AccessTokenPrincipal = notification.Principal; } } diff --git a/src/OpenIddict.Validation/OpenIddictValidationService.cs b/src/OpenIddict.Validation/OpenIddictValidationService.cs index d5dfbcbb..4dada6d8 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationService.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationService.cs @@ -321,18 +321,18 @@ namespace OpenIddict.Validation /// The that can be used to abort the operation. /// The claims principal created from the claim retrieved from the remote server. public ValueTask IntrospectTokenAsync(Uri address, string token, CancellationToken cancellationToken = default) - => IntrospectTokenAsync(address, token, type: null, cancellationToken); + => IntrospectTokenAsync(address, token, hint: null, cancellationToken); /// /// Sends an introspection request to the specified address and returns the corresponding principal. /// /// The address of the remote metadata endpoint. /// The token to introspect. - /// The token type to introspect. + /// The token type to introspect, used as a hint by the authorization server. /// The that can be used to abort the operation. /// The claims principal created from the claim retrieved from the remote server. public async ValueTask IntrospectTokenAsync( - Uri address, string token, string? type, CancellationToken cancellationToken = default) + Uri address, string token, string? hint, CancellationToken cancellationToken = default) { if (address is null) { @@ -384,7 +384,7 @@ namespace OpenIddict.Validation Address = address, Request = request, Token = token, - TokenType = type + TokenTypeHint = hint }; await dispatcher.DispatchAsync(context); @@ -445,8 +445,7 @@ namespace OpenIddict.Validation { Request = request, Response = response, - Token = token, - TokenType = type + Token = token }; await dispatcher.DispatchAsync(context); @@ -506,10 +505,10 @@ namespace OpenIddict.Validation var factory = scope.ServiceProvider.GetRequiredService(); var transaction = await factory.CreateTransactionAsync(); - var context = new ProcessAuthenticationContext(transaction) + var context = new ValidateTokenContext(transaction) { Token = token, - TokenType = TokenTypeHints.AccessToken + ValidTokenTypes = { TokenTypeHints.AccessToken } }; await dispatcher.DispatchAsync(context); diff --git a/test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.cs b/test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.cs index 500bf9d0..24ae7351 100644 --- a/test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.cs +++ b/test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.cs @@ -27,6 +27,7 @@ using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers; using static OpenIddict.Server.OpenIddictServerEvents; using static OpenIddict.Server.OpenIddictServerHandlers; +using static OpenIddict.Server.OpenIddictServerHandlers.Protection; using SR = OpenIddict.Abstractions.OpenIddictResources; namespace OpenIddict.Server.AspNetCore.IntegrationTests @@ -55,12 +56,12 @@ namespace OpenIddict.Server.AspNetCore.IntegrationTests return default; })); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("access_token", context.Token); - Assert.Equal(TokenTypeHints.AccessToken, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AccessToken }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AccessToken) @@ -106,12 +107,12 @@ namespace OpenIddict.Server.AspNetCore.IntegrationTests return default; })); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("access_token", context.Token); - Assert.Equal(TokenTypeHints.AccessToken, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AccessToken }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AccessToken) diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs index 007b1eab..b0d82312 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs @@ -18,6 +18,7 @@ using Xunit; using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Server.OpenIddictServerEvents; using static OpenIddict.Server.OpenIddictServerHandlers; +using static OpenIddict.Server.OpenIddictServerHandlers.Protection; using SR = OpenIddict.Abstractions.OpenIddictResources; namespace OpenIddict.Server.IntegrationTests @@ -303,12 +304,12 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); - Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AuthorizationCode }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) @@ -396,12 +397,12 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); - Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AuthorizationCode }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) @@ -439,12 +440,12 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("8xLOxBtZp8", context.Token); - Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.RefreshToken }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) @@ -481,12 +482,12 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); - Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AuthorizationCode }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) @@ -524,12 +525,12 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); - Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AuthorizationCode }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) @@ -567,12 +568,12 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("8xLOxBtZp8", context.Token); - Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.RefreshToken }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) @@ -610,12 +611,12 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); - Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AuthorizationCode }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) @@ -655,12 +656,12 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); - Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AuthorizationCode }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) @@ -700,12 +701,12 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); - Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AuthorizationCode }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) @@ -744,12 +745,12 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); - Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AuthorizationCode }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) @@ -790,12 +791,12 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); - Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AuthorizationCode }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) @@ -836,12 +837,12 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); - Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AuthorizationCode }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) @@ -884,12 +885,12 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); - Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AuthorizationCode }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) @@ -932,12 +933,12 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); - Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AuthorizationCode }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) @@ -986,12 +987,12 @@ namespace OpenIddict.Server.IntegrationTests options.EnableDegradedMode(); options.RegisterScopes(Scopes.Phone, Scopes.Profile); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); - Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AuthorizationCode }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) @@ -1032,12 +1033,12 @@ namespace OpenIddict.Server.IntegrationTests options.EnableDegradedMode(); options.RegisterScopes(Scopes.Phone, Scopes.Profile); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); - Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AuthorizationCode }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) @@ -1078,12 +1079,12 @@ namespace OpenIddict.Server.IntegrationTests options.EnableDegradedMode(); options.RegisterScopes(Scopes.Phone, Scopes.Profile); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("8xLOxBtZp8", context.Token); - Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.RefreshToken }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) @@ -1122,12 +1123,12 @@ namespace OpenIddict.Server.IntegrationTests options.EnableDegradedMode(); options.RegisterScopes(Scopes.Phone, Scopes.Profile); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("8xLOxBtZp8", context.Token); - Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.RefreshToken }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) @@ -1861,12 +1862,12 @@ namespace OpenIddict.Server.IntegrationTests await using var server = await CreateServerAsync(options => { - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); - Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AuthorizationCode }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) @@ -1927,12 +1928,12 @@ namespace OpenIddict.Server.IntegrationTests await using var server = await CreateServerAsync(options => { - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); - Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AuthorizationCode }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) @@ -2087,12 +2088,12 @@ namespace OpenIddict.Server.IntegrationTests // Arrange await using var server = await CreateServerAsync(options => { - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); - Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AuthorizationCode }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) @@ -2152,12 +2153,12 @@ namespace OpenIddict.Server.IntegrationTests // Arrange await using var server = await CreateServerAsync(options => { - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("8xLOxBtZp8", context.Token); - Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.RefreshToken }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) @@ -2209,12 +2210,12 @@ namespace OpenIddict.Server.IntegrationTests await using var server = await CreateServerAsync(options => { - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); - Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AuthorizationCode }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) @@ -2281,12 +2282,12 @@ namespace OpenIddict.Server.IntegrationTests await using var server = await CreateServerAsync(options => { - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("8xLOxBtZp8", context.Token); - Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.RefreshToken }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) @@ -2349,12 +2350,12 @@ namespace OpenIddict.Server.IntegrationTests await using var server = await CreateServerAsync(options => { - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); - Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AuthorizationCode }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) @@ -2437,12 +2438,12 @@ namespace OpenIddict.Server.IntegrationTests { options.SetRefreshTokenReuseLeeway(leeway: null); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("8xLOxBtZp8", context.Token); - Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.RefreshToken }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) @@ -2511,12 +2512,12 @@ namespace OpenIddict.Server.IntegrationTests { options.SetRefreshTokenReuseLeeway(TimeSpan.FromSeconds(5)); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("8xLOxBtZp8", context.Token); - Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.RefreshToken }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) @@ -2588,12 +2589,12 @@ namespace OpenIddict.Server.IntegrationTests { options.SetRefreshTokenReuseLeeway(TimeSpan.FromMinutes(5)); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("8xLOxBtZp8", context.Token); - Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.RefreshToken }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) @@ -2670,12 +2671,12 @@ namespace OpenIddict.Server.IntegrationTests await using var server = await CreateServerAsync(options => { - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); - Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AuthorizationCode }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) @@ -2776,12 +2777,12 @@ namespace OpenIddict.Server.IntegrationTests { options.SetRefreshTokenReuseLeeway(leeway: null); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("8xLOxBtZp8", context.Token); - Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.RefreshToken }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) @@ -2869,12 +2870,12 @@ namespace OpenIddict.Server.IntegrationTests { options.SetRefreshTokenReuseLeeway(TimeSpan.FromSeconds(5)); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("8xLOxBtZp8", context.Token); - Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.RefreshToken }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) @@ -2965,12 +2966,12 @@ namespace OpenIddict.Server.IntegrationTests { options.SetRefreshTokenReuseLeeway(TimeSpan.FromMinutes(5)); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("8xLOxBtZp8", context.Token); - Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.RefreshToken }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) @@ -3049,12 +3050,12 @@ namespace OpenIddict.Server.IntegrationTests await using var server = await CreateServerAsync(options => { - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); - Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AuthorizationCode }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) @@ -3136,12 +3137,12 @@ namespace OpenIddict.Server.IntegrationTests await using var server = await CreateServerAsync(options => { - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("8xLOxBtZp8", context.Token); - Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.RefreshToken }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) @@ -3198,12 +3199,12 @@ namespace OpenIddict.Server.IntegrationTests await using var server = await CreateServerAsync(options => { - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); - Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AuthorizationCode }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) @@ -3302,12 +3303,12 @@ namespace OpenIddict.Server.IntegrationTests { options.DisableRollingRefreshTokens(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("8xLOxBtZp8", context.Token); - Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.RefreshToken }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) @@ -3385,12 +3386,12 @@ namespace OpenIddict.Server.IntegrationTests await using var server = await CreateServerAsync(options => { - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); - Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AuthorizationCode }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) @@ -3484,12 +3485,12 @@ namespace OpenIddict.Server.IntegrationTests await using var server = await CreateServerAsync(options => { - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); - Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AuthorizationCode }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) @@ -3579,12 +3580,12 @@ namespace OpenIddict.Server.IntegrationTests await using var server = await CreateServerAsync(options => { - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("8xLOxBtZp8", context.Token); - Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.RefreshToken }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) @@ -3664,12 +3665,12 @@ namespace OpenIddict.Server.IntegrationTests await using var server = await CreateServerAsync(options => { - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("8xLOxBtZp8", context.Token); - Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.RefreshToken }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) @@ -3769,12 +3770,14 @@ namespace OpenIddict.Server.IntegrationTests { options.DisableRollingRefreshTokens(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetTokenType(context.TokenType) + .SetTokenType(context.Request.IsAuthorizationCodeGrantType() ? + TokenTypeHints.AuthorizationCode : + TokenTypeHints.RefreshToken) .SetPresenters("Fabrikam") .SetTokenId("0270F515-C5B1-4FBF-B673-D7CAF7CCDABC") .SetClaim(Claims.Subject, "Bob le Bricoleur"); diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Introspection.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Introspection.cs index 4adfa0f7..1c0cebf4 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Introspection.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Introspection.cs @@ -19,6 +19,7 @@ using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Server.OpenIddictServerEvents; using static OpenIddict.Server.OpenIddictServerHandlers; using static OpenIddict.Server.OpenIddictServerHandlers.Introspection; +using static OpenIddict.Server.OpenIddictServerHandlers.Protection; using SR = OpenIddict.Abstractions.OpenIddictResources; namespace OpenIddict.Server.IntegrationTests @@ -190,7 +191,7 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -237,7 +238,7 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -278,7 +279,7 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -322,7 +323,7 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -591,7 +592,7 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -637,7 +638,7 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -686,7 +687,7 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -730,7 +731,7 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -787,7 +788,7 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -842,7 +843,7 @@ namespace OpenIddict.Server.IntegrationTests await using var server = await CreateServerAsync(options => { - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -902,7 +903,7 @@ namespace OpenIddict.Server.IntegrationTests await using var server = await CreateServerAsync(options => { - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -969,7 +970,7 @@ namespace OpenIddict.Server.IntegrationTests await using var server = await CreateServerAsync(options => { - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -1099,7 +1100,7 @@ namespace OpenIddict.Server.IntegrationTests await using var server = await CreateServerAsync(options => { - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -1191,7 +1192,7 @@ namespace OpenIddict.Server.IntegrationTests await using var server = await CreateServerAsync(options => { - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -1290,7 +1291,7 @@ namespace OpenIddict.Server.IntegrationTests await using var server = await CreateServerAsync(options => { - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -1402,7 +1403,7 @@ namespace OpenIddict.Server.IntegrationTests await using var server = await CreateServerAsync(options => { - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -1474,7 +1475,7 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -1520,7 +1521,7 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -1569,7 +1570,7 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -1613,7 +1614,7 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Revocation.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Revocation.cs index 4f6f91de..95560956 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Revocation.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Revocation.cs @@ -15,6 +15,7 @@ using Xunit; using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Server.OpenIddictServerEvents; using static OpenIddict.Server.OpenIddictServerHandlers; +using static OpenIddict.Server.OpenIddictServerHandlers.Protection; using static OpenIddict.Server.OpenIddictServerHandlers.Revocation; using SR = OpenIddict.Abstractions.OpenIddictResources; @@ -168,7 +169,7 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -209,7 +210,7 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -253,7 +254,7 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -530,7 +531,7 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -576,7 +577,7 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -625,7 +626,7 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -673,7 +674,7 @@ namespace OpenIddict.Server.IntegrationTests await using var server = await CreateServerAsync(options => { - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -734,7 +735,7 @@ namespace OpenIddict.Server.IntegrationTests await using var server = await CreateServerAsync(options => { - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -798,7 +799,7 @@ namespace OpenIddict.Server.IntegrationTests await using var server = await CreateServerAsync(options => { - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -849,7 +850,7 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -895,7 +896,7 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -944,7 +945,7 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -988,7 +989,7 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Userinfo.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Userinfo.cs index e62ac645..33a50e0e 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Userinfo.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Userinfo.cs @@ -14,6 +14,7 @@ using Xunit; using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Server.OpenIddictServerEvents; using static OpenIddict.Server.OpenIddictServerHandlers; +using static OpenIddict.Server.OpenIddictServerHandlers.Protection; using SR = OpenIddict.Abstractions.OpenIddictResources; namespace OpenIddict.Server.IntegrationTests @@ -177,7 +178,7 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -223,7 +224,7 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -269,7 +270,7 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -318,7 +319,7 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -362,7 +363,7 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -403,7 +404,7 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -452,7 +453,7 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -499,7 +500,7 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -539,7 +540,7 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -586,7 +587,7 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -632,7 +633,7 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -681,7 +682,7 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -725,7 +726,7 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -774,7 +775,7 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs index 8f4a302c..b07ddac1 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs @@ -21,6 +21,7 @@ using Xunit.Abstractions; using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Server.OpenIddictServerEvents; using static OpenIddict.Server.OpenIddictServerHandlers; +using static OpenIddict.Server.OpenIddictServerHandlers.Protection; using SR = OpenIddict.Abstractions.OpenIddictResources; namespace OpenIddict.Server.IntegrationTests @@ -190,12 +191,12 @@ namespace OpenIddict.Server.IntegrationTests return default; })); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("access_token", context.Token); - Assert.Equal(TokenTypeHints.AccessToken, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AccessToken }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AccessToken) @@ -237,12 +238,12 @@ namespace OpenIddict.Server.IntegrationTests return default; })); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("access_token", context.Token); - Assert.Equal(TokenTypeHints.AccessToken, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AccessToken }, context.ValidTokenTypes); var identity = new ClaimsIdentity("Bearer"); identity.AddClaim(new Claim(Claims.IssuedAt, "1577836800", ClaimValueTypes.Integer64)); @@ -289,12 +290,12 @@ namespace OpenIddict.Server.IntegrationTests return default; })); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("access_token", context.Token); - Assert.Equal(TokenTypeHints.AccessToken, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AccessToken }, context.ValidTokenTypes); var identity = new ClaimsIdentity("Bearer"); identity.AddClaim(new Claim(Claims.ExpiresAt, "2524608000", ClaimValueTypes.Integer64)); @@ -341,12 +342,12 @@ namespace OpenIddict.Server.IntegrationTests return default; })); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("access_token", context.Token); - Assert.Equal(TokenTypeHints.AccessToken, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AccessToken }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AccessToken) @@ -391,12 +392,12 @@ namespace OpenIddict.Server.IntegrationTests return default; })); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("access_token", context.Token); - Assert.Equal(TokenTypeHints.AccessToken, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AccessToken }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AccessToken) @@ -441,12 +442,12 @@ namespace OpenIddict.Server.IntegrationTests return default; })); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("access_token", context.Token); - Assert.Equal(TokenTypeHints.AccessToken, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AccessToken }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AccessToken) @@ -491,12 +492,12 @@ namespace OpenIddict.Server.IntegrationTests return default; })); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("access_token", context.Token); - Assert.Equal(TokenTypeHints.AccessToken, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AccessToken }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AccessToken) @@ -541,12 +542,12 @@ namespace OpenIddict.Server.IntegrationTests return default; })); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("access_token", context.Token); - Assert.Equal(TokenTypeHints.AccessToken, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AccessToken }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AccessToken) @@ -590,12 +591,12 @@ namespace OpenIddict.Server.IntegrationTests return default; })); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("access_token", context.Token); - Assert.Equal(TokenTypeHints.AccessToken, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AccessToken }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AccessToken) @@ -639,12 +640,12 @@ namespace OpenIddict.Server.IntegrationTests return default; })); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("access_token", context.Token); - Assert.Equal(TokenTypeHints.AccessToken, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AccessToken }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AccessToken) @@ -688,12 +689,12 @@ namespace OpenIddict.Server.IntegrationTests return default; })); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("access_token", context.Token); - Assert.Equal(TokenTypeHints.AccessToken, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AccessToken }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(null) @@ -738,12 +739,12 @@ namespace OpenIddict.Server.IntegrationTests return default; })); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("access_token", context.Token); - Assert.Equal(TokenTypeHints.AccessToken, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AccessToken }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) @@ -848,12 +849,12 @@ namespace OpenIddict.Server.IntegrationTests return default; })); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("id_token", context.Token); - Assert.Equal(TokenTypeHints.IdToken, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.IdToken }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.IdToken) @@ -959,12 +960,12 @@ namespace OpenIddict.Server.IntegrationTests return default; })); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("authorization_code", context.Token); - Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AuthorizationCode }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) @@ -1071,12 +1072,12 @@ namespace OpenIddict.Server.IntegrationTests return default; })); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("refresh_token", context.Token); - Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.RefreshToken }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) @@ -1226,7 +1227,7 @@ namespace OpenIddict.Server.IntegrationTests options.EnableDegradedMode(); options.SetUserinfoEndpointUris("/challenge"); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { @@ -1681,7 +1682,7 @@ namespace OpenIddict.Server.IntegrationTests return default; }); - builder.SetOrder(EvaluateTokenTypes.Descriptor.Order + 500); + builder.SetOrder(EvaluateGeneratedTokens.Descriptor.Order + 500); }); }); @@ -1741,7 +1742,7 @@ namespace OpenIddict.Server.IntegrationTests return default; }); - builder.SetOrder(EvaluateTokenTypes.Descriptor.Order + 500); + builder.SetOrder(EvaluateGeneratedTokens.Descriptor.Order + 500); }); }); @@ -1792,7 +1793,7 @@ namespace OpenIddict.Server.IntegrationTests return default; }); - builder.SetOrder(EvaluateTokenTypes.Descriptor.Order + 500); + builder.SetOrder(EvaluateGeneratedTokens.Descriptor.Order + 500); }); }); @@ -1820,12 +1821,12 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); - Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AuthorizationCode }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) @@ -1848,7 +1849,7 @@ namespace OpenIddict.Server.IntegrationTests return default; }); - builder.SetOrder(EvaluateTokenTypes.Descriptor.Order + 500); + builder.SetOrder(EvaluateGeneratedTokens.Descriptor.Order + 500); }); }); @@ -1874,12 +1875,12 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS", context.Token); - Assert.Equal(TokenTypeHints.DeviceCode, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.DeviceCode }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity()) .SetTokenType(TokenTypeHints.DeviceCode) @@ -1914,7 +1915,7 @@ namespace OpenIddict.Server.IntegrationTests return default; }); - builder.SetOrder(EvaluateTokenTypes.Descriptor.Order + 500); + builder.SetOrder(EvaluateGeneratedTokens.Descriptor.Order + 500); }); }); @@ -1940,12 +1941,12 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("8xLOxBtZp8", context.Token); - Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.RefreshToken }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) @@ -1967,7 +1968,7 @@ namespace OpenIddict.Server.IntegrationTests return default; }); - builder.SetOrder(EvaluateTokenTypes.Descriptor.Order + 500); + builder.SetOrder(EvaluateGeneratedTokens.Descriptor.Order + 500); }); }); @@ -2011,7 +2012,7 @@ namespace OpenIddict.Server.IntegrationTests return default; }); - builder.SetOrder(EvaluateTokenTypes.Descriptor.Order + 500); + builder.SetOrder(EvaluateGeneratedTokens.Descriptor.Order + 500); }); }); @@ -2056,7 +2057,7 @@ namespace OpenIddict.Server.IntegrationTests return default; }); - builder.SetOrder(EvaluateTokenTypes.Descriptor.Order + 500); + builder.SetOrder(EvaluateGeneratedTokens.Descriptor.Order + 500); }); }); @@ -2102,7 +2103,7 @@ namespace OpenIddict.Server.IntegrationTests return default; }); - builder.SetOrder(EvaluateTokenTypes.Descriptor.Order + 500); + builder.SetOrder(EvaluateGeneratedTokens.Descriptor.Order + 500); }); }); @@ -2149,7 +2150,7 @@ namespace OpenIddict.Server.IntegrationTests return default; }); - builder.SetOrder(EvaluateTokenTypes.Descriptor.Order + 500); + builder.SetOrder(EvaluateGeneratedTokens.Descriptor.Order + 500); }); }); @@ -2195,7 +2196,7 @@ namespace OpenIddict.Server.IntegrationTests return default; }); - builder.SetOrder(EvaluateTokenTypes.Descriptor.Order + 500); + builder.SetOrder(EvaluateGeneratedTokens.Descriptor.Order + 500); }); }); @@ -2220,12 +2221,12 @@ namespace OpenIddict.Server.IntegrationTests options.EnableDegradedMode(); options.RegisterScopes(Scopes.Profile); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("8xLOxBtZp8", context.Token); - Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.RefreshToken }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) @@ -2292,7 +2293,7 @@ namespace OpenIddict.Server.IntegrationTests return default; }); - builder.SetOrder(EvaluateTokenTypes.Descriptor.Order + 500); + builder.SetOrder(EvaluateGeneratedTokens.Descriptor.Order + 500); }); }); @@ -2318,12 +2319,12 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); - Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AuthorizationCode }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) @@ -2347,7 +2348,7 @@ namespace OpenIddict.Server.IntegrationTests return default; }); - builder.SetOrder(EvaluateTokenTypes.Descriptor.Order + 500); + builder.SetOrder(EvaluateGeneratedTokens.Descriptor.Order + 500); }); }); @@ -2373,12 +2374,12 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS", context.Token); - Assert.Equal(TokenTypeHints.DeviceCode, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.DeviceCode }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity()) .SetTokenType(TokenTypeHints.DeviceCode) @@ -2414,7 +2415,7 @@ namespace OpenIddict.Server.IntegrationTests return default; }); - builder.SetOrder(EvaluateTokenTypes.Descriptor.Order + 500); + builder.SetOrder(EvaluateGeneratedTokens.Descriptor.Order + 500); }); }); @@ -2440,12 +2441,12 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("8xLOxBtZp8", context.Token); - Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.RefreshToken }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) @@ -2468,7 +2469,7 @@ namespace OpenIddict.Server.IntegrationTests return default; }); - builder.SetOrder(EvaluateTokenTypes.Descriptor.Order + 500); + builder.SetOrder(EvaluateGeneratedTokens.Descriptor.Order + 500); }); }); @@ -2503,7 +2504,7 @@ namespace OpenIddict.Server.IntegrationTests return default; }); - builder.SetOrder(EvaluateTokenTypes.Descriptor.Order + 500); + builder.SetOrder(EvaluateGeneratedTokens.Descriptor.Order + 500); }); options.AddEventHandler(builder => @@ -2549,7 +2550,7 @@ namespace OpenIddict.Server.IntegrationTests return default; }); - builder.SetOrder(EvaluateTokenTypes.Descriptor.Order + 500); + builder.SetOrder(EvaluateGeneratedTokens.Descriptor.Order + 500); }); options.AddEventHandler(builder => @@ -2596,7 +2597,7 @@ namespace OpenIddict.Server.IntegrationTests return default; }); - builder.SetOrder(EvaluateTokenTypes.Descriptor.Order + 500); + builder.SetOrder(EvaluateGeneratedTokens.Descriptor.Order + 500); }); options.AddEventHandler(builder => @@ -2649,7 +2650,7 @@ namespace OpenIddict.Server.IntegrationTests return default; }); - builder.SetOrder(EvaluateTokenTypes.Descriptor.Order + 500); + builder.SetOrder(EvaluateGeneratedTokens.Descriptor.Order + 500); }); }); @@ -2698,7 +2699,7 @@ namespace OpenIddict.Server.IntegrationTests return default; }); - builder.SetOrder(EvaluateTokenTypes.Descriptor.Order + 500); + builder.SetOrder(EvaluateGeneratedTokens.Descriptor.Order + 500); }); }); @@ -2726,12 +2727,12 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); - Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AuthorizationCode }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) @@ -2755,7 +2756,7 @@ namespace OpenIddict.Server.IntegrationTests return default; }); - builder.SetOrder(EvaluateTokenTypes.Descriptor.Order + 500); + builder.SetOrder(EvaluateGeneratedTokens.Descriptor.Order + 500); }); }); @@ -2781,12 +2782,12 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS", context.Token); - Assert.Equal(TokenTypeHints.DeviceCode, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.DeviceCode }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity()) .SetTokenType(TokenTypeHints.DeviceCode) @@ -2822,7 +2823,7 @@ namespace OpenIddict.Server.IntegrationTests return default; }); - builder.SetOrder(EvaluateTokenTypes.Descriptor.Order + 500); + builder.SetOrder(EvaluateGeneratedTokens.Descriptor.Order + 500); }); }); @@ -2848,12 +2849,12 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("8xLOxBtZp8", context.Token); - Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.RefreshToken }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) @@ -2876,7 +2877,7 @@ namespace OpenIddict.Server.IntegrationTests return default; }); - builder.SetOrder(EvaluateTokenTypes.Descriptor.Order + 500); + builder.SetOrder(EvaluateGeneratedTokens.Descriptor.Order + 500); }); }); @@ -2911,7 +2912,7 @@ namespace OpenIddict.Server.IntegrationTests return default; }); - builder.SetOrder(EvaluateTokenTypes.Descriptor.Order + 500); + builder.SetOrder(EvaluateGeneratedTokens.Descriptor.Order + 500); }); options.AddEventHandler(builder => @@ -2966,7 +2967,7 @@ namespace OpenIddict.Server.IntegrationTests return default; }); - builder.SetOrder(EvaluateTokenTypes.Descriptor.Order + 500); + builder.SetOrder(EvaluateGeneratedTokens.Descriptor.Order + 500); }); }); @@ -3013,7 +3014,7 @@ namespace OpenIddict.Server.IntegrationTests return default; }); - builder.SetOrder(EvaluateTokenTypes.Descriptor.Order + 500); + builder.SetOrder(EvaluateGeneratedTokens.Descriptor.Order + 500); }); }); @@ -3056,7 +3057,7 @@ namespace OpenIddict.Server.IntegrationTests return default; }); - builder.SetOrder(EvaluateTokenTypes.Descriptor.Order + 500); + builder.SetOrder(EvaluateGeneratedTokens.Descriptor.Order + 500); }); }); @@ -3080,12 +3081,12 @@ namespace OpenIddict.Server.IntegrationTests { options.EnableDegradedMode(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("8xLOxBtZp8", context.Token); - Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.RefreshToken }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) @@ -3149,12 +3150,12 @@ namespace OpenIddict.Server.IntegrationTests await using var server = await CreateServerAsync(options => { - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); - Assert.Equal(TokenTypeHints.AuthorizationCode, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AuthorizationCode }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AuthorizationCode) @@ -3229,12 +3230,12 @@ namespace OpenIddict.Server.IntegrationTests { options.DisableAuthorizationStorage(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("8xLOxBtZp8", context.Token); - Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.RefreshToken }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) @@ -3293,12 +3294,12 @@ namespace OpenIddict.Server.IntegrationTests options.DisableAuthorizationStorage(); options.DisableRollingRefreshTokens(); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("8xLOxBtZp8", context.Token); - Assert.Equal(TokenTypeHints.RefreshToken, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.RefreshToken }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.RefreshToken) @@ -3852,10 +3853,10 @@ namespace OpenIddict.Server.IntegrationTests options.AddEventHandler(builder => builder.UseInlineHandler(context => default)); - options.AddEventHandler(builder => + options.AddEventHandler(builder => builder.UseInlineHandler(context => default)); - options.AddEventHandler(builder => + options.AddEventHandler(builder => builder.UseInlineHandler(context => default)); }); } diff --git a/test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.cs b/test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.cs index 6180680f..3d2347b5 100644 --- a/test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.cs +++ b/test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.cs @@ -24,6 +24,7 @@ using Xunit.Abstractions; using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Server.OpenIddictServerEvents; using static OpenIddict.Server.OpenIddictServerHandlers; +using static OpenIddict.Server.OpenIddictServerHandlers.Protection; using static OpenIddict.Server.Owin.OpenIddictServerOwinHandlers; using SR = OpenIddict.Abstractions.OpenIddictResources; @@ -53,12 +54,12 @@ namespace OpenIddict.Server.Owin.IntegrationTests return default; })); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("access_token", context.Token); - Assert.Equal(TokenTypeHints.AccessToken, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AccessToken }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AccessToken) @@ -104,12 +105,12 @@ namespace OpenIddict.Server.Owin.IntegrationTests return default; })); - options.AddEventHandler(builder => + options.AddEventHandler(builder => { builder.UseInlineHandler(context => { Assert.Equal("access_token", context.Token); - Assert.Equal(TokenTypeHints.AccessToken, context.TokenType); + Assert.Equal(new[] { TokenTypeHints.AccessToken }, context.ValidTokenTypes); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) .SetTokenType(TokenTypeHints.AccessToken)