diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs index 7d0d2986..a8c568b8 100644 --- a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs +++ b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs @@ -680,7 +680,8 @@ public static partial class OpenIddictClientSystemNetHttpHandlers // If the returned Content-Type doesn't indicate the response has a JSON payload, // ignore it and allow other handlers in the pipeline to process the HTTP response. if (!string.Equals(response.Content.Headers.ContentType?.MediaType, - MediaTypes.Json, StringComparison.OrdinalIgnoreCase)) + MediaTypes.Json, StringComparison.OrdinalIgnoreCase) && + !HasJsonStructuredSyntaxSuffix(response.Content.Headers.ContentType)) { return; } @@ -708,6 +709,15 @@ public static partial class OpenIddictClientSystemNetHttpHandlers return; } + + static bool HasJsonStructuredSyntaxSuffix(MediaTypeHeaderValue? type) => + // If the length of the media type is less than the expected number of characters needed + // to compose a JSON-derived type (i.e application/*+json), assume the content is not JSON. + type?.MediaType is { Length: >= 18 } && + // JSON media types MUST always start with "application/". + type.MediaType.AsSpan(0, 12).Equals("application/".AsSpan(), StringComparison.OrdinalIgnoreCase) && + // JSON media types MUST always end with "+json". + type.MediaType.AsSpan()[^5..].Equals("+json".AsSpan(), StringComparison.OrdinalIgnoreCase); } } diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Discovery.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Discovery.cs index c9495164..cdeaf2ca 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Discovery.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Discovery.cs @@ -20,8 +20,9 @@ public static partial class OpenIddictClientWebIntegrationHandlers */ AmendIssuer.Descriptor, AmendGrantTypes.Descriptor, - AmendTokenEndpointClientAuthenticationMethods.Descriptor, AmendCodeChallengeMethods.Descriptor, + AmendScopes.Descriptor, + AmendTokenEndpointClientAuthenticationMethods.Descriptor, AmendEndpoints.Descriptor); /// @@ -120,18 +121,18 @@ public static partial class OpenIddictClientWebIntegrationHandlers } /// - /// Contains the logic responsible for amending the client authentication - /// methods supported by the token endpoint for the providers that require it. + /// Contains the logic responsible for amending the supported + /// code challenge methods for the providers that require it. /// - public sealed class AmendTokenEndpointClientAuthenticationMethods : IOpenIddictClientHandler + public sealed class AmendCodeChallengeMethods : IOpenIddictClientHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(ExtractTokenEndpointClientAuthenticationMethods.Descriptor.Order + 500) + .UseSingletonHandler() + .SetOrder(ExtractCodeChallengeMethods.Descriptor.Order + 500) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); @@ -143,17 +144,14 @@ 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 - // 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 token once it has been generated by OpenIddict. - if (context.Registration.ProviderName is Providers.Apple) + // Microsoft Account supports both the "plain" and "S256" code challenge methods but + // doesn't list them in the server configuration metadata. To ensure the OpenIddict + // client uses Proof Key for Code Exchange for the Microsoft provider, the 2 methods + // are manually added to the list of supported code challenge methods by this handler. + if (context.Registration.ProviderName is Providers.Microsoft) { - context.Configuration.TokenEndpointAuthMethodsSupported.Add( - ClientAuthenticationMethods.PrivateKeyJwt); + context.Configuration.CodeChallengeMethodsSupported.Add(CodeChallengeMethods.Plain); + context.Configuration.CodeChallengeMethodsSupported.Add(CodeChallengeMethods.Sha256); } return default; @@ -161,18 +159,17 @@ public static partial class OpenIddictClientWebIntegrationHandlers } /// - /// Contains the logic responsible for amending the supported - /// code challenge methods for the providers that require it. + /// Contains the logic responsible for amending the supported scopes for the providers that require it. /// - public sealed class AmendCodeChallengeMethods : IOpenIddictClientHandler + public sealed class AmendScopes : IOpenIddictClientHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(ExtractCodeChallengeMethods.Descriptor.Order + 500) + .UseSingletonHandler() + .SetOrder(ExtractScopes.Descriptor.Order + 500) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); @@ -184,14 +181,54 @@ public static partial class OpenIddictClientWebIntegrationHandlers throw new ArgumentNullException(nameof(context)); } - // Microsoft Account supports both the "plain" and "S256" code challenge methods but - // doesn't list them in the server configuration metadata. To ensure the OpenIddict - // client uses Proof Key for Code Exchange for the Microsoft provider, the 2 methods - // are manually added to the list of supported code challenge methods by this handler. - if (context.Registration.ProviderName is Providers.Microsoft) + // While it is a recommended node, Xero doesn't include "scopes_supported" in its server + // configuration and thus is treated as an OAuth 2.0-only provider by the OpenIddict client. + // + // To avoid that, the "openid" scope is manually added to indicate OpenID Connect is supported. + if (context.Registration.ProviderName is Providers.Xero) { - context.Configuration.CodeChallengeMethodsSupported.Add(CodeChallengeMethods.Plain); - context.Configuration.CodeChallengeMethodsSupported.Add(CodeChallengeMethods.Sha256); + context.Configuration.ScopesSupported.Add(Scopes.OpenId); + } + + return default; + } + } + + /// + /// Contains the logic responsible for amending the client authentication + /// methods supported by the token endpoint for the providers that require it. + /// + public sealed class AmendTokenEndpointClientAuthenticationMethods : 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) + { + if (context is null) + { + 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 + // 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 token once it has been generated by OpenIddict. + if (context.Registration.ProviderName is Providers.Apple) + { + context.Configuration.TokenEndpointAuthMethodsSupported.Add( + ClientAuthenticationMethods.PrivateKeyJwt); } return default; diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs index 825d9dc1..fe50ffe4 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs @@ -211,13 +211,11 @@ public static partial class OpenIddictClientWebIntegrationHandlers response.Content.Headers.ContentType = context.Registration.ProviderName switch { - Providers.Mixcloud or // Mixcloud returns JSON-formatted contents declared as "text/javascript". - Providers.Patreon or // Patreon returns JSON-formatted contents declared as "application/vnd.api+json". - Providers.Vimeo // Vimeo returns JSON-formatted contents declared as "application/vnd.vimeo.user+json". - => new MediaTypeHeaderValue(MediaTypes.Json) - { - CharSet = Charsets.Utf8 - }, + // Mixcloud returns JSON-formatted contents declared as "text/javascript". + Providers.Mixcloud => new MediaTypeHeaderValue(MediaTypes.Json) + { + CharSet = Charsets.Utf8 + }, _ => response.Content.Headers.ContentType }; diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml index aa49e234..dba95b85 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml @@ -824,6 +824,18 @@ + + + + + +