Browse Source

Allow configuring the supported client authentication methods and use invalid_client for client assertion errors

pull/1897/head
Kévin Chalet 2 years ago
parent
commit
00fa3f3494
  1. 1
      src/OpenIddict.Abstractions/OpenIddictConstants.cs
  2. 12
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  3. 2
      src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
  4. 3
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConfiguration.cs
  5. 1
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Device.cs
  6. 1
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Exchange.cs
  7. 1
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Introspection.cs
  8. 1
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Revocation.cs
  9. 71
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs
  10. 3
      src/OpenIddict.Server.Owin/OpenIddictServerOwinConfiguration.cs
  11. 1
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Device.cs
  12. 1
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Exchange.cs
  13. 1
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Introspection.cs
  14. 1
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Revocation.cs
  15. 68
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs
  16. 24
      src/OpenIddict.Server/OpenIddictServerConfiguration.cs
  17. 4
      src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs
  18. 16
      src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs
  19. 24
      src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs
  20. 24
      src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs
  21. 50
      src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs
  22. 24
      src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs
  23. 2
      src/OpenIddict.Server/OpenIddictServerHandlers.cs
  24. 24
      src/OpenIddict.Server/OpenIddictServerOptions.cs
  25. 68
      test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.Exchange.cs
  26. 64
      test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.Introspection.cs
  27. 64
      test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.Revocation.cs
  28. 13
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Device.cs
  29. 44
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Discovery.cs
  30. 13
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs
  31. 13
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Introspection.cs
  32. 13
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Revocation.cs
  33. 68
      test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.Exchange.cs
  34. 64
      test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.Introspection.cs
  35. 64
      test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.Revocation.cs

1
src/OpenIddict.Abstractions/OpenIddictConstants.cs

@ -167,6 +167,7 @@ public static class OpenIddictConstants
public const string ClientSecretBasic = "client_secret_basic"; public const string ClientSecretBasic = "client_secret_basic";
public const string ClientSecretJwt = "client_secret_jwt"; public const string ClientSecretJwt = "client_secret_jwt";
public const string ClientSecretPost = "client_secret_post"; public const string ClientSecretPost = "client_secret_post";
public const string None = "none";
public const string PrivateKeyJwt = "private_key_jwt"; public const string PrivateKeyJwt = "private_key_jwt";
} }

12
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -1563,6 +1563,12 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId
<data name="ID0418" xml:space="preserve"> <data name="ID0418" xml:space="preserve">
<value>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.</value> <value>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.</value>
</data> </data>
<data name="ID0419" xml:space="preserve">
<value>At least one client authentication method must be configured when enabling the device authorization, introspection, revocation or token endpoints.</value>
</data>
<data name="ID0420" xml:space="preserve">
<value>The '{0}' client assertion type must be configured when enabling the '{1}' client authentication method.</value>
</data>
<data name="ID2000" xml:space="preserve"> <data name="ID2000" xml:space="preserve">
<value>The security token is missing.</value> <value>The security token is missing.</value>
</data> </data>
@ -2082,6 +2088,9 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId
<data name="ID2173" xml:space="preserve"> <data name="ID2173" xml:space="preserve">
<value>The '{0}' claim returned in the specified client assertion doesn't match the expected value.</value> <value>The '{0}' claim returned in the specified client assertion doesn't match the expected value.</value>
</data> </data>
<data name="ID2174" xml:space="preserve">
<value>The '{0}' client authentication method is not supported.</value>
</data>
<data name="ID4000" xml:space="preserve"> <data name="ID4000" xml:space="preserve">
<value>The '{0}' parameter shouldn't be null or empty at this point.</value> <value>The '{0}' parameter shouldn't be null or empty at this point.</value>
</data> </data>
@ -2724,6 +2733,9 @@ This may indicate that the hashed entry is corrupted or malformed.</value>
<data name="ID6226" xml:space="preserve"> <data name="ID6226" xml:space="preserve">
<value>The authentication demand was rejected because the public application '{ClientId}' was not allowed to send a client assertion.</value> <value>The authentication demand was rejected because the public application '{ClientId}' was not allowed to send a client assertion.</value>
</data> </data>
<data name="ID6227" xml:space="preserve">
<value>The request was rejected because the '{Method}' client authentication method that was used by the client application is not enabled in the server options.</value>
</data>
<data name="ID8000" xml:space="preserve"> <data name="ID8000" xml:space="preserve">
<value>https://documentation.openiddict.com/errors/{0}</value> <value>https://documentation.openiddict.com/errors/{0}</value>
</data> </data>

2
src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs

@ -1401,7 +1401,7 @@ public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplication
var value = await Store.GetClientSecretAsync(application, cancellationToken); var value = await Store.GetClientSecretAsync(application, cancellationToken);
if (string.IsNullOrEmpty(value)) 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; return false;
} }

3
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. // Register the built-in event handlers used by the OpenIddict ASP.NET Core server components.
options.Handlers.AddRange(OpenIddictServerAspNetCoreHandlers.DefaultHandlers); options.Handlers.AddRange(OpenIddictServerAspNetCoreHandlers.DefaultHandlers);
// Enable client_secret_basic support by default.
options.ClientAuthenticationMethods.Add(ClientAuthenticationMethods.ClientSecretBasic);
} }
/// <inheritdoc/> /// <inheritdoc/>

1
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Device.cs

@ -19,6 +19,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers
* Device request extraction: * Device request extraction:
*/ */
ExtractPostRequest<ExtractDeviceRequestContext>.Descriptor, ExtractPostRequest<ExtractDeviceRequestContext>.Descriptor,
ValidateClientAuthenticationMethod<ExtractDeviceRequestContext>.Descriptor,
ExtractBasicAuthenticationCredentials<ExtractDeviceRequestContext>.Descriptor, ExtractBasicAuthenticationCredentials<ExtractDeviceRequestContext>.Descriptor,
/* /*

1
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Exchange.cs

@ -17,6 +17,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers
* Token request extraction: * Token request extraction:
*/ */
ExtractPostRequest<ExtractTokenRequestContext>.Descriptor, ExtractPostRequest<ExtractTokenRequestContext>.Descriptor,
ValidateClientAuthenticationMethod<ExtractTokenRequestContext>.Descriptor,
ExtractBasicAuthenticationCredentials<ExtractTokenRequestContext>.Descriptor, ExtractBasicAuthenticationCredentials<ExtractTokenRequestContext>.Descriptor,
/* /*

1
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Introspection.cs

@ -17,6 +17,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers
* Introspection request extraction: * Introspection request extraction:
*/ */
ExtractGetOrPostRequest<ExtractIntrospectionRequestContext>.Descriptor, ExtractGetOrPostRequest<ExtractIntrospectionRequestContext>.Descriptor,
ValidateClientAuthenticationMethod<ExtractIntrospectionRequestContext>.Descriptor,
ExtractBasicAuthenticationCredentials<ExtractIntrospectionRequestContext>.Descriptor, ExtractBasicAuthenticationCredentials<ExtractIntrospectionRequestContext>.Descriptor,
/* /*

1
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Revocation.cs

@ -17,6 +17,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers
* Revocation request extraction: * Revocation request extraction:
*/ */
ExtractPostRequest<ExtractRevocationRequestContext>.Descriptor, ExtractPostRequest<ExtractRevocationRequestContext>.Descriptor,
ValidateClientAuthenticationMethod<ExtractRevocationRequestContext>.Descriptor,
ExtractBasicAuthenticationCredentials<ExtractRevocationRequestContext>.Descriptor, ExtractBasicAuthenticationCredentials<ExtractRevocationRequestContext>.Descriptor,
/* /*

71
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs

@ -642,6 +642,75 @@ public static partial class OpenIddictServerAspNetCoreHandlers
} }
} }
/// <summary>
/// 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.
/// </summary>
public sealed class ValidateClientAuthenticationMethod<TContext> : IOpenIddictServerHandler<TContext>
where TContext : BaseValidatingContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ValidateClientAuthenticationMethod<TContext>>()
.SetOrder(ExtractPostRequest<TContext>.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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;
}
}
/// <summary> /// <summary>
/// Contains the logic responsible for extracting client credentials from the standard HTTP Authorization header. /// 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. /// 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<TContext>() = OpenIddictServerHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpRequest>() .AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ExtractBasicAuthenticationCredentials<TContext>>() .UseSingletonHandler<ExtractBasicAuthenticationCredentials<TContext>>()
.SetOrder(ExtractPostRequest<TContext>.Descriptor.Order + 1_000) .SetOrder(ValidateClientAuthenticationMethod<TContext>.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn) .SetType(OpenIddictServerHandlerType.BuiltIn)
.Build(); .Build();

3
src/OpenIddict.Server.Owin/OpenIddictServerOwinConfiguration.cs

@ -26,6 +26,9 @@ public sealed class OpenIddictServerOwinConfiguration : IConfigureOptions<OpenId
// Register the built-in event handlers used by the OpenIddict OWIN server components. // Register the built-in event handlers used by the OpenIddict OWIN server components.
options.Handlers.AddRange(OpenIddictServerOwinHandlers.DefaultHandlers); options.Handlers.AddRange(OpenIddictServerOwinHandlers.DefaultHandlers);
// Enable client_secret_basic support by default.
options.ClientAuthenticationMethods.Add(ClientAuthenticationMethods.ClientSecretBasic);
} }
/// <inheritdoc/> /// <inheritdoc/>

1
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Device.cs

@ -19,6 +19,7 @@ public static partial class OpenIddictServerOwinHandlers
* Device request extraction: * Device request extraction:
*/ */
ExtractPostRequest<ExtractDeviceRequestContext>.Descriptor, ExtractPostRequest<ExtractDeviceRequestContext>.Descriptor,
ValidateClientAuthenticationMethod<ExtractDeviceRequestContext>.Descriptor,
ExtractBasicAuthenticationCredentials<ExtractDeviceRequestContext>.Descriptor, ExtractBasicAuthenticationCredentials<ExtractDeviceRequestContext>.Descriptor,
/* /*

1
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Exchange.cs

@ -17,6 +17,7 @@ public static partial class OpenIddictServerOwinHandlers
* Token request extraction: * Token request extraction:
*/ */
ExtractPostRequest<ExtractTokenRequestContext>.Descriptor, ExtractPostRequest<ExtractTokenRequestContext>.Descriptor,
ValidateClientAuthenticationMethod<ExtractTokenRequestContext>.Descriptor,
ExtractBasicAuthenticationCredentials<ExtractTokenRequestContext>.Descriptor, ExtractBasicAuthenticationCredentials<ExtractTokenRequestContext>.Descriptor,
/* /*

1
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Introspection.cs

@ -17,6 +17,7 @@ public static partial class OpenIddictServerOwinHandlers
* Introspection request extraction: * Introspection request extraction:
*/ */
ExtractGetOrPostRequest<ExtractIntrospectionRequestContext>.Descriptor, ExtractGetOrPostRequest<ExtractIntrospectionRequestContext>.Descriptor,
ValidateClientAuthenticationMethod<ExtractIntrospectionRequestContext>.Descriptor,
ExtractBasicAuthenticationCredentials<ExtractIntrospectionRequestContext>.Descriptor, ExtractBasicAuthenticationCredentials<ExtractIntrospectionRequestContext>.Descriptor,
/* /*

1
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Revocation.cs

@ -17,6 +17,7 @@ public static partial class OpenIddictServerOwinHandlers
* Revocation request extraction: * Revocation request extraction:
*/ */
ExtractPostRequest<ExtractRevocationRequestContext>.Descriptor, ExtractPostRequest<ExtractRevocationRequestContext>.Descriptor,
ValidateClientAuthenticationMethod<ExtractRevocationRequestContext>.Descriptor,
ExtractBasicAuthenticationCredentials<ExtractRevocationRequestContext>.Descriptor, ExtractBasicAuthenticationCredentials<ExtractRevocationRequestContext>.Descriptor,
/* /*

68
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs

@ -695,6 +695,72 @@ public static partial class OpenIddictServerOwinHandlers
} }
} }
/// <summary>
/// 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.
/// </summary>
public sealed class ValidateClientAuthenticationMethod<TContext> : IOpenIddictServerHandler<TContext>
where TContext : BaseValidatingContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireOwinRequest>()
.UseSingletonHandler<ValidateClientAuthenticationMethod<TContext>>()
.SetOrder(ExtractPostRequest<TContext>.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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;
}
}
/// <summary> /// <summary>
/// Contains the logic responsible for extracting client credentials from the standard HTTP Authorization header. /// 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. /// 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<TContext>() = OpenIddictServerHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireOwinRequest>() .AddFilter<RequireOwinRequest>()
.UseSingletonHandler<ExtractBasicAuthenticationCredentials<TContext>>() .UseSingletonHandler<ExtractBasicAuthenticationCredentials<TContext>>()
.SetOrder(ExtractPostRequest<TContext>.Descriptor.Order + 1_000) .SetOrder(ValidateClientAuthenticationMethod<TContext>.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn) .SetType(OpenIddictServerHandlerType.BuiltIn)
.Build(); .Build();

24
src/OpenIddict.Server/OpenIddictServerConfiguration.cs

@ -127,6 +127,30 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions<OpenId
} }
} }
// Ensure at least one client authentication method is enabled (unless no non-interactive endpoint was enabled).
if (options.ClientAuthenticationMethods.Count is 0 && (options.DeviceEndpointUris.Count is not 0 ||
options.IntrospectionEndpointUris.Count is not 0 ||
options.RevocationEndpointUris.Count is not 0 ||
options.TokenEndpointUris.Count is not 0))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0419));
}
// Ensure the client authentication methods/client assertion types configuration is consistent.
if (options.ClientAuthenticationMethods.Contains(ClientAuthenticationMethods.PrivateKeyJwt) &&
!options.ClientAssertionTypes.Contains(ClientAssertionTypes.JwtBearer))
{
throw new InvalidOperationException(SR.FormatID0420(
ClientAssertionTypes.JwtBearer, ClientAuthenticationMethods.PrivateKeyJwt));
}
if (options.ClientAuthenticationMethods.Contains(ClientAuthenticationMethods.ClientSecretJwt) &&
!options.ClientAssertionTypes.Contains(ClientAssertionTypes.JwtBearer))
{
throw new InvalidOperationException(SR.FormatID0420(
ClientAssertionTypes.JwtBearer, ClientAuthenticationMethods.ClientSecretJwt));
}
// Ensure reference tokens support was not enabled when token storage is disabled. // Ensure reference tokens support was not enabled when token storage is disabled.
if (options.DisableTokenStorage && (options.UseReferenceAccessTokens || options.UseReferenceRefreshTokens)) if (options.DisableTokenStorage && (options.UseReferenceAccessTokens || options.UseReferenceRefreshTokens))
{ {

4
src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs

@ -433,10 +433,10 @@ public static partial class OpenIddictServerHandlers
// Ensure the specified client_assertion_type is supported. // Ensure the specified client_assertion_type is supported.
if (!string.IsNullOrEmpty(context.Request.ClientAssertionType) && if (!string.IsNullOrEmpty(context.Request.ClientAssertionType) &&
!string.Equals(context.Request.ClientAssertionType, ClientAssertionTypes.JwtBearer, StringComparison.Ordinal)) !context.Options.ClientAssertionTypes.Contains(context.Request.ClientAssertionType))
{ {
context.Reject( context.Reject(
error: Errors.InvalidRequest, error: Errors.InvalidClient,
description: SR.FormatID2032(Parameters.ClientAssertionType), description: SR.FormatID2032(Parameters.ClientAssertionType),
uri: SR.FormatID8000(SR.ID2032)); uri: SR.FormatID8000(SR.ID2032));

16
src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs

@ -510,30 +510,22 @@ public static partial class OpenIddictServerHandlers
// but is supported by OpenIddict 4.3.0 and higher for consistency with the other endpoints. // but is supported by OpenIddict 4.3.0 and higher for consistency with the other endpoints.
if (context.DeviceEndpoint is not null) if (context.DeviceEndpoint is not null)
{ {
context.DeviceEndpointAuthenticationMethods.Add(ClientAuthenticationMethods.ClientSecretBasic); context.DeviceEndpointAuthenticationMethods.UnionWith(context.Options.ClientAuthenticationMethods);
context.DeviceEndpointAuthenticationMethods.Add(ClientAuthenticationMethods.ClientSecretPost);
context.DeviceEndpointAuthenticationMethods.Add(ClientAuthenticationMethods.PrivateKeyJwt);
} }
if (context.IntrospectionEndpoint is not null) if (context.IntrospectionEndpoint is not null)
{ {
context.IntrospectionEndpointAuthenticationMethods.Add(ClientAuthenticationMethods.ClientSecretBasic); context.IntrospectionEndpointAuthenticationMethods.UnionWith(context.Options.ClientAuthenticationMethods);
context.IntrospectionEndpointAuthenticationMethods.Add(ClientAuthenticationMethods.ClientSecretPost);
context.IntrospectionEndpointAuthenticationMethods.Add(ClientAuthenticationMethods.PrivateKeyJwt);
} }
if (context.RevocationEndpoint is not null) if (context.RevocationEndpoint is not null)
{ {
context.RevocationEndpointAuthenticationMethods.Add(ClientAuthenticationMethods.ClientSecretBasic); context.RevocationEndpointAuthenticationMethods.UnionWith(context.Options.ClientAuthenticationMethods);
context.RevocationEndpointAuthenticationMethods.Add(ClientAuthenticationMethods.ClientSecretPost);
context.RevocationEndpointAuthenticationMethods.Add(ClientAuthenticationMethods.PrivateKeyJwt);
} }
if (context.TokenEndpoint is not null) if (context.TokenEndpoint is not null)
{ {
context.TokenEndpointAuthenticationMethods.Add(ClientAuthenticationMethods.ClientSecretBasic); context.TokenEndpointAuthenticationMethods.UnionWith(context.Options.ClientAuthenticationMethods);
context.TokenEndpointAuthenticationMethods.Add(ClientAuthenticationMethods.ClientSecretPost);
context.TokenEndpointAuthenticationMethods.Add(ClientAuthenticationMethods.PrivateKeyJwt);
} }
return default; return default;

24
src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs

@ -551,18 +551,6 @@ public static partial class OpenIddictServerHandlers
return default; 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. // Reject requests that use multiple client authentication methods.
// //
// See https://tools.ietf.org/html/rfc6749#section-2.3 for more information. // See https://tools.ietf.org/html/rfc6749#section-2.3 for more information.
@ -579,6 +567,18 @@ public static partial class OpenIddictServerHandlers
return default; 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;
}
// Reject grant_type=client_credentials requests missing the client credentials. // Reject grant_type=client_credentials requests missing the client credentials.
// //
// See https://tools.ietf.org/html/rfc6749#section-4.4.1 for more information. // See https://tools.ietf.org/html/rfc6749#section-4.4.1 for more information.

24
src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs

@ -414,18 +414,6 @@ public static partial class OpenIddictServerHandlers
return default; 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. // Reject requests that use multiple client authentication methods.
// //
// See https://tools.ietf.org/html/rfc6749#section-2.3 for more information. // See https://tools.ietf.org/html/rfc6749#section-2.3 for more information.
@ -442,6 +430,18 @@ public static partial class OpenIddictServerHandlers
return default; 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; return default;
} }
} }

50
src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs

@ -298,7 +298,13 @@ public static partial class OpenIddictServerHandlers
})) }))
{ {
context.Reject( 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 description: context.ValidTokenTypes.Count switch
{ {
1 when context.ValidTokenTypes.Contains(TokenTypeHints.AuthorizationCode) 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.Logger.LogTrace(result.Exception, SR.GetResourceString(SR.ID6000), context.Token);
context.Reject( 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 description: result.Exception switch
{ {
SecurityTokenInvalidTypeException => context.ValidTokenTypes.Count switch SecurityTokenInvalidTypeException => context.ValidTokenTypes.Count switch
@ -791,7 +803,13 @@ public static partial class OpenIddictServerHandlers
if (context.Principal is null) if (context.Principal is null)
{ {
context.Reject( 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 description: context.ValidTokenTypes.Count switch
{ {
1 when context.ValidTokenTypes.Contains(TokenTypeHints.AuthorizationCode) 1 when context.ValidTokenTypes.Contains(TokenTypeHints.AuthorizationCode)
@ -874,8 +892,9 @@ public static partial class OpenIddictServerHandlers
context.Reject( context.Reject(
error: context.Principal.GetTokenType() switch error: context.Principal.GetTokenType() switch
{ {
TokenTypeHints.DeviceCode => Errors.ExpiredToken, TokenTypeHints.ClientAssertion => Errors.InvalidClient,
_ => Errors.InvalidToken TokenTypeHints.DeviceCode => Errors.ExpiredToken,
_ => Errors.InvalidToken
}, },
description: context.Principal.GetTokenType() switch description: context.Principal.GetTokenType() switch
{ {
@ -953,7 +972,12 @@ public static partial class OpenIddictServerHandlers
context.Logger.LogInformation(SR.GetResourceString(SR.ID6002), context.TokenId); context.Logger.LogInformation(SR.GetResourceString(SR.ID6002), context.TokenId);
context.Reject( context.Reject(
error: Errors.InvalidToken, error: context.Principal.GetTokenType() switch
{
TokenTypeHints.ClientAssertion => Errors.InvalidClient,
_ => Errors.InvalidToken
},
description: context.Principal.GetTokenType() switch description: context.Principal.GetTokenType() switch
{ {
TokenTypeHints.AuthorizationCode => SR.GetResourceString(SR.ID2010), 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.Logger.LogInformation(SR.GetResourceString(SR.ID6005), context.TokenId);
context.Reject( context.Reject(
error: Errors.InvalidToken, error: context.Principal.GetTokenType() switch
{
TokenTypeHints.ClientAssertion => Errors.InvalidClient,
_ => Errors.InvalidToken
},
description: context.Principal.GetTokenType() switch description: context.Principal.GetTokenType() switch
{ {
TokenTypeHints.AuthorizationCode => SR.GetResourceString(SR.ID2016), 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.Logger.LogInformation(SR.GetResourceString(SR.ID6006), context.AuthorizationId);
context.Reject( context.Reject(
error: Errors.InvalidToken, error: context.Principal.GetTokenType() switch
{
TokenTypeHints.ClientAssertion => Errors.InvalidClient,
_ => Errors.InvalidToken
},
description: context.Principal.GetTokenType() switch description: context.Principal.GetTokenType() switch
{ {
TokenTypeHints.AuthorizationCode => SR.GetResourceString(SR.ID2020), TokenTypeHints.AuthorizationCode => SR.GetResourceString(SR.ID2020),

24
src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs

@ -361,18 +361,6 @@ public static partial class OpenIddictServerHandlers
return default; 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. // Reject requests that use multiple client authentication methods.
// //
// See https://tools.ietf.org/html/rfc6749#section-2.3 for more information. // See https://tools.ietf.org/html/rfc6749#section-2.3 for more information.
@ -389,6 +377,18 @@ public static partial class OpenIddictServerHandlers
return default; 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; return default;
} }
} }

2
src/OpenIddict.Server/OpenIddictServerHandlers.cs

@ -579,7 +579,7 @@ public static partial class OpenIddictServerHandlers
if (context.RejectClientAssertion) if (context.RejectClientAssertion)
{ {
context.Reject( context.Reject(
error: notification.Error ?? Errors.InvalidRequest, error: notification.Error ?? Errors.InvalidClient,
description: notification.ErrorDescription, description: notification.ErrorDescription,
uri: notification.ErrorUri); uri: notification.ErrorUri);
return; return;

24
src/OpenIddict.Server/OpenIddictServerOptions.cs

@ -305,9 +305,31 @@ public sealed class OpenIddictServerOptions
/// </summary> /// </summary>
public bool DisableScopeValidation { get; set; } public bool DisableScopeValidation { get; set; }
/// <summary>
/// Gets the OAuth 2.0 client assertion types enabled for this application.
/// </summary>
public HashSet<string> ClientAssertionTypes { get; } = new(StringComparer.Ordinal)
{
OpenIddictConstants.ClientAssertionTypes.JwtBearer
};
/// <summary>
/// Gets the OAuth 2.0 client authentication methods enabled for this application.
/// </summary>
public HashSet<string> 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
};
/// <summary> /// <summary>
/// Gets the OAuth 2.0 code challenge methods enabled for this application. /// 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).
/// </summary> /// </summary>
public HashSet<string> CodeChallengeMethods { get; } = new(StringComparer.Ordinal); public HashSet<string> CodeChallengeMethods { get; } = new(StringComparer.Ordinal);

68
test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.Exchange.cs

@ -15,6 +15,74 @@ namespace OpenIddict.Server.AspNetCore.IntegrationTests;
public partial class OpenIddictServerAspNetCoreIntegrationTests : OpenIddictServerIntegrationTests 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<ExtractTokenRequestContext>(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] [Fact]
public async Task ExtractTokenRequest_MultipleClientCredentialsCauseAnError() public async Task ExtractTokenRequest_MultipleClientCredentialsCauseAnError()
{ {

64
test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.Introspection.cs

@ -14,6 +14,70 @@ namespace OpenIddict.Server.AspNetCore.IntegrationTests;
public partial class OpenIddictServerAspNetCoreIntegrationTests : OpenIddictServerIntegrationTests 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<ExtractIntrospectionRequestContext>(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] [Fact]
public async Task ExtractIntrospectionRequest_MultipleClientCredentialsCauseAnError() public async Task ExtractIntrospectionRequest_MultipleClientCredentialsCauseAnError()
{ {

64
test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.Revocation.cs

@ -14,6 +14,70 @@ namespace OpenIddict.Server.AspNetCore.IntegrationTests;
public partial class OpenIddictServerAspNetCoreIntegrationTests : OpenIddictServerIntegrationTests 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<ExtractRevocationRequestContext>(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] [Fact]
public async Task ExtractRevocationRequest_MultipleClientCredentialsCauseAnError() public async Task ExtractRevocationRequest_MultipleClientCredentialsCauseAnError()
{ {

13
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Device.cs

@ -177,19 +177,26 @@ public abstract partial class OpenIddictServerIntegrationTests
public async Task ValidateDeviceRequest_RequestIsRejectedWhenUnsupportedClientAssertionTypeIsSpecified() public async Task ValidateDeviceRequest_RequestIsRejectedWhenUnsupportedClientAssertionTypeIsSpecified()
{ {
// Arrange // 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(); await using var client = await server.CreateClientAsync();
// Act // Act
var response = await client.PostAsync("/connect/device", new OpenIddictRequest var response = await client.PostAsync("/connect/device", new OpenIddictRequest
{ {
ClientAssertion = "2YotnFZFEjr1zCsicMWpAA", ClientAssertion = "2YotnFZFEjr1zCsicMWpAA",
ClientAssertionType = "unknown", ClientAssertionType = ClientAssertionTypes.JwtBearer,
ClientId = "Fabrikam" ClientId = "Fabrikam"
}); });
// Assert // Assert
Assert.Equal(Errors.InvalidRequest, response.Error); Assert.Equal(Errors.InvalidClient, response.Error);
Assert.Equal(SR.FormatID2032(Parameters.ClientAssertionType), response.ErrorDescription); Assert.Equal(SR.FormatID2032(Parameters.ClientAssertionType), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2032), response.ErrorUri); Assert.Equal(SR.FormatID8000(SR.ID2032), response.ErrorUri);
} }

44
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Discovery.cs

@ -409,7 +409,12 @@ public abstract partial class OpenIddictServerIntegrationTests
public async Task HandleConfigurationRequest_SupportedClientAuthenticationMethodsAreIncludedWhenTokenEndpointIsEnabled() public async Task HandleConfigurationRequest_SupportedClientAuthenticationMethodsAreIncludedWhenTokenEndpointIsEnabled()
{ {
// Arrange // 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(); await using var client = await server.CreateClientAsync();
// Act // Act
@ -418,8 +423,10 @@ public abstract partial class OpenIddictServerIntegrationTests
// Assert // Assert
Assert.NotNull(methods); Assert.NotNull(methods);
Assert.Contains(ClientAuthenticationMethods.ClientSecretBasic, methods); Assert.Equal(3, methods.Length);
Assert.Contains(ClientAuthenticationMethods.ClientSecretPost, methods); Assert.Contains(ClientAuthenticationMethods.ClientSecretPost, methods);
Assert.Contains(ClientAuthenticationMethods.PrivateKeyJwt, methods);
Assert.Contains("custom", methods);
} }
[Fact] [Fact]
@ -444,7 +451,12 @@ public abstract partial class OpenIddictServerIntegrationTests
public async Task HandleConfigurationRequest_SupportedClientAuthenticationMethodsAreIncludedWhenIntrospectionEndpointIsEnabled() public async Task HandleConfigurationRequest_SupportedClientAuthenticationMethodsAreIncludedWhenIntrospectionEndpointIsEnabled()
{ {
// Arrange // 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(); await using var client = await server.CreateClientAsync();
// Act // Act
@ -453,8 +465,10 @@ public abstract partial class OpenIddictServerIntegrationTests
// Assert // Assert
Assert.NotNull(methods); Assert.NotNull(methods);
Assert.Contains(ClientAuthenticationMethods.ClientSecretBasic, methods); Assert.Equal(3, methods.Length);
Assert.Contains(ClientAuthenticationMethods.ClientSecretPost, methods); Assert.Contains(ClientAuthenticationMethods.ClientSecretPost, methods);
Assert.Contains(ClientAuthenticationMethods.PrivateKeyJwt, methods);
Assert.Contains("custom", methods);
} }
[Fact] [Fact]
@ -479,7 +493,12 @@ public abstract partial class OpenIddictServerIntegrationTests
public async Task HandleConfigurationRequest_SupportedClientAuthenticationMethodsAreIncludedWhenRevocationEndpointIsEnabled() public async Task HandleConfigurationRequest_SupportedClientAuthenticationMethodsAreIncludedWhenRevocationEndpointIsEnabled()
{ {
// Arrange // 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(); await using var client = await server.CreateClientAsync();
// Act // Act
@ -488,8 +507,10 @@ public abstract partial class OpenIddictServerIntegrationTests
// Assert // Assert
Assert.NotNull(methods); Assert.NotNull(methods);
Assert.Contains(ClientAuthenticationMethods.ClientSecretBasic, methods); Assert.Equal(3, methods.Length);
Assert.Contains(ClientAuthenticationMethods.ClientSecretPost, methods); Assert.Contains(ClientAuthenticationMethods.ClientSecretPost, methods);
Assert.Contains(ClientAuthenticationMethods.PrivateKeyJwt, methods);
Assert.Contains("custom", methods);
} }
[Fact] [Fact]
@ -515,7 +536,12 @@ public abstract partial class OpenIddictServerIntegrationTests
public async Task HandleConfigurationRequest_SupportedClientAuthenticationMethodsAreIncludedWhenDeviceEndpointIsEnabled() public async Task HandleConfigurationRequest_SupportedClientAuthenticationMethodsAreIncludedWhenDeviceEndpointIsEnabled()
{ {
// Arrange // 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(); await using var client = await server.CreateClientAsync();
// Act // Act
@ -524,8 +550,10 @@ public abstract partial class OpenIddictServerIntegrationTests
// Assert // Assert
Assert.NotNull(methods); Assert.NotNull(methods);
Assert.Contains(ClientAuthenticationMethods.ClientSecretBasic, methods); Assert.Equal(3, methods.Length);
Assert.Contains(ClientAuthenticationMethods.ClientSecretPost, methods); Assert.Contains(ClientAuthenticationMethods.ClientSecretPost, methods);
Assert.Contains(ClientAuthenticationMethods.PrivateKeyJwt, methods);
Assert.Contains("custom", methods);
} }
[Fact] [Fact]

13
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs

@ -1418,20 +1418,27 @@ public abstract partial class OpenIddictServerIntegrationTests
public async Task ValidateTokenRequest_RequestIsRejectedWhenUnsupportedClientAssertionTypeIsSpecified() public async Task ValidateTokenRequest_RequestIsRejectedWhenUnsupportedClientAssertionTypeIsSpecified()
{ {
// Arrange // 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(); await using var client = await server.CreateClientAsync();
// Act // Act
var response = await client.PostAsync("/connect/token", new OpenIddictRequest var response = await client.PostAsync("/connect/token", new OpenIddictRequest
{ {
ClientAssertion = "2YotnFZFEjr1zCsicMWpAA", ClientAssertion = "2YotnFZFEjr1zCsicMWpAA",
ClientAssertionType = "unknown", ClientAssertionType = ClientAssertionTypes.JwtBearer,
ClientId = "Fabrikam", ClientId = "Fabrikam",
GrantType = GrantTypes.ClientCredentials GrantType = GrantTypes.ClientCredentials
}); });
// Assert // Assert
Assert.Equal(Errors.InvalidRequest, response.Error); Assert.Equal(Errors.InvalidClient, response.Error);
Assert.Equal(SR.FormatID2032(Parameters.ClientAssertionType), response.ErrorDescription); Assert.Equal(SR.FormatID2032(Parameters.ClientAssertionType), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2032), response.ErrorUri); Assert.Equal(SR.FormatID8000(SR.ID2032), response.ErrorUri);
} }

13
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Introspection.cs

@ -200,20 +200,27 @@ public abstract partial class OpenIddictServerIntegrationTests
public async Task ValidateIntrospectionRequest_RequestIsRejectedWhenUnsupportedClientAssertionTypeIsSpecified() public async Task ValidateIntrospectionRequest_RequestIsRejectedWhenUnsupportedClientAssertionTypeIsSpecified()
{ {
// Arrange // 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(); await using var client = await server.CreateClientAsync();
// Act // Act
var response = await client.PostAsync("/connect/introspect", new OpenIddictRequest var response = await client.PostAsync("/connect/introspect", new OpenIddictRequest
{ {
ClientAssertion = "2YotnFZFEjr1zCsicMWpAA", ClientAssertion = "2YotnFZFEjr1zCsicMWpAA",
ClientAssertionType = "unknown", ClientAssertionType = ClientAssertionTypes.JwtBearer,
ClientId = "Fabrikam", ClientId = "Fabrikam",
Token = "2YotnFZFEjr1zCsicMWpAA" Token = "2YotnFZFEjr1zCsicMWpAA"
}); });
// Assert // Assert
Assert.Equal(Errors.InvalidRequest, response.Error); Assert.Equal(Errors.InvalidClient, response.Error);
Assert.Equal(SR.FormatID2032(Parameters.ClientAssertionType), response.ErrorDescription); Assert.Equal(SR.FormatID2032(Parameters.ClientAssertionType), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2032), response.ErrorUri); Assert.Equal(SR.FormatID8000(SR.ID2032), response.ErrorUri);
} }

13
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Revocation.cs

@ -199,20 +199,27 @@ public abstract partial class OpenIddictServerIntegrationTests
public async Task ValidateRevocationRequest_RequestIsRejectedWhenUnsupportedClientAssertionTypeIsSpecified() public async Task ValidateRevocationRequest_RequestIsRejectedWhenUnsupportedClientAssertionTypeIsSpecified()
{ {
// Arrange // 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(); await using var client = await server.CreateClientAsync();
// Act // Act
var response = await client.PostAsync("/connect/revoke", new OpenIddictRequest var response = await client.PostAsync("/connect/revoke", new OpenIddictRequest
{ {
ClientAssertion = "2YotnFZFEjr1zCsicMWpAA", ClientAssertion = "2YotnFZFEjr1zCsicMWpAA",
ClientAssertionType = "unknown", ClientAssertionType = ClientAssertionTypes.JwtBearer,
ClientId = "Fabrikam", ClientId = "Fabrikam",
Token = "2YotnFZFEjr1zCsicMWpAA" Token = "2YotnFZFEjr1zCsicMWpAA"
}); });
// Assert // Assert
Assert.Equal(Errors.InvalidRequest, response.Error); Assert.Equal(Errors.InvalidClient, response.Error);
Assert.Equal(SR.FormatID2032(Parameters.ClientAssertionType), response.ErrorDescription); Assert.Equal(SR.FormatID2032(Parameters.ClientAssertionType), response.ErrorDescription);
Assert.Equal(SR.FormatID8000(SR.ID2032), response.ErrorUri); Assert.Equal(SR.FormatID8000(SR.ID2032), response.ErrorUri);
} }

68
test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.Exchange.cs

@ -13,6 +13,74 @@ namespace OpenIddict.Server.Owin.IntegrationTests;
public partial class OpenIddictServerOwinIntegrationTests : OpenIddictServerIntegrationTests 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<ExtractTokenRequestContext>(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] [Fact]
public async Task ExtractTokenRequest_MultipleClientCredentialsCauseAnError() public async Task ExtractTokenRequest_MultipleClientCredentialsCauseAnError()
{ {

64
test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.Introspection.cs

@ -13,6 +13,70 @@ namespace OpenIddict.Server.Owin.IntegrationTests;
public partial class OpenIddictServerOwinIntegrationTests : OpenIddictServerIntegrationTests 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<ExtractIntrospectionRequestContext>(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] [Fact]
public async Task ExtractIntrospectionRequest_MultipleClientCredentialsCauseAnError() public async Task ExtractIntrospectionRequest_MultipleClientCredentialsCauseAnError()
{ {

64
test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.Revocation.cs

@ -13,6 +13,70 @@ namespace OpenIddict.Server.Owin.IntegrationTests;
public partial class OpenIddictServerOwinIntegrationTests : OpenIddictServerIntegrationTests 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<ExtractRevocationRequestContext>(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] [Fact]
public async Task ExtractRevocationRequest_MultipleClientCredentialsCauseAnError() public async Task ExtractRevocationRequest_MultipleClientCredentialsCauseAnError()
{ {

Loading…
Cancel
Save