diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Discovery.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Discovery.cs index 42d7586c..bb217bbb 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Discovery.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Discovery.cs @@ -23,8 +23,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers AmendGrantTypes.Descriptor, AmendCodeChallengeMethods.Descriptor, AmendScopes.Descriptor, - AmendDeviceAuthorizationEndpointClientAuthenticationMethods.Descriptor, - AmendTokenEndpointClientAuthenticationMethods.Descriptor, + AmendClientAuthenticationMethods.Descriptor, AmendEndpoints.Descriptor ]; @@ -230,6 +229,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers /// Contains the logic responsible for amending the client authentication methods /// supported by the device authorization endpoint for the providers that require it. /// + [Obsolete("This class is obsolete and will be removed in a future version.", error: true)] public sealed class AmendDeviceAuthorizationEndpointClientAuthenticationMethods : IOpenIddictClientHandler { /// @@ -244,32 +244,14 @@ public static partial class OpenIddictClientWebIntegrationHandlers /// public ValueTask HandleAsync(HandleConfigurationResponseContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - // Google doesn't properly implement the device authorization grant, doesn't support - // basic client authentication for the device authorization endpoint and returns a - // generic "invalid_request" request when using "client_secret_basic" instead of - // sending the client identifier in the request form. To work around this limitation, - // "client_secret_post" is listed as the only supported client authentication method. - if (context.Registration.ProviderType is ProviderTypes.Google) - { - context.Configuration.DeviceAuthorizationEndpointAuthMethodsSupported.Clear(); - context.Configuration.DeviceAuthorizationEndpointAuthMethodsSupported.Add( - ClientAuthenticationMethods.ClientSecretPost); - } - - return default; - } + => throw new NotSupportedException(SR.GetResourceString(SR.ID0403)); } /// /// Contains the logic responsible for amending the client authentication /// methods supported by the token endpoint for the providers that require it. /// + [Obsolete("This class is obsolete and will be removed in a future version.", error: true)] public sealed class AmendTokenEndpointClientAuthenticationMethods : IOpenIddictClientHandler { /// @@ -282,6 +264,27 @@ public static partial class OpenIddictClientWebIntegrationHandlers .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); + /// + public ValueTask HandleAsync(HandleConfigurationResponseContext context) + => throw new NotSupportedException(SR.GetResourceString(SR.ID0403)); + } + + /// + /// Contains the logic responsible for amending the supported client + /// authentication methods for the providers that require it. + /// + public sealed class AmendClientAuthenticationMethods : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ExtractTokenEndpointClientAuthenticationMethods.Descriptor.Order + 500) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + /// public ValueTask HandleAsync(HandleConfigurationResponseContext context) { @@ -290,8 +293,8 @@ public static partial class OpenIddictClientWebIntegrationHandlers throw new ArgumentNullException(nameof(context)); } - // Apple implements a non-standard client authentication method for the token endpoint - // that is inspired by the standard private_key_jwt method but doesn't use the standard + // Apple implements a non-standard client authentication method for its endpoints that + // is inspired by the standard private_key_jwt method but doesn't use the standard // client_assertion/client_assertion_type parameters. Instead, the client assertion // must be sent as a "dynamic" client secret using client_secret_post. Since the logic // is the same as private_key_jwt, the configuration is amended to assume Apple supports @@ -299,10 +302,25 @@ public static partial class OpenIddictClientWebIntegrationHandlers // parameter using the client assertion once it has been generated by OpenIddict. if (context.Registration.ProviderType is ProviderTypes.Apple) { + context.Configuration.RevocationEndpointAuthMethodsSupported.Add( + ClientAuthenticationMethods.PrivateKeyJwt); + context.Configuration.TokenEndpointAuthMethodsSupported.Add( ClientAuthenticationMethods.PrivateKeyJwt); } + // Google doesn't properly implement the device authorization grant, doesn't support + // basic client authentication for the device authorization endpoint and returns + // a generic "invalid_request" error when using "client_secret_basic" instead of + // sending the client identifier in the request form. To work around this limitation, + // "client_secret_post" is listed as the only supported client authentication method. + else if (context.Registration.ProviderType is ProviderTypes.Google) + { + context.Configuration.DeviceAuthorizationEndpointAuthMethodsSupported.Clear(); + context.Configuration.DeviceAuthorizationEndpointAuthMethodsSupported.Add( + ClientAuthenticationMethods.ClientSecretPost); + } + // LinkedIn doesn't support sending the client credentials using basic authentication but // doesn't return a "token_endpoint_auth_methods_supported" node containing alternative // authentication methods, making basic authentication the default authentication method. diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs index 5be17d9f..f55005ed 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs @@ -49,6 +49,12 @@ public static partial class OpenIddictClientWebIntegrationHandlers IncludeStateParameterInRedirectUri.Descriptor, AttachAdditionalChallengeParameters.Descriptor, + /* + * Revocation processing: + */ + AttachNonStandardRevocationClientAssertionClaims.Descriptor, + AttachRevocationRequestNonStandardClientCredentials.Descriptor, + ..Authentication.DefaultHandlers, ..Device.DefaultHandlers, ..Discovery.DefaultHandlers, @@ -371,8 +377,8 @@ public static partial class OpenIddictClientWebIntegrationHandlers } /// - /// Contains the logic responsible for amending the client - /// assertion methods for the providers that require it. + /// Contains the logic responsible for adding non-standard claims to the client + /// assertions used for the token endpoint for the providers that require it. /// public sealed class AttachNonStandardClientAssertionClaims : IOpenIddictClientHandler { @@ -491,8 +497,8 @@ public static partial class OpenIddictClientWebIntegrationHandlers Debug.Assert(context.TokenRequest is not null, SR.GetResourceString(SR.ID4008)); - // Apple implements a non-standard client authentication method for the token endpoint - // that is inspired by the standard private_key_jwt method but doesn't use the standard + // Apple implements a non-standard client authentication method for its endpoints that + // is inspired by the standard private_key_jwt method but doesn't use the standard // client_assertion/client_assertion_type parameters. Instead, the client assertion // must be sent as a "dynamic" client secret using client_secret_post. Since the logic // is the same as private_key_jwt, the configuration is amended to assume Apple supports @@ -1536,4 +1542,94 @@ public static partial class OpenIddictClientWebIntegrationHandlers return default; } } + + /// + /// Contains the logic responsible for adding non-standard claims to the client + /// assertions used for the revocation endpoint for the providers that require it. + /// + public sealed class AttachNonStandardRevocationClientAssertionClaims : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(PrepareRevocationClientAssertionPrincipal.Descriptor.Order + 500) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessRevocationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.ClientAssertionPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + + // For client assertions to be considered valid by the Apple ID authentication service, + // the team identifier associated with the developer account MUST be used as the issuer + // and the static "https://appleid.apple.com" URL MUST be used as the token audience. + // + // For more information about the custom client authentication method implemented by Apple, + // see https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens. + if (context.Registration.ProviderType is ProviderTypes.Apple) + { + var settings = context.Registration.GetAppleSettings(); + + context.ClientAssertionPrincipal.SetClaim(Claims.Private.Issuer, settings.TeamId); + context.ClientAssertionPrincipal.SetAudiences("https://appleid.apple.com"); + } + + return default; + } + } + + /// + /// Contains the logic responsible for attaching custom client credentials + /// parameters to the revocation request for the providers that require it. + /// + public sealed class AttachRevocationRequestNonStandardClientCredentials : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(AttachRevocationRequestClientCredentials.Descriptor.Order + 500) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessRevocationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.RevocationRequest is not null, SR.GetResourceString(SR.ID4008)); + + // Apple implements a non-standard client authentication method for its endpoints that + // is inspired by the standard private_key_jwt method but doesn't use the standard + // client_assertion/client_assertion_type parameters. Instead, the client assertion + // must be sent as a "dynamic" client secret using client_secret_post. Since the logic + // is the same as private_key_jwt, the configuration is amended to assume Apple supports + // private_key_jwt and an event handler is responsible for populating the client_secret + // parameter using the client assertion once it has been generated by OpenIddict. + if (context.Registration.ProviderType is ProviderTypes.Apple) + { + context.RevocationRequest.ClientSecret = context.RevocationRequest.ClientAssertion; + context.RevocationRequest.ClientAssertion = null; + context.RevocationRequest.ClientAssertionType = null; + } + + return default; + } + } } diff --git a/src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs b/src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs index 47394bdb..526f4b59 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs +++ b/src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs @@ -116,7 +116,9 @@ public static class OpenIddictClientHandlerFilters /// /// Represents a filter that excludes the associated handlers if no client assertion is generated. /// - public sealed class RequireClientAssertionGenerated : IOpenIddictClientHandlerFilter + public sealed class RequireClientAssertionGenerated : + IOpenIddictClientHandlerFilter, + IOpenIddictClientHandlerFilter { /// public ValueTask IsActiveAsync(ProcessAuthenticationContext context) @@ -128,6 +130,17 @@ public static class OpenIddictClientHandlerFilters return new(context.GenerateClientAssertion); } + + /// + ValueTask IOpenIddictClientHandlerFilter.IsActiveAsync(ProcessRevocationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new(context.GenerateClientAssertion); + } } ///