diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Authentication.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Authentication.cs new file mode 100644 index 00000000..0713218c --- /dev/null +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Authentication.cs @@ -0,0 +1,62 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System.Collections.Immutable; +using static OpenIddict.Client.WebIntegration.OpenIddictClientWebIntegrationConstants; + +namespace OpenIddict.Client.WebIntegration; + +public static partial class OpenIddictClientWebIntegrationHandlers +{ + public static class Authentication + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Authorization request preparation: + */ + MapNonStandardRequestParameters.Descriptor); + + /// + /// Contains the logic responsible for mapping non-standard request parameters + /// to their standard equivalent for the providers that require it. + /// + public sealed class MapNonStandardRequestParameters : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(PrepareAuthorizationRequestContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Some providers implement old drafts of the OAuth 2.0 specification that didn't support + // the "response_type" parameter but relied on a "type" parameter to determine the type + // of flow (web server or user agent-based). Since the "user_agent" value more or less + // corresponds to the legacy OAuth 2.0-only implicit flow, it is deliberately not + // supported, so the only supported value is "web_server" (aka authorization code flow). + + if (context.Registration.ProviderName is Providers.Basecamp) + { + context.Request["type"] = "web_server"; + context.Request.ResponseType = null; + } + + return default; + } + } + } +} diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Exchange.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Exchange.cs index 5da911b1..db8154bf 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Exchange.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Exchange.cs @@ -26,6 +26,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers /* * Token request preparation: */ + MapNonStandardRequestParameters.Descriptor, AttachNonStandardBasicAuthenticationCredentials.Descriptor, AttachNonStandardRequestHeaders.Descriptor, AttachNonStandardQueryStringParameters.Descriptor, @@ -36,6 +37,51 @@ public static partial class OpenIddictClientWebIntegrationHandlers */ MapNonStandardResponseParameters.Descriptor); + /// + /// Contains the logic responsible for mapping non-standard request parameters + /// to their standard equivalent for the providers that require it. + /// + public sealed class MapNonStandardRequestParameters : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(PrepareTokenRequestContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Some providers implement old drafts of the OAuth 2.0 specification that + // didn't support the "response_type" parameter but relied on a "type" + // parameter to determine the type of request (web server or refresh). + + if (context.Registration.ProviderName is Providers.Basecamp) + { + context.Request["type"] = context.Request.GrantType switch + { + GrantTypes.AuthorizationCode => "web_server", + GrantTypes.RefreshToken => "refresh", + + _ => null + }; + + context.Request.GrantType = null; + } + + return default; + } + } + /// /// Contains the logic responsible for attaching the client credentials to the HTTP Authorization /// header using a non-standard construction logic for the providers that require it. @@ -259,8 +305,8 @@ public static partial class OpenIddictClientWebIntegrationHandlers } /// - /// Contains the logic responsible for attaching non-standard query string - /// parameters to the token request for the providers that require it. + /// Contains the logic responsible for mapping non-standard response parameters + /// to their standard equivalent for the providers that require it. /// public sealed class MapNonStandardResponseParameters : IOpenIddictClientHandler { diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs index 44679c25..6f618c29 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs @@ -257,6 +257,13 @@ public static partial class OpenIddictClientWebIntegrationHandlers context.Response = context.Registration.ProviderName switch { + // Basecamp returns a nested "identity" object and a collection of "accounts". + Providers.Basecamp => new(context.Response["identity"]?.GetNamedParameters() ?? + throw new InvalidOperationException(SR.FormatID0334("identity"))) + { + ["accounts"] = context.Response["accounts"] + }, + // Fitbit returns a nested "user" object. Providers.Fitbit => new(context.Response["user"]?.GetNamedParameters() ?? throw new InvalidOperationException(SR.FormatID0334("user"))), diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs index 493cbc91..940ab599 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs @@ -41,6 +41,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers FormatNonStandardScopeParameter.Descriptor, IncludeStateParameterInRedirectUri.Descriptor, AttachAdditionalChallengeParameters.Descriptor) + .AddRange(Authentication.DefaultHandlers) .AddRange(Discovery.DefaultHandlers) .AddRange(Exchange.DefaultHandlers) .AddRange(Protection.DefaultHandlers) @@ -881,7 +882,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers // By default, Google doesn't return a refresh token but allows sending an "access_type" // parameter to retrieve one (but it is only returned during the first authorization dance). - if (context.Registration.ProviderName is Providers.Google) + else if (context.Registration.ProviderName is Providers.Google) { var options = context.Registration.GetGoogleOptions(); diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml index 94755cb5..ae64b25b 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml @@ -86,6 +86,33 @@ + + + + + + + + + + + + +