From 00fa3f349420faa106df47b2f870e239faec8d76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Sat, 30 Sep 2023 17:09:10 +0200 Subject: [PATCH] Allow configuring the supported client authentication methods and use invalid_client for client assertion errors --- .../OpenIddictConstants.cs | 1 + .../OpenIddictResources.resx | 12 ++++ .../Managers/OpenIddictApplicationManager.cs | 2 +- ...OpenIddictServerAspNetCoreConfiguration.cs | 3 + ...enIddictServerAspNetCoreHandlers.Device.cs | 1 + ...IddictServerAspNetCoreHandlers.Exchange.cs | 1 + ...tServerAspNetCoreHandlers.Introspection.cs | 1 + ...dictServerAspNetCoreHandlers.Revocation.cs | 1 + .../OpenIddictServerAspNetCoreHandlers.cs | 71 ++++++++++++++++++- .../OpenIddictServerOwinConfiguration.cs | 3 + .../OpenIddictServerOwinHandlers.Device.cs | 1 + .../OpenIddictServerOwinHandlers.Exchange.cs | 1 + ...nIddictServerOwinHandlers.Introspection.cs | 1 + ...OpenIddictServerOwinHandlers.Revocation.cs | 1 + .../OpenIddictServerOwinHandlers.cs | 68 +++++++++++++++++- .../OpenIddictServerConfiguration.cs | 24 +++++++ .../OpenIddictServerHandlers.Device.cs | 4 +- .../OpenIddictServerHandlers.Discovery.cs | 16 ++--- .../OpenIddictServerHandlers.Exchange.cs | 24 +++---- .../OpenIddictServerHandlers.Introspection.cs | 24 +++---- .../OpenIddictServerHandlers.Protection.cs | 50 ++++++++++--- .../OpenIddictServerHandlers.Revocation.cs | 24 +++---- .../OpenIddictServerHandlers.cs | 2 +- .../OpenIddictServerOptions.cs | 24 ++++++- ...rverAspNetCoreIntegrationTests.Exchange.cs | 68 ++++++++++++++++++ ...spNetCoreIntegrationTests.Introspection.cs | 64 +++++++++++++++++ ...erAspNetCoreIntegrationTests.Revocation.cs | 64 +++++++++++++++++ ...OpenIddictServerIntegrationTests.Device.cs | 13 +++- ...nIddictServerIntegrationTests.Discovery.cs | 44 +++++++++--- ...enIddictServerIntegrationTests.Exchange.cs | 13 +++- ...ictServerIntegrationTests.Introspection.cs | 13 +++- ...IddictServerIntegrationTests.Revocation.cs | 13 +++- ...dictServerOwinIntegrationTests.Exchange.cs | 68 ++++++++++++++++++ ...erverOwinIntegrationTests.Introspection.cs | 64 +++++++++++++++++ ...ctServerOwinIntegrationTests.Revocation.cs | 64 +++++++++++++++++ 35 files changed, 765 insertions(+), 83 deletions(-) diff --git a/src/OpenIddict.Abstractions/OpenIddictConstants.cs b/src/OpenIddict.Abstractions/OpenIddictConstants.cs index 2014b032..a3948036 100644 --- a/src/OpenIddict.Abstractions/OpenIddictConstants.cs +++ b/src/OpenIddict.Abstractions/OpenIddictConstants.cs @@ -167,6 +167,7 @@ public static class OpenIddictConstants public const string ClientSecretBasic = "client_secret_basic"; public const string ClientSecretJwt = "client_secret_jwt"; public const string ClientSecretPost = "client_secret_post"; + public const string None = "none"; public const string PrivateKeyJwt = "private_key_jwt"; } diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx index 11c43f17..4ea9d0ae 100644 --- a/src/OpenIddict.Abstractions/OpenIddictResources.resx +++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx @@ -1563,6 +1563,12 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId A client identifier must be specified in the client registration or web provider options when using 'response_type=none', the authorization code/hybrid/implicit flows or the device authorization flow. + + At least one client authentication method must be configured when enabling the device authorization, introspection, revocation or token endpoints. + + + The '{0}' client assertion type must be configured when enabling the '{1}' client authentication method. + The security token is missing. @@ -2082,6 +2088,9 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId The '{0}' claim returned in the specified client assertion doesn't match the expected value. + + The '{0}' client authentication method is not supported. + The '{0}' parameter shouldn't be null or empty at this point. @@ -2724,6 +2733,9 @@ This may indicate that the hashed entry is corrupted or malformed. The authentication demand was rejected because the public application '{ClientId}' was not allowed to send a client assertion. + + The request was rejected because the '{Method}' client authentication method that was used by the client application is not enabled in the server options. + https://documentation.openiddict.com/errors/{0} diff --git a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs index eb0385d9..1f8679a0 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs @@ -1401,7 +1401,7 @@ public class OpenIddictApplicationManager : IOpenIddictApplication var value = await Store.GetClientSecretAsync(application, cancellationToken); if (string.IsNullOrEmpty(value)) { - Logger.LogError(SR.GetResourceString(SR.ID6160), await GetClientIdAsync(application, cancellationToken)); + Logger.LogInformation(SR.GetResourceString(SR.ID6160), await GetClientIdAsync(application, cancellationToken)); return false; } diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConfiguration.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConfiguration.cs index fd81da00..35454181 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConfiguration.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConfiguration.cs @@ -47,6 +47,9 @@ public sealed class OpenIddictServerAspNetCoreConfiguration : IConfigureOptions< // Register the built-in event handlers used by the OpenIddict ASP.NET Core server components. options.Handlers.AddRange(OpenIddictServerAspNetCoreHandlers.DefaultHandlers); + + // Enable client_secret_basic support by default. + options.ClientAuthenticationMethods.Add(ClientAuthenticationMethods.ClientSecretBasic); } /// diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Device.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Device.cs index 5a15c724..fc096e20 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Device.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Device.cs @@ -19,6 +19,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers * Device request extraction: */ ExtractPostRequest.Descriptor, + ValidateClientAuthenticationMethod.Descriptor, ExtractBasicAuthenticationCredentials.Descriptor, /* diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Exchange.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Exchange.cs index 88887927..39df0cc8 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Exchange.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Exchange.cs @@ -17,6 +17,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers * Token request extraction: */ ExtractPostRequest.Descriptor, + ValidateClientAuthenticationMethod.Descriptor, ExtractBasicAuthenticationCredentials.Descriptor, /* diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Introspection.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Introspection.cs index c380ca74..b6d31198 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Introspection.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Introspection.cs @@ -17,6 +17,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers * Introspection request extraction: */ ExtractGetOrPostRequest.Descriptor, + ValidateClientAuthenticationMethod.Descriptor, ExtractBasicAuthenticationCredentials.Descriptor, /* diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Revocation.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Revocation.cs index 58d6aadd..59f03575 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Revocation.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Revocation.cs @@ -17,6 +17,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers * Revocation request extraction: */ ExtractPostRequest.Descriptor, + ValidateClientAuthenticationMethod.Descriptor, ExtractBasicAuthenticationCredentials.Descriptor, /* diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs index 4cd0a333..3239a855 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs @@ -642,6 +642,75 @@ public static partial class OpenIddictServerAspNetCoreHandlers } } + /// + /// Contains the logic responsible for validating the authentication method used by the client application. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public sealed class ValidateClientAuthenticationMethod : IOpenIddictServerHandler + where TContext : BaseValidatingContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(ExtractPostRequest.Descriptor.Order + 1_000) + .SetType(OpenIddictServerHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(TContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.Transaction.Request is not null, SR.GetResourceString(SR.ID4008)); + + // This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var request = context.Transaction.GetHttpRequest() ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0114)); + + // Reject requests that use client_secret_post if support was explicitly disabled in the options. + if (!string.IsNullOrEmpty(context.Transaction.Request.ClientSecret) && + !context.Options.ClientAuthenticationMethods.Contains(ClientAuthenticationMethods.ClientSecretPost)) + { + context.Logger.LogInformation(SR.GetResourceString(SR.ID6227), ClientAuthenticationMethods.ClientSecretPost); + + context.Reject( + error: Errors.InvalidClient, + description: SR.FormatID2174(ClientAuthenticationMethods.ClientSecretPost), + uri: SR.FormatID8000(SR.ID2174)); + + return default; + } + + // Reject requests that use client_secret_basic if support was explicitly disabled in the options. + // + // Note: the client_secret_jwt authentication method is not supported by OpenIddict out-of-the-box but + // is specified here to account for custom implementations that explicitly add client_secret_jwt support. + string? header = request.Headers[HeaderNames.Authorization]; + if (!string.IsNullOrEmpty(header) && header.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase) && + !context.Options.ClientAuthenticationMethods.Contains(ClientAuthenticationMethods.ClientSecretBasic)) + { + context.Logger.LogInformation(SR.GetResourceString(SR.ID6227), ClientAuthenticationMethods.ClientSecretBasic); + + context.Reject( + error: Errors.InvalidClient, + description: SR.FormatID2174(ClientAuthenticationMethods.ClientSecretBasic), + uri: SR.FormatID8000(SR.ID2174)); + + return default; + } + + return default; + } + } + /// /// Contains the logic responsible for extracting client credentials from the standard HTTP Authorization header. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. @@ -656,7 +725,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler>() - .SetOrder(ExtractPostRequest.Descriptor.Order + 1_000) + .SetOrder(ValidateClientAuthenticationMethod.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinConfiguration.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinConfiguration.cs index c3498696..73a13881 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinConfiguration.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinConfiguration.cs @@ -26,6 +26,9 @@ public sealed class OpenIddictServerOwinConfiguration : IConfigureOptions diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Device.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Device.cs index 251321b0..112789dd 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Device.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Device.cs @@ -19,6 +19,7 @@ public static partial class OpenIddictServerOwinHandlers * Device request extraction: */ ExtractPostRequest.Descriptor, + ValidateClientAuthenticationMethod.Descriptor, ExtractBasicAuthenticationCredentials.Descriptor, /* diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Exchange.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Exchange.cs index 9192c5f0..86ea552d 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Exchange.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Exchange.cs @@ -17,6 +17,7 @@ public static partial class OpenIddictServerOwinHandlers * Token request extraction: */ ExtractPostRequest.Descriptor, + ValidateClientAuthenticationMethod.Descriptor, ExtractBasicAuthenticationCredentials.Descriptor, /* diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Introspection.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Introspection.cs index 99dbf53e..a8e63431 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Introspection.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Introspection.cs @@ -17,6 +17,7 @@ public static partial class OpenIddictServerOwinHandlers * Introspection request extraction: */ ExtractGetOrPostRequest.Descriptor, + ValidateClientAuthenticationMethod.Descriptor, ExtractBasicAuthenticationCredentials.Descriptor, /* diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Revocation.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Revocation.cs index 8aa04aa3..04facc82 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Revocation.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Revocation.cs @@ -17,6 +17,7 @@ public static partial class OpenIddictServerOwinHandlers * Revocation request extraction: */ ExtractPostRequest.Descriptor, + ValidateClientAuthenticationMethod.Descriptor, ExtractBasicAuthenticationCredentials.Descriptor, /* diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs index 8379b87a..4bb7481e 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs @@ -695,6 +695,72 @@ public static partial class OpenIddictServerOwinHandlers } } + /// + /// Contains the logic responsible for validating the authentication method used by the client application. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public sealed class ValidateClientAuthenticationMethod : IOpenIddictServerHandler + where TContext : BaseValidatingContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(ExtractPostRequest.Descriptor.Order + 1_000) + .SetType(OpenIddictServerHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(TContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.Transaction.Request is not null, SR.GetResourceString(SR.ID4008)); + + // This handler only applies to OWIN requests. If The OWIN request cannot be resolved, + // this may indicate that the request was incorrectly processed by another server stack. + var request = context.Transaction.GetOwinRequest() ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0120)); + + // Reject requests that use client_secret_post if support was explicitly disabled in the options. + if (!string.IsNullOrEmpty(context.Transaction.Request.ClientSecret) && + !context.Options.ClientAuthenticationMethods.Contains(ClientAuthenticationMethods.ClientSecretPost)) + { + context.Logger.LogInformation(SR.GetResourceString(SR.ID6227), ClientAuthenticationMethods.ClientSecretPost); + + context.Reject( + error: Errors.InvalidClient, + description: SR.FormatID2174(ClientAuthenticationMethods.ClientSecretPost), + uri: SR.FormatID8000(SR.ID2174)); + + return default; + } + + // Reject requests that use client_secret_basic if support was explicitly disabled in the options. + var header = request.Headers[Headers.Authorization]; + if (!string.IsNullOrEmpty(header) && header.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase) && + !context.Options.ClientAuthenticationMethods.Contains(ClientAuthenticationMethods.ClientSecretBasic)) + { + context.Logger.LogInformation(SR.GetResourceString(SR.ID6227, ClientAuthenticationMethods.ClientSecretBasic)); + + context.Reject( + error: Errors.InvalidClient, + description: SR.FormatID2174(ClientAuthenticationMethods.ClientSecretBasic), + uri: SR.FormatID8000(SR.ID2174)); + + return default; + } + + return default; + } + } + /// /// Contains the logic responsible for extracting client credentials from the standard HTTP Authorization header. /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. @@ -709,7 +775,7 @@ public static partial class OpenIddictServerOwinHandlers = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler>() - .SetOrder(ExtractPostRequest.Descriptor.Order + 1_000) + .SetOrder(ValidateClientAuthenticationMethod.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); diff --git a/src/OpenIddict.Server/OpenIddictServerConfiguration.cs b/src/OpenIddict.Server/OpenIddictServerConfiguration.cs index 33fdbc40..639ca36e 100644 --- a/src/OpenIddict.Server/OpenIddictServerConfiguration.cs +++ b/src/OpenIddict.Server/OpenIddictServerConfiguration.cs @@ -127,6 +127,30 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions Errors.InvalidClient, + + _ => Errors.InvalidToken + }, description: context.ValidTokenTypes.Count switch { 1 when context.ValidTokenTypes.Contains(TokenTypeHints.AuthorizationCode) @@ -422,7 +428,13 @@ public static partial class OpenIddictServerHandlers context.Logger.LogTrace(result.Exception, SR.GetResourceString(SR.ID6000), context.Token); context.Reject( - error: Errors.InvalidToken, + error: context.ValidTokenTypes.Count switch + { + 1 when context.ValidTokenTypes.Contains(TokenTypeHints.ClientAssertion) + => Errors.InvalidClient, + + _ => Errors.InvalidToken + }, description: result.Exception switch { SecurityTokenInvalidTypeException => context.ValidTokenTypes.Count switch @@ -791,7 +803,13 @@ public static partial class OpenIddictServerHandlers if (context.Principal is null) { context.Reject( - error: Errors.InvalidToken, + error: context.ValidTokenTypes.Count switch + { + 1 when context.ValidTokenTypes.Contains(TokenTypeHints.ClientAssertion) + => Errors.InvalidClient, + + _ => Errors.InvalidToken + }, description: context.ValidTokenTypes.Count switch { 1 when context.ValidTokenTypes.Contains(TokenTypeHints.AuthorizationCode) @@ -874,8 +892,9 @@ public static partial class OpenIddictServerHandlers context.Reject( error: context.Principal.GetTokenType() switch { - TokenTypeHints.DeviceCode => Errors.ExpiredToken, - _ => Errors.InvalidToken + TokenTypeHints.ClientAssertion => Errors.InvalidClient, + TokenTypeHints.DeviceCode => Errors.ExpiredToken, + _ => Errors.InvalidToken }, description: context.Principal.GetTokenType() switch { @@ -953,7 +972,12 @@ public static partial class OpenIddictServerHandlers context.Logger.LogInformation(SR.GetResourceString(SR.ID6002), context.TokenId); context.Reject( - error: Errors.InvalidToken, + error: context.Principal.GetTokenType() switch + { + TokenTypeHints.ClientAssertion => Errors.InvalidClient, + + _ => Errors.InvalidToken + }, description: context.Principal.GetTokenType() switch { TokenTypeHints.AuthorizationCode => SR.GetResourceString(SR.ID2010), @@ -1011,7 +1035,12 @@ public static partial class OpenIddictServerHandlers context.Logger.LogInformation(SR.GetResourceString(SR.ID6005), context.TokenId); context.Reject( - error: Errors.InvalidToken, + error: context.Principal.GetTokenType() switch + { + TokenTypeHints.ClientAssertion => Errors.InvalidClient, + + _ => Errors.InvalidToken + }, description: context.Principal.GetTokenType() switch { TokenTypeHints.AuthorizationCode => SR.GetResourceString(SR.ID2016), @@ -1123,7 +1152,12 @@ public static partial class OpenIddictServerHandlers context.Logger.LogInformation(SR.GetResourceString(SR.ID6006), context.AuthorizationId); context.Reject( - error: Errors.InvalidToken, + error: context.Principal.GetTokenType() switch + { + TokenTypeHints.ClientAssertion => Errors.InvalidClient, + + _ => Errors.InvalidToken + }, description: context.Principal.GetTokenType() switch { TokenTypeHints.AuthorizationCode => SR.GetResourceString(SR.ID2020), diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs index 618b108a..9753d1f5 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs @@ -361,18 +361,6 @@ public static partial class OpenIddictServerHandlers return default; } - // Ensure the specified client_assertion_type is supported. - if (!string.IsNullOrEmpty(context.Request.ClientAssertionType) && - !string.Equals(context.Request.ClientAssertionType, ClientAssertionTypes.JwtBearer, StringComparison.Ordinal)) - { - context.Reject( - error: Errors.InvalidRequest, - description: SR.FormatID2032(Parameters.ClientAssertionType), - uri: SR.FormatID8000(SR.ID2032)); - - return default; - } - // Reject requests that use multiple client authentication methods. // // See https://tools.ietf.org/html/rfc6749#section-2.3 for more information. @@ -389,6 +377,18 @@ public static partial class OpenIddictServerHandlers return default; } + // Ensure the specified client_assertion_type is supported. + if (!string.IsNullOrEmpty(context.Request.ClientAssertionType) && + !context.Options.ClientAssertionTypes.Contains(context.Request.ClientAssertionType)) + { + context.Reject( + error: Errors.InvalidClient, + description: SR.FormatID2032(Parameters.ClientAssertionType), + uri: SR.FormatID8000(SR.ID2032)); + + return default; + } + return default; } } diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs index 91facd3e..55a10146 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs @@ -579,7 +579,7 @@ public static partial class OpenIddictServerHandlers if (context.RejectClientAssertion) { context.Reject( - error: notification.Error ?? Errors.InvalidRequest, + error: notification.Error ?? Errors.InvalidClient, description: notification.ErrorDescription, uri: notification.ErrorUri); return; diff --git a/src/OpenIddict.Server/OpenIddictServerOptions.cs b/src/OpenIddict.Server/OpenIddictServerOptions.cs index 14e92f8d..b13d187e 100644 --- a/src/OpenIddict.Server/OpenIddictServerOptions.cs +++ b/src/OpenIddict.Server/OpenIddictServerOptions.cs @@ -305,9 +305,31 @@ public sealed class OpenIddictServerOptions /// public bool DisableScopeValidation { get; set; } + /// + /// Gets the OAuth 2.0 client assertion types enabled for this application. + /// + public HashSet ClientAssertionTypes { get; } = new(StringComparer.Ordinal) + { + OpenIddictConstants.ClientAssertionTypes.JwtBearer + }; + + /// + /// Gets the OAuth 2.0 client authentication methods enabled for this application. + /// + public HashSet ClientAuthenticationMethods { get; } = new(StringComparer.Ordinal) + { + // Note: client_secret_basic is deliberately not added here as it requires + // a dedicated event handler (typically provided by the host integration) + // to extract the client credentials from the standard Authorization header. + // + // Both the ASP.NET Core and OWIN hosts support the client_secret_basic + // authentication method and automatically add it to this list at runtime. + OpenIddictConstants.ClientAuthenticationMethods.ClientSecretPost, + OpenIddictConstants.ClientAuthenticationMethods.PrivateKeyJwt + }; + /// /// Gets the OAuth 2.0 code challenge methods enabled for this application. - /// By default, only the S256 method is allowed (if the code flow is enabled). /// public HashSet CodeChallengeMethods { get; } = new(StringComparer.Ordinal); diff --git a/test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.Exchange.cs b/test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.Exchange.cs index 836a01d7..b36b7787 100644 --- a/test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.Exchange.cs +++ b/test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.Exchange.cs @@ -15,6 +15,74 @@ namespace OpenIddict.Server.AspNetCore.IntegrationTests; public partial class OpenIddictServerAspNetCoreIntegrationTests : OpenIddictServerIntegrationTests { + [Fact] + public async Task ExtractTokenRequest_ClientSecretFromRequestCausesAnErrorWhenClientSecretPostIsDisabled() + { + // Arrange + await using var server = await CreateServerAsync(options => + { + options.EnableDegradedMode(); + + options.Configure(options => options.ClientAuthenticationMethods.Remove(ClientAuthenticationMethods.ClientSecretPost)); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.Equal(Errors.InvalidClient, response.Error); + Assert.Equal(SR.FormatID2174(ClientAuthenticationMethods.ClientSecretPost), response.ErrorDescription); + } + + [Fact] + public async Task ExtractTokenRequest_ClientSecretFromHeaderCausesAnErrorWhenClientSecretBasicIsDisabled() + { + // Arrange + await using var server = await CreateServerAsync(options => + { + options.EnableDegradedMode(); + + options.Configure(options => options.ClientAuthenticationMethods.Remove(ClientAuthenticationMethods.ClientSecretBasic)); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + var request = context.Transaction.GetHttpRequest()!; + request.Headers[HeaderNames.Authorization] = "Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW"; + + return default; + }); + + builder.SetOrder(int.MinValue); + }); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ClientId = "Fabrikam", + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.Equal(Errors.InvalidClient, response.Error); + Assert.Equal(SR.FormatID2174(ClientAuthenticationMethods.ClientSecretBasic), response.ErrorDescription); + } + [Fact] public async Task ExtractTokenRequest_MultipleClientCredentialsCauseAnError() { diff --git a/test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.Introspection.cs b/test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.Introspection.cs index 893ac784..5b9d51d7 100644 --- a/test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.Introspection.cs +++ b/test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.Introspection.cs @@ -14,6 +14,70 @@ namespace OpenIddict.Server.AspNetCore.IntegrationTests; public partial class OpenIddictServerAspNetCoreIntegrationTests : OpenIddictServerIntegrationTests { + [Fact] + public async Task ExtractIntrospectionRequest_ClientSecretFromRequestCausesAnErrorWhenClientSecretPostIsDisabled() + { + // Arrange + await using var server = await CreateServerAsync(options => + { + options.EnableDegradedMode(); + + options.Configure(options => options.ClientAuthenticationMethods.Remove(ClientAuthenticationMethods.ClientSecretPost)); + }); + + await using var client = await server.CreateClientAsync(); + + // 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.FormatID2174(ClientAuthenticationMethods.ClientSecretPost), response.ErrorDescription); + } + + [Fact] + public async Task ExtractIntrospectionRequest_ClientSecretFromHeaderCausesAnErrorWhenClientSecretBasicIsDisabled() + { + // Arrange + await using var server = await CreateServerAsync(options => + { + options.EnableDegradedMode(); + + options.Configure(options => options.ClientAuthenticationMethods.Remove(ClientAuthenticationMethods.ClientSecretBasic)); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + var request = context.Transaction.GetHttpRequest()!; + request.Headers[HeaderNames.Authorization] = "Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW"; + + return default; + }); + + builder.SetOrder(int.MinValue); + }); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/connect/introspect", new OpenIddictRequest + { + ClientId = "Fabrikam", + Token = "2YotnFZFEjr1zCsicMWpAA" + }); + + // Assert + Assert.Equal(Errors.InvalidClient, response.Error); + Assert.Equal(SR.FormatID2174(ClientAuthenticationMethods.ClientSecretBasic), response.ErrorDescription); + } + [Fact] public async Task ExtractIntrospectionRequest_MultipleClientCredentialsCauseAnError() { diff --git a/test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.Revocation.cs b/test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.Revocation.cs index b9c1f5fb..a53bea6a 100644 --- a/test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.Revocation.cs +++ b/test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.Revocation.cs @@ -14,6 +14,70 @@ namespace OpenIddict.Server.AspNetCore.IntegrationTests; public partial class OpenIddictServerAspNetCoreIntegrationTests : OpenIddictServerIntegrationTests { + [Fact] + public async Task ExtractRevocationRequest_ClientSecretFromRequestCausesAnErrorWhenClientSecretPostIsDisabled() + { + // Arrange + await using var server = await CreateServerAsync(options => + { + options.EnableDegradedMode(); + + options.Configure(options => options.ClientAuthenticationMethods.Remove(ClientAuthenticationMethods.ClientSecretPost)); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/connect/revoke", new OpenIddictRequest + { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + Token = "2YotnFZFEjr1zCsicMWpAA" + }); + + // Assert + Assert.Equal(Errors.InvalidClient, response.Error); + Assert.Equal(SR.FormatID2174(ClientAuthenticationMethods.ClientSecretPost), response.ErrorDescription); + } + + [Fact] + public async Task ExtractRevocationRequest_ClientSecretFromHeaderCausesAnErrorWhenClientSecretBasicIsDisabled() + { + // Arrange + await using var server = await CreateServerAsync(options => + { + options.EnableDegradedMode(); + + options.Configure(options => options.ClientAuthenticationMethods.Remove(ClientAuthenticationMethods.ClientSecretBasic)); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + var request = context.Transaction.GetHttpRequest()!; + request.Headers[HeaderNames.Authorization] = "Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW"; + + return default; + }); + + builder.SetOrder(int.MinValue); + }); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/connect/revoke", new OpenIddictRequest + { + ClientId = "Fabrikam", + Token = "2YotnFZFEjr1zCsicMWpAA" + }); + + // Assert + Assert.Equal(Errors.InvalidClient, response.Error); + Assert.Equal(SR.FormatID2174(ClientAuthenticationMethods.ClientSecretBasic), response.ErrorDescription); + } + [Fact] public async Task ExtractRevocationRequest_MultipleClientCredentialsCauseAnError() { diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Device.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Device.cs index 3d4b5523..98f63e12 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Device.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Device.cs @@ -177,19 +177,26 @@ public abstract partial class OpenIddictServerIntegrationTests public async Task ValidateDeviceRequest_RequestIsRejectedWhenUnsupportedClientAssertionTypeIsSpecified() { // Arrange - await using var server = await CreateServerAsync(options => options.EnableDegradedMode()); + await using var server = await CreateServerAsync(options => + { + options.EnableDegradedMode(); + + options.Configure(options => options.ClientAuthenticationMethods.Remove(ClientAuthenticationMethods.PrivateKeyJwt)); + options.Configure(options => options.ClientAssertionTypes.Remove(ClientAssertionTypes.JwtBearer)); + }); + await using var client = await server.CreateClientAsync(); // Act var response = await client.PostAsync("/connect/device", new OpenIddictRequest { ClientAssertion = "2YotnFZFEjr1zCsicMWpAA", - ClientAssertionType = "unknown", + ClientAssertionType = ClientAssertionTypes.JwtBearer, ClientId = "Fabrikam" }); // Assert - Assert.Equal(Errors.InvalidRequest, response.Error); + Assert.Equal(Errors.InvalidClient, response.Error); Assert.Equal(SR.FormatID2032(Parameters.ClientAssertionType), response.ErrorDescription); Assert.Equal(SR.FormatID8000(SR.ID2032), response.ErrorUri); } diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Discovery.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Discovery.cs index dd24a450..57774301 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Discovery.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Discovery.cs @@ -409,7 +409,12 @@ public abstract partial class OpenIddictServerIntegrationTests public async Task HandleConfigurationRequest_SupportedClientAuthenticationMethodsAreIncludedWhenTokenEndpointIsEnabled() { // Arrange - await using var server = await CreateServerAsync(); + await using var server = await CreateServerAsync(options => options.Configure(options => + { + options.ClientAuthenticationMethods.Remove(ClientAuthenticationMethods.ClientSecretBasic); + options.ClientAuthenticationMethods.Add("custom"); + })); + await using var client = await server.CreateClientAsync(); // Act @@ -418,8 +423,10 @@ public abstract partial class OpenIddictServerIntegrationTests // Assert Assert.NotNull(methods); - Assert.Contains(ClientAuthenticationMethods.ClientSecretBasic, methods); + Assert.Equal(3, methods.Length); Assert.Contains(ClientAuthenticationMethods.ClientSecretPost, methods); + Assert.Contains(ClientAuthenticationMethods.PrivateKeyJwt, methods); + Assert.Contains("custom", methods); } [Fact] @@ -444,7 +451,12 @@ public abstract partial class OpenIddictServerIntegrationTests public async Task HandleConfigurationRequest_SupportedClientAuthenticationMethodsAreIncludedWhenIntrospectionEndpointIsEnabled() { // Arrange - await using var server = await CreateServerAsync(); + await using var server = await CreateServerAsync(options => options.Configure(options => + { + options.ClientAuthenticationMethods.Remove(ClientAuthenticationMethods.ClientSecretBasic); + options.ClientAuthenticationMethods.Add("custom"); + })); + await using var client = await server.CreateClientAsync(); // Act @@ -453,8 +465,10 @@ public abstract partial class OpenIddictServerIntegrationTests // Assert Assert.NotNull(methods); - Assert.Contains(ClientAuthenticationMethods.ClientSecretBasic, methods); + Assert.Equal(3, methods.Length); Assert.Contains(ClientAuthenticationMethods.ClientSecretPost, methods); + Assert.Contains(ClientAuthenticationMethods.PrivateKeyJwt, methods); + Assert.Contains("custom", methods); } [Fact] @@ -479,7 +493,12 @@ public abstract partial class OpenIddictServerIntegrationTests public async Task HandleConfigurationRequest_SupportedClientAuthenticationMethodsAreIncludedWhenRevocationEndpointIsEnabled() { // Arrange - await using var server = await CreateServerAsync(); + await using var server = await CreateServerAsync(options => options.Configure(options => + { + options.ClientAuthenticationMethods.Remove(ClientAuthenticationMethods.ClientSecretBasic); + options.ClientAuthenticationMethods.Add("custom"); + })); + await using var client = await server.CreateClientAsync(); // Act @@ -488,8 +507,10 @@ public abstract partial class OpenIddictServerIntegrationTests // Assert Assert.NotNull(methods); - Assert.Contains(ClientAuthenticationMethods.ClientSecretBasic, methods); + Assert.Equal(3, methods.Length); Assert.Contains(ClientAuthenticationMethods.ClientSecretPost, methods); + Assert.Contains(ClientAuthenticationMethods.PrivateKeyJwt, methods); + Assert.Contains("custom", methods); } [Fact] @@ -515,7 +536,12 @@ public abstract partial class OpenIddictServerIntegrationTests public async Task HandleConfigurationRequest_SupportedClientAuthenticationMethodsAreIncludedWhenDeviceEndpointIsEnabled() { // Arrange - await using var server = await CreateServerAsync(); + await using var server = await CreateServerAsync(options => options.Configure(options => + { + options.ClientAuthenticationMethods.Remove(ClientAuthenticationMethods.ClientSecretBasic); + options.ClientAuthenticationMethods.Add("custom"); + })); + await using var client = await server.CreateClientAsync(); // Act @@ -524,8 +550,10 @@ public abstract partial class OpenIddictServerIntegrationTests // Assert Assert.NotNull(methods); - Assert.Contains(ClientAuthenticationMethods.ClientSecretBasic, methods); + Assert.Equal(3, methods.Length); Assert.Contains(ClientAuthenticationMethods.ClientSecretPost, methods); + Assert.Contains(ClientAuthenticationMethods.PrivateKeyJwt, methods); + Assert.Contains("custom", methods); } [Fact] diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs index f3d2e929..4f7e6db5 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs @@ -1418,20 +1418,27 @@ public abstract partial class OpenIddictServerIntegrationTests public async Task ValidateTokenRequest_RequestIsRejectedWhenUnsupportedClientAssertionTypeIsSpecified() { // Arrange - await using var server = await CreateServerAsync(options => options.EnableDegradedMode()); + await using var server = await CreateServerAsync(options => + { + options.EnableDegradedMode(); + + options.Configure(options => options.ClientAuthenticationMethods.Remove(ClientAuthenticationMethods.PrivateKeyJwt)); + options.Configure(options => options.ClientAssertionTypes.Remove(ClientAssertionTypes.JwtBearer)); + }); + await using var client = await server.CreateClientAsync(); // Act var response = await client.PostAsync("/connect/token", new OpenIddictRequest { ClientAssertion = "2YotnFZFEjr1zCsicMWpAA", - ClientAssertionType = "unknown", + ClientAssertionType = ClientAssertionTypes.JwtBearer, ClientId = "Fabrikam", GrantType = GrantTypes.ClientCredentials }); // Assert - Assert.Equal(Errors.InvalidRequest, response.Error); + Assert.Equal(Errors.InvalidClient, response.Error); Assert.Equal(SR.FormatID2032(Parameters.ClientAssertionType), response.ErrorDescription); Assert.Equal(SR.FormatID8000(SR.ID2032), response.ErrorUri); } diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Introspection.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Introspection.cs index 8f47aa50..6875934b 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Introspection.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Introspection.cs @@ -200,20 +200,27 @@ public abstract partial class OpenIddictServerIntegrationTests public async Task ValidateIntrospectionRequest_RequestIsRejectedWhenUnsupportedClientAssertionTypeIsSpecified() { // Arrange - await using var server = await CreateServerAsync(options => options.EnableDegradedMode()); + await using var server = await CreateServerAsync(options => + { + options.EnableDegradedMode(); + + options.Configure(options => options.ClientAuthenticationMethods.Remove(ClientAuthenticationMethods.PrivateKeyJwt)); + options.Configure(options => options.ClientAssertionTypes.Remove(ClientAssertionTypes.JwtBearer)); + }); + await using var client = await server.CreateClientAsync(); // Act var response = await client.PostAsync("/connect/introspect", new OpenIddictRequest { ClientAssertion = "2YotnFZFEjr1zCsicMWpAA", - ClientAssertionType = "unknown", + ClientAssertionType = ClientAssertionTypes.JwtBearer, ClientId = "Fabrikam", Token = "2YotnFZFEjr1zCsicMWpAA" }); // Assert - Assert.Equal(Errors.InvalidRequest, response.Error); + Assert.Equal(Errors.InvalidClient, response.Error); Assert.Equal(SR.FormatID2032(Parameters.ClientAssertionType), response.ErrorDescription); Assert.Equal(SR.FormatID8000(SR.ID2032), response.ErrorUri); } diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Revocation.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Revocation.cs index aa027893..b87e32db 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Revocation.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Revocation.cs @@ -199,20 +199,27 @@ public abstract partial class OpenIddictServerIntegrationTests public async Task ValidateRevocationRequest_RequestIsRejectedWhenUnsupportedClientAssertionTypeIsSpecified() { // Arrange - await using var server = await CreateServerAsync(options => options.EnableDegradedMode()); + await using var server = await CreateServerAsync(options => + { + options.EnableDegradedMode(); + + options.Configure(options => options.ClientAuthenticationMethods.Remove(ClientAuthenticationMethods.PrivateKeyJwt)); + options.Configure(options => options.ClientAssertionTypes.Remove(ClientAssertionTypes.JwtBearer)); + }); + await using var client = await server.CreateClientAsync(); // Act var response = await client.PostAsync("/connect/revoke", new OpenIddictRequest { ClientAssertion = "2YotnFZFEjr1zCsicMWpAA", - ClientAssertionType = "unknown", + ClientAssertionType = ClientAssertionTypes.JwtBearer, ClientId = "Fabrikam", Token = "2YotnFZFEjr1zCsicMWpAA" }); // Assert - Assert.Equal(Errors.InvalidRequest, response.Error); + Assert.Equal(Errors.InvalidClient, response.Error); Assert.Equal(SR.FormatID2032(Parameters.ClientAssertionType), response.ErrorDescription); Assert.Equal(SR.FormatID8000(SR.ID2032), response.ErrorUri); } diff --git a/test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.Exchange.cs b/test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.Exchange.cs index 7f87161f..9bc2a00d 100644 --- a/test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.Exchange.cs +++ b/test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.Exchange.cs @@ -13,6 +13,74 @@ namespace OpenIddict.Server.Owin.IntegrationTests; public partial class OpenIddictServerOwinIntegrationTests : OpenIddictServerIntegrationTests { + [Fact] + public async Task ExtractTokenRequest_ClientSecretFromRequestCausesAnErrorWhenClientSecretPostIsDisabled() + { + // Arrange + await using var server = await CreateServerAsync(options => + { + options.EnableDegradedMode(); + + options.Configure(options => options.ClientAuthenticationMethods.Remove(ClientAuthenticationMethods.ClientSecretPost)); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.Equal(Errors.InvalidClient, response.Error); + Assert.Equal(SR.FormatID2174(ClientAuthenticationMethods.ClientSecretPost), response.ErrorDescription); + } + + [Fact] + public async Task ExtractTokenRequest_ClientSecretFromHeaderCausesAnErrorWhenClientSecretBasicIsDisabled() + { + // Arrange + await using var server = await CreateServerAsync(options => + { + options.EnableDegradedMode(); + + options.Configure(options => options.ClientAuthenticationMethods.Remove(ClientAuthenticationMethods.ClientSecretBasic)); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + var request = context.Transaction.GetOwinRequest()!; + request.Headers["Authorization"] = "Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW"; + + return default; + }); + + builder.SetOrder(int.MinValue); + }); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/connect/token", new OpenIddictRequest + { + ClientId = "Fabrikam", + GrantType = GrantTypes.Password, + Username = "johndoe", + Password = "A3ddj3w" + }); + + // Assert + Assert.Equal(Errors.InvalidClient, response.Error); + Assert.Equal(SR.FormatID2174(ClientAuthenticationMethods.ClientSecretBasic), response.ErrorDescription); + } + [Fact] public async Task ExtractTokenRequest_MultipleClientCredentialsCauseAnError() { diff --git a/test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.Introspection.cs b/test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.Introspection.cs index a68e918c..9e3be940 100644 --- a/test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.Introspection.cs +++ b/test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.Introspection.cs @@ -13,6 +13,70 @@ namespace OpenIddict.Server.Owin.IntegrationTests; public partial class OpenIddictServerOwinIntegrationTests : OpenIddictServerIntegrationTests { + [Fact] + public async Task ExtractIntrospectionRequest_ClientSecretFromRequestCausesAnErrorWhenClientSecretPostIsDisabled() + { + // Arrange + await using var server = await CreateServerAsync(options => + { + options.EnableDegradedMode(); + + options.Configure(options => options.ClientAuthenticationMethods.Remove(ClientAuthenticationMethods.ClientSecretPost)); + }); + + await using var client = await server.CreateClientAsync(); + + // 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.FormatID2174(ClientAuthenticationMethods.ClientSecretPost), response.ErrorDescription); + } + + [Fact] + public async Task ExtractIntrospectionRequest_ClientSecretFromHeaderCausesAnErrorWhenClientSecretBasicIsDisabled() + { + // Arrange + await using var server = await CreateServerAsync(options => + { + options.EnableDegradedMode(); + + options.Configure(options => options.ClientAuthenticationMethods.Remove(ClientAuthenticationMethods.ClientSecretBasic)); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + var request = context.Transaction.GetOwinRequest()!; + request.Headers["Authorization"] = "Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW"; + + return default; + }); + + builder.SetOrder(int.MinValue); + }); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/connect/introspect", new OpenIddictRequest + { + ClientId = "Fabrikam", + Token = "2YotnFZFEjr1zCsicMWpAA" + }); + + // Assert + Assert.Equal(Errors.InvalidClient, response.Error); + Assert.Equal(SR.FormatID2174(ClientAuthenticationMethods.ClientSecretBasic), response.ErrorDescription); + } + [Fact] public async Task ExtractIntrospectionRequest_MultipleClientCredentialsCauseAnError() { diff --git a/test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.Revocation.cs b/test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.Revocation.cs index 800a7acc..9fef46ab 100644 --- a/test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.Revocation.cs +++ b/test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.Revocation.cs @@ -13,6 +13,70 @@ namespace OpenIddict.Server.Owin.IntegrationTests; public partial class OpenIddictServerOwinIntegrationTests : OpenIddictServerIntegrationTests { + [Fact] + public async Task ExtractRevocationRequest_ClientSecretFromRequestCausesAnErrorWhenClientSecretPostIsDisabled() + { + // Arrange + await using var server = await CreateServerAsync(options => + { + options.EnableDegradedMode(); + + options.Configure(options => options.ClientAuthenticationMethods.Remove(ClientAuthenticationMethods.ClientSecretPost)); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/connect/revoke", new OpenIddictRequest + { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + Token = "2YotnFZFEjr1zCsicMWpAA" + }); + + // Assert + Assert.Equal(Errors.InvalidClient, response.Error); + Assert.Equal(SR.FormatID2174(ClientAuthenticationMethods.ClientSecretPost), response.ErrorDescription); + } + + [Fact] + public async Task ExtractRevocationRequest_ClientSecretFromHeaderCausesAnErrorWhenClientSecretBasicIsDisabled() + { + // Arrange + await using var server = await CreateServerAsync(options => + { + options.EnableDegradedMode(); + + options.Configure(options => options.ClientAuthenticationMethods.Remove(ClientAuthenticationMethods.ClientSecretBasic)); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + var request = context.Transaction.GetOwinRequest()!; + request.Headers["Authorization"] = "Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW"; + + return default; + }); + + builder.SetOrder(int.MinValue); + }); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/connect/revoke", new OpenIddictRequest + { + ClientId = "Fabrikam", + Token = "2YotnFZFEjr1zCsicMWpAA" + }); + + // Assert + Assert.Equal(Errors.InvalidClient, response.Error); + Assert.Equal(SR.FormatID2174(ClientAuthenticationMethods.ClientSecretBasic), response.ErrorDescription); + } + [Fact] public async Task ExtractRevocationRequest_MultipleClientCredentialsCauseAnError() {