From a941660a411c51aeb087ec13fddb88cfb8be903c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Mon, 18 Sep 2023 11:35:23 +0200 Subject: [PATCH] Move the client authentication logic to the ProcessAuthentication event --- .../OpenIddictResources.resx | 92 +--- .../OpenIddictServerConfiguration.cs | 26 +- .../OpenIddictServerEvents.cs | 2 +- .../OpenIddictServerExtensions.cs | 1 + .../OpenIddictServerHandlerFilters.cs | 17 + ...OpenIddictServerHandlers.Authentication.cs | 128 ++---- .../OpenIddictServerHandlers.Device.cs | 224 ++------- .../OpenIddictServerHandlers.Exchange.cs | 276 ++---------- .../OpenIddictServerHandlers.Introspection.cs | 280 ++---------- .../OpenIddictServerHandlers.Revocation.cs | 280 ++---------- .../OpenIddictServerHandlers.Session.cs | 121 ++--- .../OpenIddictServerHandlers.Userinfo.cs | 10 +- .../OpenIddictServerHandlers.cs | 302 ++++++++++++- ...enIddictServerIntegrationTests.Exchange.cs | 24 + ...ictServerIntegrationTests.Introspection.cs | 425 +++++++++--------- ...IddictServerIntegrationTests.Revocation.cs | 304 +++++++------ .../OpenIddictServerIntegrationTests.cs | 47 +- 17 files changed, 933 insertions(+), 1626 deletions(-) diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx index db1dd8f7b..82e4ffbea 100644 --- a/src/OpenIddict.Abstractions/OpenIddictResources.resx +++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx @@ -117,13 +117,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - An identity cannot be extracted from this token request. -This generally indicates that the OpenIddict server stack was asked to validate a token for an invalid grant type (e.g password). - An identity cannot be extracted from this request. -This generally indicates that the OpenIddict server stack was asked to validate a token for an endpoint it doesn't manage. +This generally indicates that the OpenIddict server stack was asked to authenticate a request for an endpoint it doesn't manage. To validate tokens received by custom API endpoints, the OpenIddict validation handler (e.g OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme or OpenIddictValidationOwinDefaults.AuthenticationType) must be used instead. @@ -426,19 +422,19 @@ To use key rollover, register both the new certificate and the old one in the cr No custom authorization request validation handler was found. When enabling the degraded mode, a custom 'IOpenIddictServerHandler<ValidateAuthorizationRequestContext>' must be implemented to validate authorization requests (e.g to ensure the client_id and redirect_uri are valid). - No custom device request validation handler was found. When enabling the degraded mode, a custom 'IOpenIddictServerHandler<ValidateDeviceRequestContext>' must be implemented to validate device requests (e.g to ensure the client_id and client_secret are valid). + No custom device request validation handler was found. When enabling the degraded mode, a custom 'IOpenIddictServerHandler<ValidateDeviceRequestContext>' (or 'IOpenIddictServerHandler<ProcessAuthenticationContext>') must be implemented to validate device requests (e.g to ensure the client_id and client_secret are valid). - No custom introspection request validation handler was found. When enabling the degraded mode, a custom 'IOpenIddictServerHandler<ValidateIntrospectionRequestContext>' must be implemented to validate introspection requests (e.g to ensure the client_id and client_secret are valid). + No custom introspection request validation handler was found. When enabling the degraded mode, a custom 'IOpenIddictServerHandler<ValidateIntrospectionRequestContext>' (or 'IOpenIddictServerHandler<ProcessAuthenticationContext>') must be implemented to validate introspection requests (e.g to ensure the client_id and client_secret are valid). No custom logout request validation handler was found. When enabling the degraded mode, a custom 'IOpenIddictServerHandler<ValidateLogoutRequestContext>' must be implemented to validate logout requests (e.g to ensure the post_logout_redirect_uri is valid). - No custom revocation request validation handler was found. When enabling the degraded mode, a custom 'IOpenIddictServerHandler<ValidateRevocationRequestContext>' must be implemented to validate revocation requests (e.g to ensure the client_id and client_secret are valid). + No custom revocation request validation handler was found. When enabling the degraded mode, a custom 'IOpenIddictServerHandler<ValidateRevocationRequestContext>' (or 'IOpenIddictServerHandler<ProcessAuthenticationContext>') must be implemented to validate revocation requests (e.g to ensure the client_id and client_secret are valid). - No custom token request validation handler was found. When enabling the degraded mode, a custom 'IOpenIddictServerHandler<ValidateTokenRequestContext>' must be implemented to validate token requests (e.g to ensure the client_id and client_secret are valid). + No custom token request validation handler was found. When enabling the degraded mode, a custom 'IOpenIddictServerHandler<ValidateTokenRequestContext>' (or 'IOpenIddictServerHandler<ProcessAuthenticationContext>') must be implemented to validate token requests (e.g to ensure the client_id and client_secret are valid). 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). @@ -2235,9 +2231,6 @@ The principal used to create the token contained the following claims: {Claims}. The authorization request was rejected because the specified response type was not compatible with PKCE. - - The authorization request was rejected because the client application was not found: '{ClientId}'. - The authorization request was rejected because the confidential application '{ClientId}' was not allowed to retrieve an access token from the authorization endpoint. @@ -2271,24 +2264,9 @@ The principal used to create the token contained the following claims: {Claims}. The device request was successfully validated. - - The device request was rejected because the mandatory '{Parameter}' parameter was missing. - The device request was rejected because invalid scopes were specified: {Scopes}. - - The device request was rejected because the client application was not found: '{ClientId}'. - - - The device request was rejected because the public application '{ClientId}' was not allowed to send a client secret. - - - The device request was rejected because the confidential application '{ClientId}' didn't specify a client secret. - - - The device request was rejected because the confidential application '{ClientId}' didn't specify valid client credentials. - The device request was rejected because the application '{ClientId}' was not allowed to use the device endpoint. @@ -2346,21 +2324,6 @@ The principal used to create the token contained the following claims: {Claims}. The token request was rejected because invalid scopes were specified: {Scopes}. - - The token request was rejected because the client application was not found: '{ClientId}'. - - - The token request was rejected because the public client application '{ClientId}' was not allowed to use the client credentials grant. - - - The token request was rejected because the public application '{ClientId}' was not allowed to send a client secret. - - - The token request was rejected because the confidential application '{ClientId}' didn't specify a client secret. - - - The token request was rejected because the confidential application '{ClientId}' didn't specify valid client credentials. - The token request was rejected because the application '{ClientId}' was not allowed to use the token endpoint. @@ -2400,18 +2363,6 @@ The principal used to create the token contained the following claims: {Claims}. The introspection request was rejected because the mandatory '{Parameter}' parameter was missing. - - The introspection request was rejected because the client application was not found: '{ClientId}'. - - - The introspection request was rejected because the public application '{ClientId}' was not allowed to send a client secret. - - - The introspection request was rejected because the confidential application '{ClientId}' didn't specify a client secret. - - - The introspection request was rejected because the confidential application '{ClientId}' didn't specify valid client credentials. - The introspection request was rejected because the application '{ClientId}' was not allowed to use the introspection endpoint. @@ -2439,18 +2390,6 @@ The principal used to create the token contained the following claims: {Claims}. The revocation request was rejected because the mandatory '{Parameter}' parameter was missing. - - The revocation request was rejected because the client application was not found: '{ClientId}'. - - - The revocation request was rejected because the public application '{ClientId}' was not allowed to send a client secret. - - - The revocation request was rejected because the confidential application '{ClientId}' didn't specify a client secret. - - - The revocation request was rejected because the confidential application '{ClientId}' didn't specify valid client credentials. - The revocation request was rejected because the application '{ClientId}' was not allowed to use the revocation endpoint. @@ -2689,9 +2628,6 @@ This may indicate that the hashed entry is corrupted or malformed. The userinfo response returned by {Uri} was successfully extracted: {Response}. - - The logout request was rejected because the client application was not found: '{ClientId}'. - The authorization request was rejected because the identity token used as a hint was issued to a different client. @@ -2758,6 +2694,24 @@ This may indicate that the hashed entry is corrupted or malformed. An error occurred while retrieving the configuration of the remote authorization server. + + The authentication demand was rejected because the mandatory '{Parameter}' parameter was missing. + + + The authentication demand was rejected because the client application was not found: '{ClientId}'. + + + The authentication demand was rejected because the public client application '{ClientId}' was not allowed to use the client credentials grant. + + + The authentication demand was rejected because the public application '{ClientId}' was not allowed to send a client secret. + + + The authentication demand was rejected because the confidential application '{ClientId}' didn't specify a client secret. + + + The authentication demand was rejected because the confidential application '{ClientId}' didn't specify valid client credentials. + https://documentation.openiddict.com/errors/{0} diff --git a/src/OpenIddict.Server/OpenIddictServerConfiguration.cs b/src/OpenIddict.Server/OpenIddictServerConfiguration.cs index 76e12fd47..33fdbc40c 100644 --- a/src/OpenIddict.Server/OpenIddictServerConfiguration.cs +++ b/src/OpenIddict.Server/OpenIddictServerConfiguration.cs @@ -178,7 +178,8 @@ public void PostConfigure(string? name, OpenIddictServerOptions options) } if (options.DeviceEndpointUris.Count is not 0 && !options.Handlers.Exists(static descriptor => - descriptor.ContextType == typeof(ValidateDeviceRequestContext) && + (descriptor.ContextType == typeof(ValidateDeviceRequestContext) || + descriptor.ContextType == typeof(ProcessAuthenticationContext)) && descriptor.Type == OpenIddictServerHandlerType.Custom && descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type)))) { @@ -186,7 +187,8 @@ public void PostConfigure(string? name, OpenIddictServerOptions options) } if (options.IntrospectionEndpointUris.Count is not 0 && !options.Handlers.Exists(static descriptor => - descriptor.ContextType == typeof(ValidateIntrospectionRequestContext) && + (descriptor.ContextType == typeof(ValidateIntrospectionRequestContext) || + descriptor.ContextType == typeof(ProcessAuthenticationContext)) && descriptor.Type == OpenIddictServerHandlerType.Custom && descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type)))) { @@ -202,7 +204,8 @@ public void PostConfigure(string? name, OpenIddictServerOptions options) } if (options.RevocationEndpointUris.Count is not 0 && !options.Handlers.Exists(static descriptor => - descriptor.ContextType == typeof(ValidateRevocationRequestContext) && + (descriptor.ContextType == typeof(ValidateRevocationRequestContext) || + descriptor.ContextType == typeof(ProcessAuthenticationContext)) && descriptor.Type == OpenIddictServerHandlerType.Custom && descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type)))) { @@ -210,7 +213,8 @@ public void PostConfigure(string? name, OpenIddictServerOptions options) } if (options.TokenEndpointUris.Count is not 0 && !options.Handlers.Exists(static descriptor => - descriptor.ContextType == typeof(ValidateTokenRequestContext) && + (descriptor.ContextType == typeof(ValidateTokenRequestContext) || + descriptor.ContextType == typeof(ProcessAuthenticationContext)) && descriptor.Type == OpenIddictServerHandlerType.Custom && descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type)))) { @@ -232,16 +236,16 @@ public void PostConfigure(string? name, OpenIddictServerOptions options) { if (!options.Handlers.Exists(static descriptor => descriptor.ContextType == typeof(ValidateTokenContext) && - descriptor.Type == OpenIddictServerHandlerType.Custom && - descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type)))) + descriptor.Type is OpenIddictServerHandlerType.Custom && + descriptor.FilterTypes.All(static type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type)))) { throw new InvalidOperationException(SR.GetResourceString(SR.ID0096)); } if (!options.Handlers.Exists(static descriptor => descriptor.ContextType == typeof(GenerateTokenContext) && - descriptor.Type == OpenIddictServerHandlerType.Custom && - descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type)))) + descriptor.Type is OpenIddictServerHandlerType.Custom && + descriptor.FilterTypes.All(static type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type)))) { throw new InvalidOperationException(SR.GetResourceString(SR.ID0097)); } @@ -249,11 +253,11 @@ public void PostConfigure(string? name, OpenIddictServerOptions options) } // Sort the handlers collection using the order associated with each handler. - options.Handlers.Sort((left, right) => left.Order.CompareTo(right.Order)); + options.Handlers.Sort(static (left, right) => left.Order.CompareTo(right.Order)); // Sort the encryption and signing credentials. - options.EncryptionCredentials.Sort((left, right) => Compare(left.Key, right.Key)); - options.SigningCredentials.Sort((left, right) => Compare(left.Key, right.Key)); + options.EncryptionCredentials.Sort(static (left, right) => Compare(left.Key, right.Key)); + options.SigningCredentials.Sort(static (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) diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.cs b/src/OpenIddict.Server/OpenIddictServerEvents.cs index 9f2149658..92a6d7a4c 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.cs +++ b/src/OpenIddict.Server/OpenIddictServerEvents.cs @@ -294,7 +294,7 @@ public OpenIddictResponse Response /// /// Represents an event called when processing an authentication operation. /// - public sealed class ProcessAuthenticationContext : BaseValidatingContext + public sealed class ProcessAuthenticationContext : BaseValidatingClientContext { /// /// Creates a new instance of the class. diff --git a/src/OpenIddict.Server/OpenIddictServerExtensions.cs b/src/OpenIddict.Server/OpenIddictServerExtensions.cs index c1c3f2a39..ff2b97bd7 100644 --- a/src/OpenIddict.Server/OpenIddictServerExtensions.cs +++ b/src/OpenIddict.Server/OpenIddictServerExtensions.cs @@ -48,6 +48,7 @@ public static OpenIddictServerBuilder AddServer(this OpenIddictBuilder builder) builder.Services.TryAddSingleton(); 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 800419cd9..e148a66a9 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs @@ -147,6 +147,23 @@ public ValueTask IsActiveAsync(BaseContext context) } } + /// + /// Represents a filter that excludes the associated handlers when no client secret is received. + /// + public sealed class RequireClientSecretParameter : IOpenIddictServerHandlerFilter + { + /// + public ValueTask IsActiveAsync(BaseContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new(!string.IsNullOrEmpty(context.Transaction.Request?.ClientSecret)); + } + } + /// /// Represents a filter that excludes the associated handlers if the request is not a configuration request. /// diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs index f35078be1..1be1daccf 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs @@ -42,8 +42,8 @@ public static class Authentication ValidateNonceParameter.Descriptor, ValidatePromptParameter.Descriptor, ValidateProofKeyForCodeExchangeParameters.Descriptor, - ValidateClientId.Descriptor, - ValidateClientType.Descriptor, + ValidateAuthentication.Descriptor, + ValidateResponseType.Descriptor, ValidateClientRedirectUri.Descriptor, ValidateScopes.Descriptor, ValidateEndpointPermissions.Descriptor, @@ -51,7 +51,6 @@ public static class Authentication ValidateResponseTypePermissions.Descriptor, ValidateScopePermissions.Descriptor, ValidateProofKeyForCodeExchangeRequirement.Descriptor, - ValidateToken.Descriptor, ValidateAuthorizedParty.Descriptor, /* @@ -1019,25 +1018,21 @@ public ValueTask HandleAsync(ValidateAuthorizationRequestContext context) } /// - /// Contains the logic responsible for rejecting authorization requests that use an invalid client_id. - /// Note: this handler is not used when the degraded mode is enabled. + /// Contains the logic responsible for applying the authentication logic to authorization requests. /// - public sealed class ValidateClientId : IOpenIddictServerHandler + public sealed class ValidateAuthentication : IOpenIddictServerHandler { - private readonly IOpenIddictApplicationManager _applicationManager; - - public ValidateClientId() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); + private readonly IOpenIddictServerDispatcher _dispatcher; - public ValidateClientId(IOpenIddictApplicationManager applicationManager) - => _applicationManager = applicationManager ?? throw new ArgumentNullException(nameof(applicationManager)); + public ValidateAuthentication(IOpenIddictServerDispatcher dispatcher) + => _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .UseScopedHandler() + .UseScopedHandler() .SetOrder(ValidateProofKeyForCodeExchangeParameters.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -1050,20 +1045,36 @@ public async ValueTask HandleAsync(ValidateAuthorizationRequestContext context) throw new ArgumentNullException(nameof(context)); } - Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId)); + 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); - var application = await _applicationManager.FindByClientIdAsync(context.ClientId); - if (application is null) + if (notification.IsRequestHandled) { - context.Logger.LogInformation(SR.GetResourceString(SR.ID6044), context.ClientId); + context.HandleRequest(); + return; + } - context.Reject( - error: Errors.InvalidRequest, - description: SR.FormatID2052(Parameters.ClientId), - uri: SR.FormatID8000(SR.ID2052)); + 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; } + + // Attach the security principal extracted from the token to the validation context. + context.IdentityTokenHintPrincipal = notification.IdentityTokenPrincipal; } } @@ -1073,13 +1084,13 @@ public async ValueTask HandleAsync(ValidateAuthorizationRequestContext context) /// Note: this handler is not used when the degraded mode is enabled /// or when response type permissions enforcement is not disabled. /// - public sealed class ValidateClientType : IOpenIddictServerHandler + public sealed class ValidateResponseType : IOpenIddictServerHandler { private readonly IOpenIddictApplicationManager _applicationManager; - public ValidateClientType() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); + public ValidateResponseType() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); - public ValidateClientType(IOpenIddictApplicationManager applicationManager) + public ValidateResponseType(IOpenIddictApplicationManager applicationManager) => _applicationManager = applicationManager ?? throw new ArgumentNullException(nameof(applicationManager)); /// @@ -1088,8 +1099,8 @@ public ValidateClientType(IOpenIddictApplicationManager applicationManager) public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() - .UseScopedHandler() - .SetOrder(ValidateClientId.Descriptor.Order + 1_000) + .UseScopedHandler() + .SetOrder(ValidateAuthentication.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -1151,7 +1162,7 @@ public ValidateClientRedirectUri(IOpenIddictApplicationManager applicationManage = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() .UseScopedHandler() - .SetOrder(ValidateClientType.Descriptor.Order + 1_000) + .SetOrder(ValidateResponseType.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -1635,67 +1646,6 @@ public async ValueTask HandleAsync(ValidateAuthorizationRequestContext context) } } - /// - /// Contains the logic responsible for validating the token(s) present in the request. - /// - public sealed class ValidateToken : IOpenIddictServerHandler - { - private readonly IOpenIddictServerDispatcher _dispatcher; - - public ValidateToken(IOpenIddictServerDispatcher dispatcher) - => _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseScopedHandler() - .SetOrder(ValidateProofKeyForCodeExchangeRequirement.Descriptor.Order + 1_000) - .SetType(OpenIddictServerHandlerType.BuiltIn) - .Build(); - - /// - public async ValueTask HandleAsync(ValidateAuthorizationRequestContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - 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(); - 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; - } - - // Attach the security principal extracted from the token to the validation context. - context.IdentityTokenHintPrincipal = notification.IdentityTokenPrincipal; - } - } - /// /// Contains the logic responsible for rejecting authorization requests that specify an identity /// token hint that cannot be used by the client application sending the authorization request. @@ -1708,7 +1658,7 @@ public sealed class ValidateAuthorizedParty : IOpenIddictServerHandler() .UseSingletonHandler() - .SetOrder(ValidateToken.Descriptor.Order + 1_000) + .SetOrder(ValidateProofKeyForCodeExchangeRequirement.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs index d57c6b5c8..63924eb76 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs @@ -33,12 +33,9 @@ public static class Device /* * Device request validation: */ - ValidateClientIdParameter.Descriptor, ValidateScopeParameter.Descriptor, ValidateScopes.Descriptor, - ValidateClientId.Descriptor, - ValidateClientType.Descriptor, - ValidateClientSecret.Descriptor, + ValidateDeviceAuthentication.Descriptor, ValidateEndpointPermissions.Descriptor, ValidateGrantTypePermissions.Descriptor, ValidateScopePermissions.Descriptor, @@ -57,7 +54,7 @@ public static class Device /* * Verification request validation: */ - ValidateToken.Descriptor, + ValidateVerificationAuthentication.Descriptor, /* * Verification request handling: @@ -332,47 +329,6 @@ public async ValueTask HandleAsync(TContext context) } } - /// - /// Contains the logic responsible for rejecting device requests that don't specify a client identifier. - /// - public sealed class ValidateClientIdParameter : 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(ValidateDeviceRequestContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - // client_id is a required parameter and MUST cause an error when missing. - // See https://tools.ietf.org/html/rfc8628#section-3.1 for more information. - if (string.IsNullOrEmpty(context.ClientId)) - { - context.Logger.LogInformation(SR.GetResourceString(SR.ID6056), Parameters.ClientId); - - context.Reject( - error: Errors.InvalidClient, - description: SR.FormatID2029(Parameters.ClientId), - uri: SR.FormatID8000(SR.ID2029)); - - return default; - } - - return default; - } - } - /// /// Contains the logic responsible for rejecting device requests that don't specify a valid scope parameter. /// @@ -384,7 +340,7 @@ public sealed class ValidateScopeParameter : IOpenIddictServerHandler() .UseSingletonHandler() - .SetOrder(ValidateClientIdParameter.Descriptor.Order + 1_000) + .SetOrder(int.MinValue + 100_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -491,26 +447,21 @@ public async ValueTask HandleAsync(ValidateDeviceRequestContext context) } /// - /// Contains the logic responsible for rejecting device requests that use an invalid client_id. - /// Note: this handler is not used when the degraded mode is enabled. + /// Contains the logic responsible for applying the authentication logic to device requests. /// - public sealed class ValidateClientId : IOpenIddictServerHandler + public sealed class ValidateDeviceAuthentication : IOpenIddictServerHandler { - private readonly IOpenIddictApplicationManager _applicationManager; - - public ValidateClientId() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); + private readonly IOpenIddictServerDispatcher _dispatcher; - public ValidateClientId(IOpenIddictApplicationManager applicationManager) - => _applicationManager = applicationManager ?? throw new ArgumentNullException(nameof(applicationManager)); + public ValidateDeviceAuthentication(IOpenIddictServerDispatcher dispatcher) + => _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .UseScopedHandler() + .UseScopedHandler() .SetOrder(ValidateScopes.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -523,152 +474,31 @@ public async ValueTask HandleAsync(ValidateDeviceRequestContext context) throw new ArgumentNullException(nameof(context)); } - Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId)); - - // Retrieve the application details corresponding to the requested client_id. - // If no entity can be found, this likely indicates that the client_id is invalid. - var application = await _applicationManager.FindByClientIdAsync(context.ClientId); - if (application is null) - { - context.Logger.LogInformation(SR.GetResourceString(SR.ID6058), context.ClientId); - - context.Reject( - error: Errors.InvalidClient, - description: SR.FormatID2052(Parameters.ClientId), - uri: SR.FormatID8000(SR.ID2052)); - - return; - } - } - } - - /// - /// Contains the logic responsible for rejecting device requests made by applications - /// whose client type is not compatible with the requested grant type. - /// Note: this handler is not used when the degraded mode is enabled. - /// - public sealed class ValidateClientType : IOpenIddictServerHandler - { - private readonly IOpenIddictApplicationManager _applicationManager; - - public ValidateClientType() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); - - public ValidateClientType(IOpenIddictApplicationManager applicationManager) - => _applicationManager = applicationManager ?? throw new ArgumentNullException(nameof(applicationManager)); - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .UseScopedHandler() - .SetOrder(ValidateClientId.Descriptor.Order + 1_000) - .SetType(OpenIddictServerHandlerType.BuiltIn) - .Build(); - - /// - public async ValueTask HandleAsync(ValidateDeviceRequestContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId)); - - var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ?? - throw new InvalidOperationException(SR.GetResourceString(SR.ID0032)); - - if (await _applicationManager.HasClientTypeAsync(application, ClientTypes.Public)) - { - // Reject device requests containing a client_secret when the client is a public application. - if (!string.IsNullOrEmpty(context.ClientSecret)) - { - context.Logger.LogInformation(SR.GetResourceString(SR.ID6059), context.ClientId); - - context.Reject( - error: Errors.InvalidClient, - description: SR.FormatID2053(Parameters.ClientSecret), - uri: SR.FormatID8000(SR.ID2053)); - - return; - } + var notification = new ProcessAuthenticationContext(context.Transaction); + await _dispatcher.DispatchAsync(notification); - return; - } + // 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); - // Confidential and hybrid applications MUST authenticate to protect them from impersonation attacks. - if (string.IsNullOrEmpty(context.ClientSecret)) + if (notification.IsRequestHandled) { - context.Logger.LogInformation(SR.GetResourceString(SR.ID6060), context.ClientId); - - context.Reject( - error: Errors.InvalidClient, - description: SR.FormatID2054(Parameters.ClientSecret), - uri: SR.FormatID8000(SR.ID2054)); - + context.HandleRequest(); return; } - } - } - /// - /// Contains the logic responsible for rejecting device requests specifying an invalid client secret. - /// Note: this handler is not used when the degraded mode is enabled. - /// - public sealed class ValidateClientSecret : IOpenIddictServerHandler - { - private readonly IOpenIddictApplicationManager _applicationManager; - - public ValidateClientSecret() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); - - public ValidateClientSecret(IOpenIddictApplicationManager applicationManager) - => _applicationManager = applicationManager ?? throw new ArgumentNullException(nameof(applicationManager)); - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .UseScopedHandler() - .SetOrder(ValidateClientType.Descriptor.Order + 1_000) - .SetType(OpenIddictServerHandlerType.BuiltIn) - .Build(); - - /// - public async ValueTask HandleAsync(ValidateDeviceRequestContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId)); - - var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ?? - throw new InvalidOperationException(SR.GetResourceString(SR.ID0032)); - - // If the application is a public client, don't validate the client secret. - if (await _applicationManager.HasClientTypeAsync(application, ClientTypes.Public)) + else if (notification.IsRequestSkipped) { + context.SkipRequest(); return; } - Debug.Assert(!string.IsNullOrEmpty(context.ClientSecret), SR.FormatID4000(Parameters.ClientSecret)); - - if (!await _applicationManager.ValidateClientSecretAsync(application, context.ClientSecret)) + else if (notification.IsRejected) { - context.Logger.LogInformation(SR.GetResourceString(SR.ID6061), context.ClientId); - context.Reject( - error: Errors.InvalidClient, - description: SR.GetResourceString(SR.ID2055), - uri: SR.FormatID8000(SR.ID2055)); - + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); return; } } @@ -697,7 +527,7 @@ public ValidateEndpointPermissions(IOpenIddictApplicationManager applicationMana .AddFilter() .AddFilter() .UseScopedHandler() - .SetOrder(ValidateClientSecret.Descriptor.Order + 1_000) + .SetOrder(ValidateDeviceAuthentication.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -1131,13 +961,13 @@ public async ValueTask HandleAsync(TContext context) } /// - /// Contains the logic responsible for validating the token(s) present in the request. + /// Contains the logic responsible for applying the authentication logic to verification requests. /// - public sealed class ValidateToken : IOpenIddictServerHandler + public sealed class ValidateVerificationAuthentication : IOpenIddictServerHandler { private readonly IOpenIddictServerDispatcher _dispatcher; - public ValidateToken(IOpenIddictServerDispatcher dispatcher) + public ValidateVerificationAuthentication(IOpenIddictServerDispatcher dispatcher) => _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); /// @@ -1145,7 +975,7 @@ public ValidateToken(IOpenIddictServerDispatcher dispatcher) /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseScopedHandler() + .UseScopedHandler() .SetOrder(int.MinValue + 100_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs index 6ddb26ad7..70300f417 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs @@ -46,14 +46,11 @@ public static class Exchange ValidateProofKeyForCodeExchangeParameters.Descriptor, ValidateScopeParameter.Descriptor, ValidateScopes.Descriptor, - ValidateClientId.Descriptor, - ValidateClientType.Descriptor, - ValidateClientSecret.Descriptor, + ValidateAuthentication.Descriptor, ValidateEndpointPermissions.Descriptor, ValidateGrantTypePermissions.Descriptor, ValidateScopePermissions.Descriptor, ValidateProofKeyForCodeExchangeRequirement.Descriptor, - ValidateToken.Descriptor, ValidatePresenters.Descriptor, ValidateRedirectUri.Descriptor, ValidateCodeVerifier.Descriptor, @@ -425,17 +422,12 @@ public ValueTask HandleAsync(ValidateTokenRequestContext context) throw new ArgumentNullException(nameof(context)); } - if (!string.IsNullOrEmpty(context.ClientId)) - { - return default; - } - - // At this stage, reject the token request unless the client identification requirement was disabled. - // Independently of this setting, also reject grant_type=authorization_code requests that don't specify - // a client_id, as the client identifier MUST be sent by the client application in the request body - // if it cannot be inferred from the client authentication method (e.g the username when using basic). + // Reject grant_type=authorization_code requests that don't specify a client_id, as the client + // identifier MUST be sent by the client application in the request body if it cannot be + // inferred from the client authentication method (e.g the username when using basic). + // // See https://tools.ietf.org/html/rfc6749#section-4.1.3 for more information. - if (!context.Options.AcceptAnonymousClients || context.Request.IsAuthorizationCodeGrantType()) + if (string.IsNullOrEmpty(context.ClientId) && context.Request.IsAuthorizationCodeGrantType()) { context.Logger.LogInformation(SR.GetResourceString(SR.ID6077), Parameters.ClientId); @@ -834,26 +826,21 @@ public async ValueTask HandleAsync(ValidateTokenRequestContext context) } /// - /// Contains the logic responsible for rejecting token requests that use an invalid client_id. - /// Note: this handler is not used when the degraded mode is enabled. + /// Contains the logic responsible for applying the authentication logic to token requests. /// - public sealed class ValidateClientId : IOpenIddictServerHandler + public sealed class ValidateAuthentication : IOpenIddictServerHandler { - private readonly IOpenIddictApplicationManager _applicationManager; - - public ValidateClientId() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); + private readonly IOpenIddictServerDispatcher _dispatcher; - public ValidateClientId(IOpenIddictApplicationManager applicationManager) - => _applicationManager = applicationManager ?? throw new ArgumentNullException(nameof(applicationManager)); + public ValidateAuthentication(IOpenIddictServerDispatcher dispatcher) + => _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .UseScopedHandler() + .UseScopedHandler() .SetOrder(ValidateScopes.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -866,167 +853,38 @@ public async ValueTask HandleAsync(ValidateTokenRequestContext context) throw new ArgumentNullException(nameof(context)); } - Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId)); - - // Retrieve the application details corresponding to the requested client_id. - // If no entity can be found, this likely indicates that the client_id is invalid. - var application = await _applicationManager.FindByClientIdAsync(context.ClientId); - if (application is null) - { - context.Logger.LogInformation(SR.GetResourceString(SR.ID6081), context.ClientId); - - context.Reject( - error: Errors.InvalidClient, - description: SR.FormatID2052(Parameters.ClientId), - uri: SR.FormatID8000(SR.ID2052)); - - return; - } - } - } - - /// - /// Contains the logic responsible for rejecting token requests made by applications - /// whose client type is not compatible with the requested grant type. - /// Note: this handler is not used when the degraded mode is enabled. - /// - public sealed class ValidateClientType : IOpenIddictServerHandler - { - private readonly IOpenIddictApplicationManager _applicationManager; - - public ValidateClientType() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); - - public ValidateClientType(IOpenIddictApplicationManager applicationManager) - => _applicationManager = applicationManager ?? throw new ArgumentNullException(nameof(applicationManager)); - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .UseScopedHandler() - .SetOrder(ValidateClientId.Descriptor.Order + 1_000) - .SetType(OpenIddictServerHandlerType.BuiltIn) - .Build(); - - /// - public async ValueTask HandleAsync(ValidateTokenRequestContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId)); - - var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ?? - throw new InvalidOperationException(SR.GetResourceString(SR.ID0032)); - - if (await _applicationManager.HasClientTypeAsync(application, ClientTypes.Public)) - { - // Public applications are not allowed to use the client credentials grant. - if (context.Request.IsClientCredentialsGrantType()) - { - context.Logger.LogInformation(SR.GetResourceString(SR.ID6082), context.Request.ClientId); - - context.Reject( - error: Errors.UnauthorizedClient, - description: SR.FormatID2043(Parameters.GrantType), - uri: SR.FormatID8000(SR.ID2043)); - - return; - } - - // Reject token requests containing a client_secret when the client is a public application. - if (!string.IsNullOrEmpty(context.ClientSecret)) - { - context.Logger.LogInformation(SR.GetResourceString(SR.ID6083), context.ClientId); - - context.Reject( - error: Errors.InvalidClient, - description: SR.FormatID2053(Parameters.ClientSecret), - uri: SR.FormatID8000(SR.ID2053)); - - return; - } + var notification = new ProcessAuthenticationContext(context.Transaction); + await _dispatcher.DispatchAsync(notification); - return; - } + // 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); - // Confidential and hybrid applications MUST authenticate to protect them from impersonation attacks. - if (string.IsNullOrEmpty(context.ClientSecret)) + if (notification.IsRequestHandled) { - context.Logger.LogInformation(SR.GetResourceString(SR.ID6084), context.ClientId); - - context.Reject( - error: Errors.InvalidClient, - description: SR.FormatID2054(Parameters.ClientSecret), - uri: SR.FormatID8000(SR.ID2054)); - + context.HandleRequest(); return; } - } - } - - /// - /// Contains the logic responsible for rejecting token requests specifying an invalid client secret. - /// Note: this handler is not used when the degraded mode is enabled. - /// - public sealed class ValidateClientSecret : IOpenIddictServerHandler - { - private readonly IOpenIddictApplicationManager _applicationManager; - - public ValidateClientSecret() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); - - public ValidateClientSecret(IOpenIddictApplicationManager applicationManager) - => _applicationManager = applicationManager ?? throw new ArgumentNullException(nameof(applicationManager)); - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .UseScopedHandler() - .SetOrder(ValidateClientType.Descriptor.Order + 1_000) - .SetType(OpenIddictServerHandlerType.BuiltIn) - .Build(); - - /// - public async ValueTask HandleAsync(ValidateTokenRequestContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId)); - - var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ?? - throw new InvalidOperationException(SR.GetResourceString(SR.ID0032)); - - // If the application is a public client, don't validate the client secret. - if (await _applicationManager.HasClientTypeAsync(application, ClientTypes.Public)) + else if (notification.IsRequestSkipped) { + context.SkipRequest(); return; } - Debug.Assert(!string.IsNullOrEmpty(context.ClientSecret), SR.FormatID4000(Parameters.ClientSecret)); - - if (!await _applicationManager.ValidateClientSecretAsync(application, context.ClientSecret)) + else if (notification.IsRejected) { - context.Logger.LogInformation(SR.GetResourceString(SR.ID6085), context.ClientId); - context.Reject( - error: Errors.InvalidClient, - description: SR.GetResourceString(SR.ID2055), - uri: SR.FormatID8000(SR.ID2055)); - + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); return; } + + // Attach the security principal extracted from the token to the validation context. + context.Principal = context.Request.IsAuthorizationCodeGrantType() ? notification.AuthorizationCodePrincipal : + context.Request.IsDeviceCodeGrantType() ? notification.DeviceCodePrincipal : + context.Request.IsRefreshTokenGrantType() ? notification.RefreshTokenPrincipal : null; } } @@ -1053,7 +911,7 @@ public ValidateEndpointPermissions(IOpenIddictApplicationManager applicationMana .AddFilter() .AddFilter() .UseScopedHandler() - .SetOrder(ValidateClientSecret.Descriptor.Order + 1_000) + .SetOrder(ValidateAuthentication.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -1285,76 +1143,6 @@ public async ValueTask HandleAsync(ValidateTokenRequestContext context) } } - /// - /// Contains the logic responsible for validating the token(s) present in the request. - /// - public sealed class ValidateToken : IOpenIddictServerHandler - { - private readonly IOpenIddictServerDispatcher _dispatcher; - - public ValidateToken(IOpenIddictServerDispatcher dispatcher) - => _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseScopedHandler() - .SetOrder(ValidateProofKeyForCodeExchangeRequirement.Descriptor.Order + 1_000) - .SetType(OpenIddictServerHandlerType.BuiltIn) - .Build(); - - /// - public async ValueTask HandleAsync(ValidateTokenRequestContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (!context.Request.IsAuthorizationCodeGrantType() && - !context.Request.IsDeviceCodeGrantType() && - !context.Request.IsRefreshTokenGrantType()) - { - return; - } - - 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(); - 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; - } - - // Attach the security principal extracted from the token to the validation context. - context.Principal = context.Request.IsAuthorizationCodeGrantType() ? notification.AuthorizationCodePrincipal : - context.Request.IsDeviceCodeGrantType() ? notification.DeviceCodePrincipal : - context.Request.IsRefreshTokenGrantType() ? notification.RefreshTokenPrincipal : null; - } - } - /// /// Contains the logic responsible for rejecting token requests that use an authorization code, /// a device code or a refresh token that was issued for a different client application. @@ -1367,7 +1155,7 @@ public sealed class ValidatePresenters : IOpenIddictServerHandler() .UseSingletonHandler() - .SetOrder(ValidateToken.Descriptor.Order + 1_000) + .SetOrder(ValidateProofKeyForCodeExchangeRequirement.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs index 5ccf74420..40148e74e 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs @@ -35,12 +35,8 @@ public static class Introspection * Introspection request validation: */ ValidateTokenParameter.Descriptor, - ValidateClientIdParameter.Descriptor, - ValidateClientId.Descriptor, - ValidateClientType.Descriptor, - ValidateClientSecret.Descriptor, + ValidateAuthentication.Descriptor, ValidateEndpointPermissions.Descriptor, - ValidateToken.Descriptor, ValidateTokenType.Descriptor, ValidateAuthorizedParty.Descriptor, @@ -371,120 +367,22 @@ public ValueTask HandleAsync(ValidateIntrospectionRequestContext context) } /// - /// Contains the logic responsible for rejecting introspection requests that don't specify a client identifier. + /// Contains the logic responsible for applying the authentication logic to introspection requests. /// - public sealed class ValidateClientIdParameter : IOpenIddictServerHandler + public sealed class ValidateAuthentication : IOpenIddictServerHandler { - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(ValidateTokenParameter.Descriptor.Order + 1_000) - .SetType(OpenIddictServerHandlerType.BuiltIn) - .Build(); - - /// - public ValueTask HandleAsync(ValidateIntrospectionRequestContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - // At this stage, reject the introspection request unless the client identification requirement was disabled. - if (!context.Options.AcceptAnonymousClients && string.IsNullOrEmpty(context.ClientId)) - { - context.Logger.LogInformation(SR.GetResourceString(SR.ID6098), Parameters.ClientId); - - context.Reject( - error: Errors.InvalidClient, - description: SR.FormatID2029(Parameters.ClientId), - uri: SR.FormatID8000(SR.ID2029)); - - return default; - } - - return default; - } - } - - /// - /// Contains the logic responsible for rejecting introspection requests that use an invalid client_id. - /// Note: this handler is not used when the degraded mode is enabled. - /// - public sealed class ValidateClientId : IOpenIddictServerHandler - { - private readonly IOpenIddictApplicationManager _applicationManager; - - public ValidateClientId() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); - - public ValidateClientId(IOpenIddictApplicationManager applicationManager) - => _applicationManager = applicationManager ?? throw new ArgumentNullException(nameof(applicationManager)); - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .UseScopedHandler() - .SetOrder(ValidateClientIdParameter.Descriptor.Order + 1_000) - .SetType(OpenIddictServerHandlerType.BuiltIn) - .Build(); - - /// - public async ValueTask HandleAsync(ValidateIntrospectionRequestContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId)); - - // Retrieve the application details corresponding to the requested client_id. - // If no entity can be found, this likely indicates that the client_id is invalid. - var application = await _applicationManager.FindByClientIdAsync(context.ClientId); - if (application is null) - { - context.Logger.LogInformation(SR.GetResourceString(SR.ID6099), context.ClientId); - - context.Reject( - error: Errors.InvalidClient, - description: SR.FormatID2052(Parameters.ClientId), - uri: SR.FormatID8000(SR.ID2052)); - - return; - } - } - } - - /// - /// Contains the logic responsible for rejecting introspection requests made by applications - /// whose client type is not compatible with the presence or absence of a client secret. - /// Note: this handler is not used when the degraded mode is enabled. - /// - public sealed class ValidateClientType : IOpenIddictServerHandler - { - private readonly IOpenIddictApplicationManager _applicationManager; - - public ValidateClientType() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); + private readonly IOpenIddictServerDispatcher _dispatcher; - public ValidateClientType(IOpenIddictApplicationManager applicationManager) - => _applicationManager = applicationManager ?? throw new ArgumentNullException(nameof(applicationManager)); + public ValidateAuthentication(IOpenIddictServerDispatcher dispatcher) + => _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .UseScopedHandler() - .SetOrder(ValidateClientId.Descriptor.Order + 1_000) + .UseScopedHandler() + .SetOrder(ValidateTokenParameter.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -496,101 +394,36 @@ public async ValueTask HandleAsync(ValidateIntrospectionRequestContext context) throw new ArgumentNullException(nameof(context)); } - Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId)); - - var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ?? - throw new InvalidOperationException(SR.GetResourceString(SR.ID0032)); - - if (await _applicationManager.HasClientTypeAsync(application, ClientTypes.Public)) - { - // Reject introspection requests containing a client_secret when the client is a public application. - if (!string.IsNullOrEmpty(context.ClientSecret)) - { - context.Logger.LogInformation(SR.GetResourceString(SR.ID6100), context.ClientId); - - context.Reject( - error: Errors.InvalidClient, - description: SR.FormatID2053(Parameters.ClientSecret), - uri: SR.FormatID8000(SR.ID2053)); - - return; - } + var notification = new ProcessAuthenticationContext(context.Transaction); + await _dispatcher.DispatchAsync(notification); - return; - } + // 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); - // Confidential and hybrid applications MUST authenticate to protect them from impersonation attacks. - if (string.IsNullOrEmpty(context.ClientSecret)) + if (notification.IsRequestHandled) { - context.Logger.LogInformation(SR.GetResourceString(SR.ID6101), context.ClientId); - - context.Reject( - error: Errors.InvalidClient, - description: SR.FormatID2054(Parameters.ClientSecret), - uri: SR.FormatID8000(SR.ID2054)); - + context.HandleRequest(); return; } - } - } - /// - /// Contains the logic responsible for rejecting introspection requests specifying an invalid client secret. - /// Note: this handler is not used when the degraded mode is enabled. - /// - public sealed class ValidateClientSecret : IOpenIddictServerHandler - { - private readonly IOpenIddictApplicationManager _applicationManager; - - public ValidateClientSecret() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); - - public ValidateClientSecret(IOpenIddictApplicationManager applicationManager) - => _applicationManager = applicationManager ?? throw new ArgumentNullException(nameof(applicationManager)); - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .UseScopedHandler() - .SetOrder(ValidateClientType.Descriptor.Order + 1_000) - .SetType(OpenIddictServerHandlerType.BuiltIn) - .Build(); - - /// - public async ValueTask HandleAsync(ValidateIntrospectionRequestContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId)); - - var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ?? - throw new InvalidOperationException(SR.GetResourceString(SR.ID0032)); - - // If the application is a public client, don't validate the client secret. - if (await _applicationManager.HasClientTypeAsync(application, ClientTypes.Public)) + else if (notification.IsRequestSkipped) { + context.SkipRequest(); return; } - Debug.Assert(!string.IsNullOrEmpty(context.ClientSecret), SR.FormatID4000(Parameters.ClientSecret)); - - if (!await _applicationManager.ValidateClientSecretAsync(application, context.ClientSecret)) + else if (notification.IsRejected) { - context.Logger.LogInformation(SR.GetResourceString(SR.ID6102), context.ClientId); - context.Reject( - error: Errors.InvalidClient, - description: SR.GetResourceString(SR.ID2055), - uri: SR.FormatID8000(SR.ID2055)); - + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); return; } + + // Attach the security principal extracted from the token to the validation context. + context.Principal = notification.GenericTokenPrincipal; } } @@ -617,7 +450,7 @@ public ValidateEndpointPermissions(IOpenIddictApplicationManager applicationMana .AddFilter() .AddFilter() .UseScopedHandler() - .SetOrder(ValidateClientSecret.Descriptor.Order + 1_000) + .SetOrder(ValidateAuthentication.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -649,67 +482,6 @@ public async ValueTask HandleAsync(ValidateIntrospectionRequestContext context) } } - /// - /// Contains the logic responsible for validating the token(s) present in the request. - /// - public sealed class ValidateToken : IOpenIddictServerHandler - { - private readonly IOpenIddictServerDispatcher _dispatcher; - - public ValidateToken(IOpenIddictServerDispatcher dispatcher) - => _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseScopedHandler() - .SetOrder(ValidateEndpointPermissions.Descriptor.Order + 1_000) - .SetType(OpenIddictServerHandlerType.BuiltIn) - .Build(); - - /// - public async ValueTask HandleAsync(ValidateIntrospectionRequestContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - 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(); - 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; - } - - // Attach the security principal extracted from the token to the validation context. - context.Principal = notification.GenericTokenPrincipal; - } - } - /// /// Contains the logic responsible for rejecting introspection requests that specify an unsupported token. /// @@ -721,7 +493,7 @@ public sealed class ValidateTokenType : IOpenIddictServerHandler() .UseSingletonHandler() - .SetOrder(ValidateToken.Descriptor.Order + 1_000) + .SetOrder(ValidateEndpointPermissions.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs index cfbe50c59..7765d30a1 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs @@ -29,12 +29,8 @@ public static class Revocation * Revocation request validation: */ ValidateTokenParameter.Descriptor, - ValidateClientIdParameter.Descriptor, - ValidateClientId.Descriptor, - ValidateClientType.Descriptor, - ValidateClientSecret.Descriptor, + ValidateAuthentication.Descriptor, ValidateEndpointPermissions.Descriptor, - ValidateToken.Descriptor, ValidateTokenType.Descriptor, ValidateAuthorizedParty.Descriptor, @@ -318,120 +314,22 @@ public ValueTask HandleAsync(ValidateRevocationRequestContext context) } /// - /// Contains the logic responsible for rejecting revocation requests that don't specify a client identifier. + /// Contains the logic responsible for applying the authentication logic to revocation requests. /// - public sealed class ValidateClientIdParameter : IOpenIddictServerHandler + public sealed class ValidateAuthentication : IOpenIddictServerHandler { - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(ValidateTokenParameter.Descriptor.Order + 1_000) - .SetType(OpenIddictServerHandlerType.BuiltIn) - .Build(); - - /// - public ValueTask HandleAsync(ValidateRevocationRequestContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - // At this stage, reject the revocation request unless the client identification requirement was disabled. - if (!context.Options.AcceptAnonymousClients && string.IsNullOrEmpty(context.ClientId)) - { - context.Logger.LogInformation(SR.GetResourceString(SR.ID6111), Parameters.ClientId); - - context.Reject( - error: Errors.InvalidClient, - description: SR.FormatID2029(Parameters.ClientId), - uri: SR.FormatID8000(SR.ID2029)); - - return default; - } - - return default; - } - } - - /// - /// Contains the logic responsible for rejecting revocation requests that use an invalid client_id. - /// Note: this handler is not used when the degraded mode is enabled. - /// - public sealed class ValidateClientId : IOpenIddictServerHandler - { - private readonly IOpenIddictApplicationManager _applicationManager; - - public ValidateClientId() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); - - public ValidateClientId(IOpenIddictApplicationManager applicationManager) - => _applicationManager = applicationManager ?? throw new ArgumentNullException(nameof(applicationManager)); - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .UseScopedHandler() - .SetOrder(ValidateClientIdParameter.Descriptor.Order + 1_000) - .SetType(OpenIddictServerHandlerType.BuiltIn) - .Build(); - - /// - public async ValueTask HandleAsync(ValidateRevocationRequestContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId)); - - // Retrieve the application details corresponding to the requested client_id. - // If no entity can be found, this likely indicates that the client_id is invalid. - var application = await _applicationManager.FindByClientIdAsync(context.ClientId); - if (application is null) - { - context.Logger.LogInformation(SR.GetResourceString(SR.ID6112), context.ClientId); - - context.Reject( - error: Errors.InvalidClient, - description: SR.FormatID2052(Parameters.ClientId), - uri: SR.FormatID8000(SR.ID2052)); - - return; - } - } - } - - /// - /// Contains the logic responsible for rejecting revocation requests made by applications - /// whose client type is not compatible with the presence or absence of a client secret. - /// Note: this handler is not used when the degraded mode is enabled. - /// - public sealed class ValidateClientType : IOpenIddictServerHandler - { - private readonly IOpenIddictApplicationManager _applicationManager; - - public ValidateClientType() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); + private readonly IOpenIddictServerDispatcher _dispatcher; - public ValidateClientType(IOpenIddictApplicationManager applicationManager) - => _applicationManager = applicationManager ?? throw new ArgumentNullException(nameof(applicationManager)); + public ValidateAuthentication(IOpenIddictServerDispatcher dispatcher) + => _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .UseScopedHandler() - .SetOrder(ValidateClientId.Descriptor.Order + 1_000) + .UseScopedHandler() + .SetOrder(ValidateTokenParameter.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -443,101 +341,36 @@ public async ValueTask HandleAsync(ValidateRevocationRequestContext context) throw new ArgumentNullException(nameof(context)); } - Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId)); - - var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ?? - throw new InvalidOperationException(SR.GetResourceString(SR.ID0032)); - - if (await _applicationManager.HasClientTypeAsync(application, ClientTypes.Public)) - { - // Reject revocation requests containing a client_secret when the client is a public application. - if (!string.IsNullOrEmpty(context.ClientSecret)) - { - context.Logger.LogInformation(SR.GetResourceString(SR.ID6113), context.ClientId); - - context.Reject( - error: Errors.InvalidClient, - description: SR.FormatID2053(Parameters.ClientSecret), - uri: SR.FormatID8000(SR.ID2053)); - - return; - } + var notification = new ProcessAuthenticationContext(context.Transaction); + await _dispatcher.DispatchAsync(notification); - return; - } + // 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); - // Confidential and hybrid applications MUST authenticate to protect them from impersonation attacks. - if (string.IsNullOrEmpty(context.ClientSecret)) + if (notification.IsRequestHandled) { - context.Logger.LogInformation(SR.GetResourceString(SR.ID6114), context.ClientId); - - context.Reject( - error: Errors.InvalidClient, - description: SR.FormatID2054(Parameters.ClientSecret), - uri: SR.FormatID8000(SR.ID2054)); - + context.HandleRequest(); return; } - } - } - /// - /// Contains the logic responsible for rejecting revocation requests specifying an invalid client secret. - /// Note: this handler is not used when the degraded mode is enabled. - /// - public sealed class ValidateClientSecret : IOpenIddictServerHandler - { - private readonly IOpenIddictApplicationManager _applicationManager; - - public ValidateClientSecret() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); - - public ValidateClientSecret(IOpenIddictApplicationManager applicationManager) - => _applicationManager = applicationManager ?? throw new ArgumentNullException(nameof(applicationManager)); - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .UseScopedHandler() - .SetOrder(ValidateClientType.Descriptor.Order + 1_000) - .SetType(OpenIddictServerHandlerType.BuiltIn) - .Build(); - - /// - public async ValueTask HandleAsync(ValidateRevocationRequestContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId)); - - var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ?? - throw new InvalidOperationException(SR.GetResourceString(SR.ID0032)); - - // If the application is a public client, don't validate the client secret. - if (await _applicationManager.HasClientTypeAsync(application, ClientTypes.Public)) + else if (notification.IsRequestSkipped) { + context.SkipRequest(); return; } - Debug.Assert(!string.IsNullOrEmpty(context.ClientSecret), SR.FormatID4000(Parameters.ClientSecret)); - - if (!await _applicationManager.ValidateClientSecretAsync(application, context.ClientSecret)) + else if (notification.IsRejected) { - context.Logger.LogInformation(SR.GetResourceString(SR.ID6115), context.ClientId); - context.Reject( - error: Errors.InvalidClient, - description: SR.GetResourceString(SR.ID2055), - uri: SR.FormatID8000(SR.ID2055)); - + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); return; } + + // Attach the security principal extracted from the token to the validation context. + context.Principal = notification.GenericTokenPrincipal; } } @@ -564,7 +397,7 @@ public ValidateEndpointPermissions(IOpenIddictApplicationManager applicationMana .AddFilter() .AddFilter() .UseScopedHandler() - .SetOrder(ValidateClientSecret.Descriptor.Order + 1_000) + .SetOrder(ValidateAuthentication.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -596,67 +429,6 @@ public async ValueTask HandleAsync(ValidateRevocationRequestContext context) } } - /// - /// Contains the logic responsible for validating the token(s) present in the request. - /// - public sealed class ValidateToken : IOpenIddictServerHandler - { - private readonly IOpenIddictServerDispatcher _dispatcher; - - public ValidateToken(IOpenIddictServerDispatcher dispatcher) - => _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseScopedHandler() - .SetOrder(ValidateEndpointPermissions.Descriptor.Order + 1_000) - .SetType(OpenIddictServerHandlerType.BuiltIn) - .Build(); - - /// - public async ValueTask HandleAsync(ValidateRevocationRequestContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - 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(); - 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; - } - - // Attach the security principal extracted from the token to the validation context. - context.Principal = notification.GenericTokenPrincipal; - } - } - /// /// Contains the logic responsible for rejecting revocation requests that specify an unsupported token. /// @@ -668,7 +440,7 @@ public sealed class ValidateTokenType : IOpenIddictServerHandler() .UseSingletonHandler() - .SetOrder(ValidateToken.Descriptor.Order + 1_000) + .SetOrder(ValidateEndpointPermissions.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs index 50d2b58d7..6ff8c03a4 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs @@ -33,10 +33,9 @@ public static class Session * Logout request validation: */ ValidatePostLogoutRedirectUriParameter.Descriptor, - ValidateClientId.Descriptor, + ValidateAuthentication.Descriptor, ValidateClientPostLogoutRedirectUri.Descriptor, ValidateEndpointPermissions.Descriptor, - ValidateToken.Descriptor, ValidateAuthorizedParty.Descriptor, /* @@ -375,30 +374,21 @@ public ValueTask HandleAsync(ValidateLogoutRequestContext context) } /// - /// Contains the logic responsible for rejecting logout requests - /// that use an invalid client_id, if one was explicitly specified. - /// Note: this handler is not used when the degraded mode is enabled. + /// Contains the logic responsible for applying the authentication logic to logout requests. /// - public sealed class ValidateClientId : IOpenIddictServerHandler + public sealed class ValidateAuthentication : IOpenIddictServerHandler { - private readonly IOpenIddictApplicationManager _applicationManager; - - public ValidateClientId() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); + private readonly IOpenIddictServerDispatcher _dispatcher; - public ValidateClientId(IOpenIddictApplicationManager applicationManager) - => _applicationManager = applicationManager ?? throw new ArgumentNullException(nameof(applicationManager)); + public ValidateAuthentication(IOpenIddictServerDispatcher dispatcher) + => _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - // Note: support for the client_id parameter was only added in the second draft of the - // https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout specification - // and is optional. As such, the client identifier is only validated if it was specified. - .AddFilter() - .UseScopedHandler() + .UseScopedHandler() .SetOrder(ValidatePostLogoutRedirectUriParameter.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -411,20 +401,36 @@ public async ValueTask HandleAsync(ValidateLogoutRequestContext context) throw new ArgumentNullException(nameof(context)); } - Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId)); + 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); - var application = await _applicationManager.FindByClientIdAsync(context.ClientId); - if (application is null) + if (notification.IsRequestHandled) { - context.Logger.LogInformation(SR.GetResourceString(SR.ID6196), context.ClientId); + context.HandleRequest(); + return; + } - context.Reject( - error: Errors.InvalidRequest, - description: SR.FormatID2052(Parameters.ClientId), - uri: SR.FormatID8000(SR.ID2052)); + 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; } + + // Attach the security principal extracted from the token to the validation context. + context.IdentityTokenHintPrincipal = notification.IdentityTokenPrincipal; } } @@ -450,7 +456,7 @@ public ValidateClientPostLogoutRedirectUri(IOpenIddictApplicationManager applica .AddFilter() .AddFilter() .UseScopedHandler() - .SetOrder(ValidateClientId.Descriptor.Order + 1_000) + .SetOrder(ValidateAuthentication.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -629,67 +635,6 @@ public async ValueTask HandleAsync(ValidateLogoutRequestContext context) } } - /// - /// Contains the logic responsible for validating the token(s) present in the request. - /// - public sealed class ValidateToken : IOpenIddictServerHandler - { - private readonly IOpenIddictServerDispatcher _dispatcher; - - public ValidateToken(IOpenIddictServerDispatcher dispatcher) - => _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseScopedHandler() - .SetOrder(ValidateEndpointPermissions.Descriptor.Order + 1_000) - .SetType(OpenIddictServerHandlerType.BuiltIn) - .Build(); - - /// - public async ValueTask HandleAsync(ValidateLogoutRequestContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - 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(); - 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; - } - - // Attach the security principal extracted from the token to the validation context. - context.IdentityTokenHintPrincipal = notification.IdentityTokenPrincipal; - } - } - /// /// Contains the logic responsible for rejecting logout requests that specify an identity /// token hint that cannot be used by the client application sending the logout request. @@ -717,7 +662,7 @@ public ValidateAuthorizedParty(IOpenIddictApplicationManager? applicationManager new ValidateAuthorizedParty(provider.GetService() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0016))); }) - .SetOrder(ValidateToken.Descriptor.Order + 1_000) + .SetOrder(ValidateEndpointPermissions.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs index 0a6e03efb..bf4b5252c 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs @@ -30,7 +30,7 @@ public static class Userinfo * Userinfo request validation: */ ValidateAccessTokenParameter.Descriptor, - ValidateToken.Descriptor, + ValidateAuthentication.Descriptor, /* * Userinfo request handling: @@ -342,13 +342,13 @@ public ValueTask HandleAsync(ValidateUserinfoRequestContext context) } /// - /// Contains the logic responsible for validating the token(s) present in the request. + /// Contains the logic responsible for applying the authentication logic to userinfo requests. /// - public sealed class ValidateToken : IOpenIddictServerHandler + public sealed class ValidateAuthentication : IOpenIddictServerHandler { private readonly IOpenIddictServerDispatcher _dispatcher; - public ValidateToken(IOpenIddictServerDispatcher dispatcher) + public ValidateAuthentication(IOpenIddictServerDispatcher dispatcher) => _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); /// @@ -356,7 +356,7 @@ public ValidateToken(IOpenIddictServerDispatcher dispatcher) /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseScopedHandler() + .UseScopedHandler() .SetOrder(ValidateAccessTokenParameter.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs index 5f1756081..419c6abaf 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs @@ -34,6 +34,9 @@ public static partial class OpenIddictServerHandlers EvaluateValidatedTokens.Descriptor, ResolveValidatedTokens.Descriptor, ValidateRequiredTokens.Descriptor, + ValidateClientId.Descriptor, + ValidateClientType.Descriptor, + ValidateClientSecret.Descriptor, ValidateAccessToken.Descriptor, ValidateAuthorizationCode.Descriptor, ValidateDeviceCode.Descriptor, @@ -52,8 +55,8 @@ public static partial class OpenIddictServerHandlers AttachCustomChallengeParameters.Descriptor, /* - * Sign-in processing: - */ + * Sign-in processing: + */ ValidateSignInDemand.Descriptor, RedeemTokenEntry.Descriptor, RestoreInternalClaims.Descriptor, @@ -224,24 +227,16 @@ public ValueTask HandleAsync(ProcessAuthenticationContext context) throw new ArgumentNullException(nameof(context)); } - switch (context.EndpointType) + return context.EndpointType switch { - case OpenIddictServerEndpointType.Authorization: - case OpenIddictServerEndpointType.Introspection: - case OpenIddictServerEndpointType.Logout: - case OpenIddictServerEndpointType.Revocation: - case OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType(): - case OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType(): - case OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType(): - case OpenIddictServerEndpointType.Userinfo: - case OpenIddictServerEndpointType.Verification: - return default; + OpenIddictServerEndpointType.Authorization or OpenIddictServerEndpointType.Device or + OpenIddictServerEndpointType.Introspection or OpenIddictServerEndpointType.Logout or + OpenIddictServerEndpointType.Revocation or OpenIddictServerEndpointType.Token or + OpenIddictServerEndpointType.Userinfo or OpenIddictServerEndpointType.Verification + => default, - case OpenIddictServerEndpointType.Token: - throw new InvalidOperationException(SR.GetResourceString(SR.ID0001)); - - default: throw new InvalidOperationException(SR.GetResourceString(SR.ID0002)); - } + _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0002)), + }; } } @@ -361,7 +356,7 @@ OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType( } /// - /// Contains the logic responsible for resolving the token from the incoming request. + /// Contains the logic responsible for resolving the tokens from the incoming request. /// public sealed class ResolveValidatedTokens : IOpenIddictServerHandler { @@ -490,6 +485,273 @@ public ValueTask HandleAsync(ProcessAuthenticationContext context) } } + /// + /// Contains the logic responsible for rejecting authentication demands that use an invalid client_id. + /// + public sealed class ValidateClientId : IOpenIddictServerHandler + { + private readonly IOpenIddictApplicationManager? _applicationManager; + + public ValidateClientId(IOpenIddictApplicationManager? applicationManager = null) + => _applicationManager = applicationManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler(static provider => + { + // Note: the application manager is only resolved if the degraded mode was not enabled to ensure + // invalid core configuration exceptions are not thrown even if the managers were registered. + var options = provider.GetRequiredService>().CurrentValue; + + return options.EnableDegradedMode ? + new ValidateClientId() : + new ValidateClientId(provider.GetService() ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0016))); + }) + .SetOrder(ValidateRequiredTokens.Descriptor.Order + 1_000) + .SetType(OpenIddictServerHandlerType.BuiltIn) + .Build(); + + /// + public async ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Don't validate the client identifier on endpoint that don't support client identification. + if (context.EndpointType is OpenIddictServerEndpointType.Userinfo or OpenIddictServerEndpointType.Verification) + { + return; + } + + if (string.IsNullOrEmpty(context.ClientId)) + { + switch (context.EndpointType) + { + // Note: support for the client_id parameter was only added in the second draft of the + // https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout specification + // and is optional. As such, the client identifier is only validated if it was specified. + case OpenIddictServerEndpointType.Logout: + return; + + case OpenIddictServerEndpointType.Introspection when context.Options.AcceptAnonymousClients: + case OpenIddictServerEndpointType.Revocation when context.Options.AcceptAnonymousClients: + case OpenIddictServerEndpointType.Token when context.Options.AcceptAnonymousClients: + return; + } + + context.Logger.LogInformation(SR.GetResourceString(SR.ID6220), Parameters.ClientId); + + context.Reject( + error: Errors.InvalidClient, + description: SR.FormatID2029(Parameters.ClientId), + uri: SR.FormatID8000(SR.ID2029)); + + return; + } + + if (!context.Options.EnableDegradedMode) + { + if (_applicationManager is null) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); + } + + // Retrieve the application details corresponding to the requested client_id. + // If no entity can be found, this likely indicates that the client_id is invalid. + var application = await _applicationManager.FindByClientIdAsync(context.ClientId); + if (application is null) + { + context.Logger.LogInformation(SR.GetResourceString(SR.ID6221), context.ClientId); + + context.Reject( + error: context.EndpointType switch + { + // For non-interactive endpoints, return "invalid_client" instead of "invalid_request". + OpenIddictServerEndpointType.Device or OpenIddictServerEndpointType.Introspection or + OpenIddictServerEndpointType.Revocation or OpenIddictServerEndpointType.Token + => Errors.InvalidClient, + + _ => Errors.InvalidRequest + }, + description: SR.FormatID2052(Parameters.ClientId), + uri: SR.FormatID8000(SR.ID2052)); + + return; + } + } + } + } + + /// + /// Contains the logic responsible for rejecting authentication demands made by applications + /// whose client type is not compatible with the presence of client credentials. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public sealed class ValidateClientType : IOpenIddictServerHandler + { + private readonly IOpenIddictApplicationManager _applicationManager; + + public ValidateClientType() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); + + public ValidateClientType(IOpenIddictApplicationManager applicationManager) + => _applicationManager = applicationManager ?? throw new ArgumentNullException(nameof(applicationManager)); + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateClientId.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(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId)); + + // Don't validate the client type on endpoint that don't support client authentication. + if (context.EndpointType is OpenIddictServerEndpointType.Authorization or + OpenIddictServerEndpointType.Logout or + OpenIddictServerEndpointType.Userinfo or + OpenIddictServerEndpointType.Verification) + { + return; + } + + var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0032)); + + if (await _applicationManager.HasClientTypeAsync(application, ClientTypes.Public)) + { + // Reject grant_type=client_credentials token requests if the application is a public client. + if (context.EndpointType is OpenIddictServerEndpointType.Token && + context.Request.IsClientCredentialsGrantType()) + { + context.Logger.LogInformation(SR.GetResourceString(SR.ID6222), context.Request.ClientId); + + context.Reject( + error: Errors.UnauthorizedClient, + description: SR.FormatID2043(Parameters.GrantType), + uri: SR.FormatID8000(SR.ID2043)); + + return; + } + + // Reject requests containing a client_secret when the client is a public application. + if (!string.IsNullOrEmpty(context.ClientSecret)) + { + context.Logger.LogInformation(SR.GetResourceString(SR.ID6223), context.ClientId); + + context.Reject( + error: Errors.InvalidClient, + description: SR.FormatID2053(Parameters.ClientSecret), + uri: SR.FormatID8000(SR.ID2053)); + + return; + } + + return; + } + + // Confidential and hybrid applications MUST authenticate to protect them from impersonation attacks. + if (string.IsNullOrEmpty(context.ClientSecret)) + { + context.Logger.LogInformation(SR.GetResourceString(SR.ID6224), context.ClientId); + + context.Reject( + error: Errors.InvalidClient, + description: SR.FormatID2054(Parameters.ClientSecret), + uri: SR.FormatID8000(SR.ID2054)); + + return; + } + } + } + + /// + /// Contains the logic responsible for rejecting authentication demands specifying an invalid client secret. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public sealed class ValidateClientSecret : IOpenIddictServerHandler + { + private readonly IOpenIddictApplicationManager _applicationManager; + + public ValidateClientSecret() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); + + public ValidateClientSecret(IOpenIddictApplicationManager applicationManager) + => _applicationManager = applicationManager ?? throw new ArgumentNullException(nameof(applicationManager)); + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateClientType.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(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId)); + Debug.Assert(!string.IsNullOrEmpty(context.ClientSecret), SR.FormatID4000(Parameters.ClientSecret)); + + // Don't validate the client secret on endpoint that don't support client authentication. + if (context.EndpointType is OpenIddictServerEndpointType.Authorization or + OpenIddictServerEndpointType.Logout or + OpenIddictServerEndpointType.Userinfo or + OpenIddictServerEndpointType.Verification) + { + return; + } + + var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0032)); + + // If the application is a public client, don't validate the client secret. + if (await _applicationManager.HasClientTypeAsync(application, ClientTypes.Public)) + { + return; + } + + if (!await _applicationManager.ValidateClientSecretAsync(application, context.ClientSecret)) + { + context.Logger.LogInformation(SR.GetResourceString(SR.ID6225), context.ClientId); + + context.Reject( + error: Errors.InvalidClient, + description: SR.GetResourceString(SR.ID2055), + uri: SR.FormatID8000(SR.ID2055)); + + return; + } + } + } + /// /// Contains the logic responsible for validating the access token resolved from the context. /// @@ -507,7 +769,7 @@ public ValidateAccessToken(IOpenIddictServerDispatcher dispatcher) = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() .UseScopedHandler() - .SetOrder(ValidateRequiredTokens.Descriptor.Order + 1_000) + .SetOrder(ValidateClientSecret.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs index dfc86190b..7671fd22a 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs @@ -1846,7 +1846,31 @@ public async Task ValidateTokenRequest_RequestIsRejectedWhenCodeVerifierIsMissin await using var server = await CreateServerAsync(options => { + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("SplxlOBeZQQYbYS6WxSbIA", context.Token); + Assert.Equal(new[] { TokenTypeHints.AuthorizationCode }, context.ValidTokenTypes); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetTokenType(TokenTypeHints.AuthorizationCode) + .SetPresenters("Fabrikam") + .SetTokenId("3E228451-1555-46F7-A471-951EFBA23A56") + .SetClaim(Claims.Subject, "Bob le Bricoleur"); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + options.Services.AddSingleton(manager); + + options.SetDeviceEndpointUris(Array.Empty()); + options.SetRevocationEndpointUris(Array.Empty()); + options.Configure(options => options.GrantTypes.Remove(GrantTypes.DeviceCode)); + options.DisableTokenStorage(); }); await using var client = await server.CreateClientAsync(); diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Introspection.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Introspection.cs index 91b5ba24f..3f718904a 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Introspection.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Introspection.cs @@ -152,14 +152,12 @@ public async Task ValidateIntrospectionRequest_MissingTokenCausesAnError() } [Fact] - public async Task ValidateIntrospectionRequest_InvalidTokenCausesAnError() + public async Task ValidateIntrospectionRequest_RequestWithoutClientIdIsRejectedWhenClientIdentificationIsRequired() { // Arrange await using var server = await CreateServerAsync(options => { - options.EnableDegradedMode(); - - options.RemoveEventHandler(NormalizeErrorResponse.Descriptor); + options.Configure(options => options.AcceptAnonymousClients = false); }); await using var client = await server.CreateClientAsync(); @@ -167,86 +165,28 @@ public async Task ValidateIntrospectionRequest_InvalidTokenCausesAnError() // Act var response = await client.PostAsync("/connect/introspect", new OpenIddictRequest { - Token = "SlAV32hkKG" + Token = "2YotnFZFEjr1zCsicMWpAA" }); // Assert - Assert.Equal(Errors.InvalidToken, response.Error); - Assert.Equal(SR.GetResourceString(SR.ID2004), response.ErrorDescription); - Assert.Equal(SR.FormatID8000(SR.ID2004), response.ErrorUri); + Assert.Equal(Errors.InvalidClient, response.Error); + Assert.Equal(SR.FormatID2029(Parameters.ClientId), response.ErrorDescription); + Assert.Equal(SR.FormatID8000(SR.ID2029), response.ErrorUri); } [Fact] - public async Task ValidateIntrospectionRequest_ExpiredTokenCausesAnError() + public async Task ValidateIntrospectionRequest_RequestIsRejectedWhenClientCannotBeFound() { // Arrange - await using var server = await CreateServerAsync(options => - { - options.EnableDegradedMode(); - - options.AddEventHandler(builder => - { - builder.UseInlineHandler(context => - { - Assert.Equal("SlAV32hkKG", context.Token); - - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetTokenType(TokenTypeHints.RefreshToken) - .SetExpirationDate(DateTimeOffset.UtcNow - TimeSpan.FromDays(1)); - - return default; - }); - - builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); - }); - - options.RemoveEventHandler(NormalizeErrorResponse.Descriptor); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - var response = await client.PostAsync("/connect/introspect", new OpenIddictRequest + var manager = CreateApplicationManager(mock => { - Token = "SlAV32hkKG", - TokenTypeHint = TokenTypeHints.RefreshToken + mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(value: null); }); - // Assert - Assert.Equal(Errors.InvalidToken, response.Error); - Assert.Equal(SR.GetResourceString(SR.ID2018), response.ErrorDescription); - Assert.Equal(SR.FormatID8000(SR.ID2018), response.ErrorUri); - } - - [Theory] - [InlineData(TokenTypeHints.AuthorizationCode)] - [InlineData(TokenTypeHints.DeviceCode)] - [InlineData(TokenTypeHints.IdToken)] - [InlineData(TokenTypeHints.UserCode)] - [InlineData("custom_token")] - public async Task ValidateIntrospectionRequest_UnsupportedTokenTypeCausesAnError(string type) - { - // Arrange await using var server = await CreateServerAsync(options => { - options.EnableDegradedMode(); - - options.AddEventHandler(builder => - { - builder.UseInlineHandler(context => - { - Assert.Equal("5HtRgAtc02", context.Token); - - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetTokenType(type); - - return default; - }); - - builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); - }); - - options.RemoveEventHandler(NormalizeErrorResponse.Descriptor); + options.Services.AddSingleton(manager); }); await using var client = await server.CreateClientAsync(); @@ -255,76 +195,47 @@ public async Task ValidateIntrospectionRequest_UnsupportedTokenTypeCausesAnError var response = await client.PostAsync("/connect/introspect", new OpenIddictRequest { ClientId = "Fabrikam", - Token = "5HtRgAtc02" + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + Token = "2YotnFZFEjr1zCsicMWpAA" }); // Assert - Assert.Equal(Errors.UnsupportedTokenType, response.Error); - Assert.Equal(SR.GetResourceString(SR.ID2076), response.ErrorDescription); - Assert.Equal(SR.FormatID8000(SR.ID2076), response.ErrorUri); + Assert.Equal(Errors.InvalidClient, response.Error); + Assert.Equal(SR.FormatID2052(Parameters.ClientId), response.ErrorDescription); + Assert.Equal(SR.FormatID8000(SR.ID2052), response.ErrorUri); + + Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); } [Fact] - public async Task ValidateIntrospectionRequest_AccessTokenCausesAnErrorWhenCallerIsNotAValidAudienceOrPresenter() + public async Task ValidateIntrospectionRequest_RequestIsRejectedWhenEndpointPermissionIsNotGranted() { // Arrange - await using var server = await CreateServerAsync(options => - { - options.EnableDegradedMode(); - - options.AddEventHandler(builder => - { - builder.UseInlineHandler(context => - { - Assert.Equal("2YotnFZFEjr1zCsicMWpAA", context.Token); - - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetTokenType(TokenTypeHints.AccessToken) - .SetAudiences("AdventureWorks") - .SetPresenters("Contoso"); - - return default; - }); - - builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); - }); + var application = new OpenIddictApplication(); - options.RemoveEventHandler(NormalizeErrorResponse.Descriptor); - }); + var manager = CreateApplicationManager(mock => + { + mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); - await using var client = await server.CreateClientAsync(); + mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny())) + .ReturnsAsync(true); - // Act - var response = await client.PostAsync("/connect/introspect", new OpenIddictRequest - { - ClientId = "Fabrikam", - Token = "2YotnFZFEjr1zCsicMWpAA", - TokenTypeHint = TokenTypeHints.AccessToken + mock.Setup(manager => manager.HasPermissionAsync(application, + Permissions.Endpoints.Introspection, It.IsAny())) + .ReturnsAsync(false); }); - // Assert - Assert.Equal(Errors.InvalidToken, response.Error); - Assert.Equal(SR.GetResourceString(SR.ID2077), response.ErrorDescription); - Assert.Equal(SR.FormatID8000(SR.ID2077), response.ErrorUri); - } - - [Fact] - public async Task ValidateIntrospectionRequest_RefreshTokenCausesAnErrorWhenCallerIsNotAValidPresenter() - { - // Arrange await using var server = await CreateServerAsync(options => { - options.EnableDegradedMode(); - options.AddEventHandler(builder => { builder.UseInlineHandler(context => { - Assert.Equal("8xLOxBtZp8", context.Token); + Assert.Equal("2YotnFZFEjr1zCsicMWpAA", context.Token); context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetTokenType(TokenTypeHints.RefreshToken) - .SetPresenters("Contoso"); + .SetTokenType(TokenTypeHints.RefreshToken); return default; }); @@ -332,7 +243,9 @@ public async Task ValidateIntrospectionRequest_RefreshTokenCausesAnErrorWhenCall builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); }); - options.RemoveEventHandler(NormalizeErrorResponse.Descriptor); + options.Services.AddSingleton(manager); + + options.Configure(options => options.IgnoreEndpointPermissions = false); }); await using var client = await server.CreateClientAsync(); @@ -341,23 +254,37 @@ public async Task ValidateIntrospectionRequest_RefreshTokenCausesAnErrorWhenCall var response = await client.PostAsync("/connect/introspect", new OpenIddictRequest { ClientId = "Fabrikam", - Token = "8xLOxBtZp8", - TokenTypeHint = TokenTypeHints.RefreshToken + Token = "2YotnFZFEjr1zCsicMWpAA" }); // Assert - Assert.Equal(Errors.InvalidToken, response.Error); - Assert.Equal(SR.GetResourceString(SR.ID2077), response.ErrorDescription); - Assert.Equal(SR.FormatID8000(SR.ID2077), response.ErrorUri); + Assert.Equal(Errors.UnauthorizedClient, response.Error); + Assert.Equal(SR.GetResourceString(SR.ID2075), response.ErrorDescription); + Assert.Equal(SR.FormatID8000(SR.ID2075), response.ErrorUri); + + Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application, + Permissions.Endpoints.Introspection, It.IsAny()), Times.Once()); } [Fact] - public async Task ValidateIntrospectionRequest_RequestWithoutClientIdIsRejectedWhenClientIdentificationIsRequired() + public async Task ValidateIntrospectionRequest_ClientSecretCannotBeUsedByPublicClients() { // Arrange - await using var server = await CreateServerAsync(builder => + var application = new OpenIddictApplication(); + + var manager = CreateApplicationManager(mock => { - builder.Configure(options => options.AcceptAnonymousClients = false); + mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny())) + .ReturnsAsync(true); + }); + + await using var server = await CreateServerAsync(options => + { + options.Services.AddSingleton(manager); }); await using var client = await server.CreateClientAsync(); @@ -365,23 +292,33 @@ public async Task ValidateIntrospectionRequest_RequestWithoutClientIdIsRejectedW // Act var response = await client.PostAsync("/connect/introspect", new OpenIddictRequest { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", Token = "2YotnFZFEjr1zCsicMWpAA" }); // Assert Assert.Equal(Errors.InvalidClient, response.Error); - Assert.Equal(SR.FormatID2029(Parameters.ClientId), response.ErrorDescription); - Assert.Equal(SR.FormatID8000(SR.ID2029), response.ErrorUri); + Assert.Equal(SR.FormatID2053(Parameters.ClientSecret), response.ErrorDescription); + Assert.Equal(SR.FormatID8000(SR.ID2053), response.ErrorUri); + + Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.Once()); } [Fact] - public async Task ValidateIntrospectionRequest_RequestIsRejectedWhenClientCannotBeFound() + public async Task ValidateIntrospectionRequest_ClientSecretIsRequiredForNonPublicClients() { // Arrange + var application = new OpenIddictApplication(); + var manager = CreateApplicationManager(mock => { mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(value: null); + .ReturnsAsync(application); + + mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny())) + .ReturnsAsync(false); }); await using var server = await CreateServerAsync(options => @@ -395,20 +332,21 @@ public async Task ValidateIntrospectionRequest_RequestIsRejectedWhenClientCannot var response = await client.PostAsync("/connect/introspect", new OpenIddictRequest { ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + ClientSecret = null, Token = "2YotnFZFEjr1zCsicMWpAA" }); // Assert Assert.Equal(Errors.InvalidClient, response.Error); - Assert.Equal(SR.FormatID2052(Parameters.ClientId), response.ErrorDescription); - Assert.Equal(SR.FormatID8000(SR.ID2052), response.ErrorUri); + Assert.Equal(SR.FormatID2054(Parameters.ClientSecret), response.ErrorDescription); + Assert.Equal(SR.FormatID8000(SR.ID2054), response.ErrorUri); - Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.Once()); } [Fact] - public async Task ValidateIntrospectionRequest_RequestIsRejectedWhenEndpointPermissionIsNotGranted() + public async Task ValidateIntrospectionRequest_RequestIsRejectedWhenClientCredentialsAreInvalid() { // Arrange var application = new OpenIddictApplication(); @@ -419,18 +357,15 @@ public async Task ValidateIntrospectionRequest_RequestIsRejectedWhenEndpointPerm .ReturnsAsync(application); mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny())) - .ReturnsAsync(true); + .ReturnsAsync(false); - mock.Setup(manager => manager.HasPermissionAsync(application, - Permissions.Endpoints.Introspection, It.IsAny())) + mock.Setup(manager => manager.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) .ReturnsAsync(false); }); await using var server = await CreateServerAsync(options => { options.Services.AddSingleton(manager); - - options.Configure(options => options.IgnoreEndpointPermissions = false); }); await using var client = await server.CreateClientAsync(); @@ -439,37 +374,26 @@ public async Task ValidateIntrospectionRequest_RequestIsRejectedWhenEndpointPerm var response = await client.PostAsync("/connect/introspect", new OpenIddictRequest { ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", Token = "2YotnFZFEjr1zCsicMWpAA" }); // Assert - Assert.Equal(Errors.UnauthorizedClient, response.Error); - Assert.Equal(SR.GetResourceString(SR.ID2075), response.ErrorDescription); - Assert.Equal(SR.FormatID8000(SR.ID2075), response.ErrorUri); Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce()); - Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(application, - Permissions.Endpoints.Introspection, It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(manager => manager.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny()), Times.Once()); } [Fact] - public async Task ValidateIntrospectionRequest_ClientSecretCannotBeUsedByPublicClients() + public async Task ValidateIntrospectionRequest_InvalidTokenCausesAnError() { // Arrange - var application = new OpenIddictApplication(); - - var manager = CreateApplicationManager(mock => + await using var server = await CreateServerAsync(options => { - mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); - - mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny())) - .ReturnsAsync(true); - }); + options.EnableDegradedMode(); - await using var server = await CreateServerAsync(builder => - { - builder.Services.AddSingleton(manager); + options.RemoveEventHandler(NormalizeErrorResponse.Descriptor); }); await using var client = await server.CreateClientAsync(); @@ -477,38 +401,86 @@ public async Task ValidateIntrospectionRequest_ClientSecretCannotBeUsedByPublicC // Act var response = await client.PostAsync("/connect/introspect", new OpenIddictRequest { - ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", - Token = "2YotnFZFEjr1zCsicMWpAA" + Token = "SlAV32hkKG" }); // Assert - Assert.Equal(Errors.InvalidClient, response.Error); - Assert.Equal(SR.FormatID2053(Parameters.ClientSecret), response.ErrorDescription); - Assert.Equal(SR.FormatID8000(SR.ID2053), response.ErrorUri); - - Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce()); - Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.Once()); + Assert.Equal(Errors.InvalidToken, response.Error); + Assert.Equal(SR.GetResourceString(SR.ID2004), response.ErrorDescription); + Assert.Equal(SR.FormatID8000(SR.ID2004), response.ErrorUri); } [Fact] - public async Task ValidateIntrospectionRequest_ClientSecretIsRequiredForNonPublicClients() + public async Task ValidateIntrospectionRequest_ExpiredTokenCausesAnError() { // Arrange - var application = new OpenIddictApplication(); - - var manager = CreateApplicationManager(mock => + await using var server = await CreateServerAsync(options => { - mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); + options.EnableDegradedMode(); - mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny())) - .ReturnsAsync(false); + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("SlAV32hkKG", context.Token); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetTokenType(TokenTypeHints.RefreshToken) + .SetExpirationDate(DateTimeOffset.UtcNow - TimeSpan.FromDays(1)); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + + options.RemoveEventHandler(NormalizeErrorResponse.Descriptor); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/connect/introspect", new OpenIddictRequest + { + Token = "SlAV32hkKG", + TokenTypeHint = TokenTypeHints.RefreshToken }); - await using var server = await CreateServerAsync(builder => + // Assert + Assert.Equal(Errors.InvalidToken, response.Error); + Assert.Equal(SR.GetResourceString(SR.ID2018), response.ErrorDescription); + Assert.Equal(SR.FormatID8000(SR.ID2018), response.ErrorUri); + } + + [Theory] + [InlineData(TokenTypeHints.AuthorizationCode)] + [InlineData(TokenTypeHints.DeviceCode)] + [InlineData(TokenTypeHints.IdToken)] + [InlineData(TokenTypeHints.UserCode)] + [InlineData("custom_token")] + public async Task ValidateIntrospectionRequest_UnsupportedTokenTypeCausesAnError(string type) + { + // Arrange + await using var server = await CreateServerAsync(options => { - builder.Services.AddSingleton(manager); + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("5HtRgAtc02", context.Token); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetTokenType(type); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + + options.RemoveEventHandler(NormalizeErrorResponse.Descriptor); }); await using var client = await server.CreateClientAsync(); @@ -517,40 +489,84 @@ public async Task ValidateIntrospectionRequest_ClientSecretIsRequiredForNonPubli var response = await client.PostAsync("/connect/introspect", new OpenIddictRequest { ClientId = "Fabrikam", - ClientSecret = null, - Token = "2YotnFZFEjr1zCsicMWpAA" + Token = "5HtRgAtc02" }); // Assert - Assert.Equal(Errors.InvalidClient, response.Error); - Assert.Equal(SR.FormatID2054(Parameters.ClientSecret), response.ErrorDescription); - Assert.Equal(SR.FormatID8000(SR.ID2054), response.ErrorUri); - - Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce()); - Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.Once()); + Assert.Equal(Errors.UnsupportedTokenType, response.Error); + Assert.Equal(SR.GetResourceString(SR.ID2076), response.ErrorDescription); + Assert.Equal(SR.FormatID8000(SR.ID2076), response.ErrorUri); } [Fact] - public async Task ValidateIntrospectionRequest_RequestIsRejectedWhenClientCredentialsAreInvalid() + public async Task ValidateIntrospectionRequest_AccessTokenCausesAnErrorWhenCallerIsNotAValidAudienceOrPresenter() { // Arrange - var application = new OpenIddictApplication(); - - var manager = CreateApplicationManager(mock => + await using var server = await CreateServerAsync(options => { - mock.Setup(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny())) - .ReturnsAsync(application); + options.EnableDegradedMode(); - mock.Setup(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny())) - .ReturnsAsync(false); + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("2YotnFZFEjr1zCsicMWpAA", context.Token); - mock.Setup(manager => manager.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) - .ReturnsAsync(false); + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetTokenType(TokenTypeHints.AccessToken) + .SetAudiences("AdventureWorks") + .SetPresenters("Contoso"); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + + options.RemoveEventHandler(NormalizeErrorResponse.Descriptor); }); + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/connect/introspect", new OpenIddictRequest + { + ClientId = "Fabrikam", + Token = "2YotnFZFEjr1zCsicMWpAA", + TokenTypeHint = TokenTypeHints.AccessToken + }); + + // Assert + Assert.Equal(Errors.InvalidToken, response.Error); + Assert.Equal(SR.GetResourceString(SR.ID2077), response.ErrorDescription); + Assert.Equal(SR.FormatID8000(SR.ID2077), response.ErrorUri); + } + + [Fact] + public async Task ValidateIntrospectionRequest_RefreshTokenCausesAnErrorWhenCallerIsNotAValidPresenter() + { + // Arrange await using var server = await CreateServerAsync(options => { - options.Services.AddSingleton(manager); + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("8xLOxBtZp8", context.Token); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetTokenType(TokenTypeHints.RefreshToken) + .SetPresenters("Contoso"); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + + options.RemoveEventHandler(NormalizeErrorResponse.Descriptor); }); await using var client = await server.CreateClientAsync(); @@ -559,15 +575,14 @@ public async Task ValidateIntrospectionRequest_RequestIsRejectedWhenClientCreden var response = await client.PostAsync("/connect/introspect", new OpenIddictRequest { ClientId = "Fabrikam", - ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", - Token = "2YotnFZFEjr1zCsicMWpAA" + Token = "8xLOxBtZp8", + TokenTypeHint = TokenTypeHints.RefreshToken }); // Assert - - Mock.Get(manager).Verify(manager => manager.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.AtLeastOnce()); - Mock.Get(manager).Verify(manager => manager.HasClientTypeAsync(application, ClientTypes.Public, It.IsAny()), Times.AtLeastOnce()); - Mock.Get(manager).Verify(manager => manager.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny()), Times.Once()); + Assert.Equal(Errors.InvalidToken, response.Error); + Assert.Equal(SR.GetResourceString(SR.ID2077), response.ErrorDescription); + Assert.Equal(SR.FormatID8000(SR.ID2077), response.ErrorUri); } [Theory] diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Revocation.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Revocation.cs index 968fe1d27..a9f418bcc 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Revocation.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Revocation.cs @@ -7,6 +7,7 @@ using System.Net.Http; using System.Security.Claims; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using Moq; using Xunit; using static OpenIddict.Server.OpenIddictServerEvents; @@ -150,146 +151,13 @@ public async Task ValidateRevocationRequest_MissingTokenCausesAnError() Assert.Equal(SR.FormatID8000(SR.ID2029), response.ErrorUri); } - [Theory] - [InlineData(TokenTypeHints.AuthorizationCode)] - [InlineData(TokenTypeHints.DeviceCode)] - [InlineData(TokenTypeHints.IdToken)] - [InlineData(TokenTypeHints.UserCode)] - [InlineData("custom_token")] - public async Task ValidateRevocationRequest_UnsupportedTokenTypeCausesAnError(string type) - { - // Arrange - await using var server = await CreateServerAsync(options => - { - options.EnableDegradedMode(); - - options.AddEventHandler(builder => - { - builder.UseInlineHandler(context => - { - Assert.Equal("5HtRgAtc02", context.Token); - - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetTokenType(type); - - return default; - }); - - builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); - }); - - options.RemoveEventHandler(NormalizeErrorResponse.Descriptor); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - var response = await client.PostAsync("/connect/revoke", new OpenIddictRequest - { - ClientId = "Fabrikam", - Token = "5HtRgAtc02" - }); - - // Assert - Assert.Equal(Errors.UnsupportedTokenType, response.Error); - Assert.Equal(SR.GetResourceString(SR.ID2079), response.ErrorDescription); - Assert.Equal(SR.FormatID8000(SR.ID2079), response.ErrorUri); - } - - [Fact] - public async Task ValidateRevocationRequest_AccessTokenCausesAnErrorWhenCallerIsNotAValidAudienceOrPresenter() - { - // Arrange - await using var server = await CreateServerAsync(options => - { - options.EnableDegradedMode(); - - options.AddEventHandler(builder => - { - builder.UseInlineHandler(context => - { - Assert.Equal("2YotnFZFEjr1zCsicMWpAA", context.Token); - - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetTokenType(TokenTypeHints.AccessToken) - .SetAudiences("AdventureWorks") - .SetPresenters("Contoso"); - - return default; - }); - - builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); - }); - - options.RemoveEventHandler(NormalizeErrorResponse.Descriptor); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - var response = await client.PostAsync("/connect/revoke", new OpenIddictRequest - { - ClientId = "Fabrikam", - Token = "2YotnFZFEjr1zCsicMWpAA", - TokenTypeHint = TokenTypeHints.AccessToken - }); - - // Assert - Assert.Equal(Errors.InvalidToken, response.Error); - Assert.Equal(SR.GetResourceString(SR.ID2080), response.ErrorDescription); - Assert.Equal(SR.FormatID8000(SR.ID2080), response.ErrorUri); - } - - [Fact] - public async Task ValidateRevocationRequest_RefreshTokenCausesAnErrorWhenCallerIsNotAValidPresenter() - { - // Arrange - await using var server = await CreateServerAsync(options => - { - options.EnableDegradedMode(); - - options.AddEventHandler(builder => - { - builder.UseInlineHandler(context => - { - Assert.Equal("8xLOxBtZp8", context.Token); - - context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) - .SetTokenType(TokenTypeHints.RefreshToken) - .SetPresenters("Contoso"); - - return default; - }); - - builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); - }); - - options.RemoveEventHandler(NormalizeErrorResponse.Descriptor); - }); - - await using var client = await server.CreateClientAsync(); - - // Act - var response = await client.PostAsync("/connect/revoke", new OpenIddictRequest - { - ClientId = "Fabrikam", - Token = "8xLOxBtZp8", - TokenTypeHint = TokenTypeHints.RefreshToken - }); - - // Assert - Assert.Equal(Errors.InvalidToken, response.Error); - Assert.Equal(SR.GetResourceString(SR.ID2080), response.ErrorDescription); - Assert.Equal(SR.FormatID8000(SR.ID2080), response.ErrorUri); - } - [Fact] public async Task ValidateRevocationRequest_RequestWithoutClientIdIsRejectedWhenClientIdentificationIsRequired() { // Arrange - await using var server = await CreateServerAsync(builder => + await using var server = await CreateServerAsync(options => { - builder.Configure(options => options.AcceptAnonymousClients = false); + options.Configure(options => options.AcceptAnonymousClients = false); }); await using var client = await server.CreateClientAsync(); @@ -317,9 +185,9 @@ public async Task ValidateRevocationRequest_RequestIsRejectedWhenClientCannotBeF .ReturnsAsync(value: null); }); - await using var server = await CreateServerAsync(builder => + await using var server = await CreateServerAsync(options => { - builder.Services.AddSingleton(manager); + options.Services.AddSingleton(manager); }); await using var client = await server.CreateClientAsync(); @@ -359,11 +227,26 @@ public async Task ValidateRevocationRequest_RequestIsRejectedWhenEndpointPermiss .ReturnsAsync(false); }); - await using var server = await CreateServerAsync(builder => + await using var server = await CreateServerAsync(options => { - builder.Services.AddSingleton(manager); + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("SlAV32hkKG", context.Token); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetTokenType(TokenTypeHints.RefreshToken); - builder.Configure(options => options.IgnoreEndpointPermissions = false); + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + + options.Services.AddSingleton(manager); + + options.Configure(options => options.IgnoreEndpointPermissions = false); }); await using var client = await server.CreateClientAsync(); @@ -401,9 +284,9 @@ public async Task ValidateRevocationRequest_ClientSecretCannotBeUsedByPublicClie .ReturnsAsync(true); }); - await using var server = await CreateServerAsync(builder => + await using var server = await CreateServerAsync(options => { - builder.Services.AddSingleton(manager); + options.Services.AddSingleton(manager); }); await using var client = await server.CreateClientAsync(); @@ -441,9 +324,9 @@ public async Task ValidateRevocationRequest_ClientSecretIsRequiredForNonPublicCl .ReturnsAsync(false); }); - await using var server = await CreateServerAsync(builder => + await using var server = await CreateServerAsync(options => { - builder.Services.AddSingleton(manager); + options.Services.AddSingleton(manager); }); await using var client = await server.CreateClientAsync(); @@ -510,6 +393,139 @@ public async Task ValidateRevocationRequest_RequestIsRejectedWhenClientCredentia Mock.Get(manager).Verify(manager => manager.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny()), Times.Once()); } + [Theory] + [InlineData(TokenTypeHints.AuthorizationCode)] + [InlineData(TokenTypeHints.DeviceCode)] + [InlineData(TokenTypeHints.IdToken)] + [InlineData(TokenTypeHints.UserCode)] + [InlineData("custom_token")] + public async Task ValidateRevocationRequest_UnsupportedTokenTypeCausesAnError(string type) + { + // Arrange + await using var server = await CreateServerAsync(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("5HtRgAtc02", context.Token); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetTokenType(type); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + + options.RemoveEventHandler(NormalizeErrorResponse.Descriptor); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/connect/revoke", new OpenIddictRequest + { + ClientId = "Fabrikam", + Token = "5HtRgAtc02" + }); + + // Assert + Assert.Equal(Errors.UnsupportedTokenType, response.Error); + Assert.Equal(SR.GetResourceString(SR.ID2079), response.ErrorDescription); + Assert.Equal(SR.FormatID8000(SR.ID2079), response.ErrorUri); + } + + [Fact] + public async Task ValidateRevocationRequest_AccessTokenCausesAnErrorWhenCallerIsNotAValidAudienceOrPresenter() + { + // Arrange + await using var server = await CreateServerAsync(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("2YotnFZFEjr1zCsicMWpAA", context.Token); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetTokenType(TokenTypeHints.AccessToken) + .SetAudiences("AdventureWorks") + .SetPresenters("Contoso"); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + + options.RemoveEventHandler(NormalizeErrorResponse.Descriptor); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/connect/revoke", new OpenIddictRequest + { + ClientId = "Fabrikam", + Token = "2YotnFZFEjr1zCsicMWpAA", + TokenTypeHint = TokenTypeHints.AccessToken + }); + + // Assert + Assert.Equal(Errors.InvalidToken, response.Error); + Assert.Equal(SR.GetResourceString(SR.ID2080), response.ErrorDescription); + Assert.Equal(SR.FormatID8000(SR.ID2080), response.ErrorUri); + } + + [Fact] + public async Task ValidateRevocationRequest_RefreshTokenCausesAnErrorWhenCallerIsNotAValidPresenter() + { + // Arrange + await using var server = await CreateServerAsync(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + Assert.Equal("8xLOxBtZp8", context.Token); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetTokenType(TokenTypeHints.RefreshToken) + .SetPresenters("Contoso"); + + return default; + }); + + builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500); + }); + + options.RemoveEventHandler(NormalizeErrorResponse.Descriptor); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/connect/revoke", new OpenIddictRequest + { + ClientId = "Fabrikam", + Token = "8xLOxBtZp8", + TokenTypeHint = TokenTypeHints.RefreshToken + }); + + // Assert + Assert.Equal(Errors.InvalidToken, response.Error); + Assert.Equal(SR.GetResourceString(SR.ID2080), response.ErrorDescription); + Assert.Equal(SR.FormatID8000(SR.ID2080), response.ErrorUri); + } + [Theory] [InlineData("custom_error", null, null)] [InlineData("custom_error", "custom_description", null)] diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs index a119ccc85..65f614038 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs @@ -484,40 +484,6 @@ public async Task ProcessAuthentication_InvalidEndpointCausesAnException() Assert.Equal(SR.GetResourceString(SR.ID0002), exception.Message); } - [Fact] - public async Task ProcessAuthentication_UnsupportedGrantTypeThrowsAnException() - { - // Arrange - await using var server = await CreateServerAsync(options => - { - options.EnableDegradedMode(); - options.SetTokenEndpointUris("/authenticate"); - - options.AddEventHandler(builder => - builder.UseInlineHandler(context => - { - context.SkipRequest(); - - return default; - })); - }); - - await using var client = await server.CreateClientAsync(); - - // Act and assert - var exception = await Assert.ThrowsAsync(delegate - { - return client.PostAsync("/authenticate", new OpenIddictRequest - { - GrantType = GrantTypes.Password, - Username = "johndoe", - Password = "A3ddj3w", - }); - }); - - Assert.Equal(SR.GetResourceString(SR.ID0001), exception.Message); - } - [Fact] public async Task ProcessAuthentication_MissingAccessTokenReturnsNull() { @@ -3880,22 +3846,13 @@ protected virtual void ConfigureServices(IServiceCollection services) options.AddEventHandler(builder => builder.UseInlineHandler(context => default)); - options.AddEventHandler(builder => - builder.UseInlineHandler(context => default)); - - options.AddEventHandler(builder => - builder.UseInlineHandler(context => default)); - options.AddEventHandler(builder => builder.UseInlineHandler(context => default)); - 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)); options.AddEventHandler(builder =>