diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Exchange.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Exchange.cs index 3b490df6..f061a2cb 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Exchange.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Exchange.cs @@ -360,9 +360,9 @@ public static partial class OpenIddictClientWebIntegrationHandlers context.Response.RefreshToken = null; } - // Note: Alibaba Cloud and Exact Online returns a non-standard "expires_in" - // parameter formatted as a string instead of a numeric type. - if (context.Registration.ProviderType is ProviderTypes.AlibabaCloud or ProviderTypes.ExactOnline && + // Note: Alibaba Cloud, Exact Online, and NetSuite return a non-standard + // "expires_in" parameter formatted as a string instead of a numeric type. + if (context.Registration.ProviderType is ProviderTypes.AlibabaCloud or ProviderTypes.ExactOnline or ProviderTypes.NetSuite && long.TryParse((string?) context.Response[Parameters.ExpiresIn], NumberStyles.Integer, CultureInfo.InvariantCulture, out long value)) { diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Introspection.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Introspection.cs new file mode 100644 index 00000000..fb59cdc0 --- /dev/null +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Introspection.cs @@ -0,0 +1,80 @@ +/* + * 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 System.Text.Json; +using OpenIddict.Extensions; +using static OpenIddict.Client.WebIntegration.OpenIddictClientWebIntegrationConstants; + +namespace OpenIddict.Client.WebIntegration; + +public static partial class OpenIddictClientWebIntegrationHandlers +{ + public static class Introspection + { + public static ImmutableArray DefaultHandlers { get; } = + [ + /* + * Introspection response extraction: + */ + MapNonStandardResponseParameters.Descriptor + ]; + + /// + /// 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 + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MaxValue - 50_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ExtractIntrospectionResponseContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.Response is null) + { + return default; + } + + // Note: NetSuite returns the "scope" parameter as a non-standard JSON array of strings, + // which requires converting it to a space-separated string to ensure the response is + // not rejected by OpenIddict when validating the type of the well-known "scope" claim. + if (context.Registration.ProviderType is ProviderTypes.NetSuite && + (JsonElement?) context.Response[Parameters.Scope] is { ValueKind: JsonValueKind.Array } element && + OpenIddictHelpers.ValidateArrayElements(element, JsonValueKind.String)) + { + var scopes = new HashSet(StringComparer.Ordinal); + + foreach (var item in element.EnumerateArray()) + { + var scope = item.GetString()?.ToLowerInvariant(); + if (!string.IsNullOrEmpty(scope)) + { + scopes.Add(scope); + } + } + + context.Response.Scope = string.Join(" ", scopes); + } + + return default; + } + } + } +} diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs index 2d68a583..ad42e689 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs @@ -62,6 +62,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers .. Device.DefaultHandlers, .. Discovery.DefaultHandlers, .. Exchange.DefaultHandlers, + .. Introspection.DefaultHandlers, .. Protection.DefaultHandlers, .. Revocation.DefaultHandlers, .. UserInfo.DefaultHandlers @@ -826,6 +827,11 @@ public static partial class OpenIddictClientWebIntegrationHandlers // While PayPal claims the OpenID Connect flavor of the code flow is supported, // their implementation doesn't return an id_token from the token endpoint. ProviderTypes.PayPal => (false, false, false), + + // NetSuite does not return an id_token when using the refresh_token grant type. + // Additionally, the at_hash inside their id_token is not a valid hash of the + // access token, but is instead a copy of the RS256 signature within the access token. + ProviderTypes.NetSuite => (true, false, false), _ => (context.ExtractBackchannelIdentityToken, context.RequireBackchannelIdentityToken, diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml index 6e1cf300..6b7e4f81 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml @@ -1584,7 +1584,24 @@ + + + + + + + +