From 03bd5756789b50659d55bef39afb1d26793a6dfb Mon Sep 17 00:00:00 2001 From: Timoxa <78698518+t1moH1ch@users.noreply.github.com> Date: Tue, 11 Feb 2025 00:18:39 +0300 Subject: [PATCH] Add Yandex and VK ID to the list of supported providers --- .../OpenIddictResources.resx | 3 + ...ClientWebIntegrationHandlers.Revocation.cs | 39 ++++++- ...ctClientWebIntegrationHandlers.Userinfo.cs | 10 +- .../OpenIddictClientWebIntegrationHandlers.cs | 100 ++++++++++++++---- ...penIddictClientWebIntegrationProviders.xml | 54 ++++++++++ 5 files changed, 180 insertions(+), 26 deletions(-) diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx index 4e260d06..25987cc0 100644 --- a/src/OpenIddict.Abstractions/OpenIddictResources.resx +++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx @@ -1707,6 +1707,9 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId A token must be specified when using revocation. + + The VK ID integration requires sending the device identifier to the token and revocation endpoints. For that, attach a ".device_id" authentication property containing the device identifier returned by the authorization endpoint. + The security token is missing. diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Revocation.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Revocation.cs index d99a5cb4..e504d4f3 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Revocation.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Revocation.cs @@ -21,6 +21,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers /* * Revocation request preparation: */ + MapNonStandardRequestParameters.Descriptor, OverrideHttpMethod.Descriptor, AttachBearerAccessToken.Descriptor, @@ -30,6 +31,43 @@ public static partial class OpenIddictClientWebIntegrationHandlers NormalizeContentType.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(PrepareRevocationRequestContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Weibo, VK ID and Yandex don't support the standard "token" parameter and + // require using the non-standard "access_token" parameter instead. + if (context.Registration.ProviderType is ProviderTypes.Weibo or ProviderTypes.VkId or ProviderTypes.Yandex) + { + context.Request.AccessToken = context.Token; + context.Request.Token = null; + context.Request.TokenTypeHint = null; + } + + return default; + } + } + /// /// Contains the logic responsible for overriding the HTTP method for the providers that require it. /// @@ -61,7 +99,6 @@ public static partial class OpenIddictClientWebIntegrationHandlers request.Method = context.Registration.ProviderType switch { - ProviderTypes.Zendesk => HttpMethod.Delete, _ => request.Method diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs index ce73cae7..9ba1ce40 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs @@ -236,6 +236,12 @@ public static partial class OpenIddictClientWebIntegrationHandlers context.Request["f"] = "json"; } + // VK ID requires attaching the "client_id" parameter to userinfo requests. + else if (context.Registration.ProviderType is ProviderTypes.VkId) + { + context.Request.ClientId = context.Registration.ClientId; + } + return default; } } @@ -406,8 +412,8 @@ public static partial class OpenIddictClientWebIntegrationHandlers ProviderTypes.ExactOnline => new(context.Response["d"]?["results"]?[0]?.GetNamedParameters() ?? throw new InvalidOperationException(SR.FormatID0334("d/results/0"))), - // Fitbit, Todoist and Zendesk return a nested "user" object. - ProviderTypes.Fitbit or ProviderTypes.Todoist or ProviderTypes.Zendesk + // These providers return a nested "user" object. + ProviderTypes.Fitbit or ProviderTypes.Todoist or ProviderTypes.VkId or ProviderTypes.Zendesk => 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 f69549e2..16895ff7 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs @@ -391,6 +391,25 @@ public static partial class OpenIddictClientWebIntegrationHandlers } } + // VK ID uses a non-standard "device_id" parameter in authorization responses. + else if (context.Registration.ProviderType is ProviderTypes.VkId) + { + var identifier = (string?) context.Request["device_id"]; + if (string.IsNullOrEmpty(identifier)) + { + context.Reject( + error: Errors.InvalidRequest, + description: SR.FormatID2029("device_id"), + uri: SR.FormatID8000(SR.ID2029)); + + return default; + } + + // Store the device identifier as an authentication property + // so it can be resolved later to make refresh token requests. + context.Properties[VkId.Properties.DeviceId] = identifier; + } + // Zoho returns the region of the authenticated user as a non-standard "location" parameter // that must be used to compute the address of the token and userinfo endpoints. else if (context.Registration.ProviderType is ProviderTypes.Zoho) @@ -407,7 +426,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers } // Ensure the specified location corresponds to well-known region. - if (location.ToUpperInvariant() is not ( "AU" or "CA" or "EU" or "IN" or "JP" or "SA" or "US")) + if (location.ToUpperInvariant() is not ("AU" or "CA" or "EU" or "IN" or "JP" or "SA" or "US")) { context.Reject( error: Errors.InvalidRequest, @@ -640,6 +659,23 @@ public static partial class OpenIddictClientWebIntegrationHandlers context.TokenRequest.UserCode = code; } + // VK ID requires attaching a non-standard "device_id" parameter to all token requests. + // + // This parameter is either resolved from the authorization response (for the authorization + // code or hybrid grants) or manually provided by the application for other grant types. + else if (context.Registration.ProviderType is ProviderTypes.VkId) + { + context.TokenRequest["device_id"] = context.GrantType switch + { + GrantTypes.AuthorizationCode or GrantTypes.Implicit => context.Request["device_id"], + + _ when context.Properties.TryGetValue(VkId.Properties.DeviceId, out string? identifier) && + !string.IsNullOrEmpty(identifier) => identifier, + + _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0467)) + }; + } + return default; } } @@ -1363,6 +1399,9 @@ public static partial class OpenIddictClientWebIntegrationHandlers // Shopify returns the email address as a custom "associated_user/email" node in token responses: ProviderTypes.Shopify => (string?) context.TokenResponse?["associated_user"]?["email"], + // Yandex returns the email address as a custom "default_email" node: + ProviderTypes.Yandex => (string?) context.UserInfoResponse?["default_email"], + _ => context.MergedPrincipal.GetClaim(ClaimTypes.Email) }); @@ -1375,8 +1414,8 @@ public static partial class OpenIddictClientWebIntegrationHandlers ProviderTypes.Trakt or ProviderTypes.WordPress => (string?) context.UserInfoResponse?["username"], - // Basecamp and Harvest don't return a username so one is created using the "first_name" and "last_name" nodes: - ProviderTypes.Basecamp or ProviderTypes.Harvest + // These providers don't return a username so one is created using the "first_name" and "last_name" nodes: + ProviderTypes.Basecamp or ProviderTypes.Harvest or ProviderTypes.VkId when context.UserInfoResponse?.HasParameter("first_name") is true && context.UserInfoResponse?.HasParameter("last_name") is true => $"{(string?) context.UserInfoResponse?["first_name"]} {(string?) context.UserInfoResponse?["last_name"]}", @@ -1423,7 +1462,8 @@ public static partial class OpenIddictClientWebIntegrationHandlers => $"{(string?) context.UserInfoResponse?["firstName"]} {(string?) context.UserInfoResponse?["lastName"]}", // These providers return the username as a custom "display_name" node: - ProviderTypes.Spotify or ProviderTypes.StackExchange or ProviderTypes.Zoom + ProviderTypes.Spotify or ProviderTypes.StackExchange or + ProviderTypes.Yandex or ProviderTypes.Zoom => (string?) context.UserInfoResponse?["display_name"], // Strava returns the username as a custom "athlete/username" node in token responses: @@ -1451,7 +1491,8 @@ public static partial class OpenIddictClientWebIntegrationHandlers { // These providers return the user identifier as a custom "user_id" node: ProviderTypes.Amazon or ProviderTypes.HubSpot or - ProviderTypes.StackExchange or ProviderTypes.Typeform + ProviderTypes.StackExchange or ProviderTypes.Typeform or + ProviderTypes.VkId => (string?) context.UserInfoResponse?["user_id"], // ArcGIS and Trakt don't return a user identifier and require using the username as the identifier: @@ -1462,16 +1503,16 @@ public static partial class OpenIddictClientWebIntegrationHandlers ProviderTypes.Atlassian => (string?) context.UserInfoResponse?["account_id"], // These providers return the user identifier as a custom "id" node: - ProviderTypes.Airtable or ProviderTypes.Basecamp or ProviderTypes.Box or - ProviderTypes.Dailymotion or ProviderTypes.Deezer or ProviderTypes.Discord or - ProviderTypes.Disqus or ProviderTypes.Facebook or ProviderTypes.GitCode or - ProviderTypes.Gitee or ProviderTypes.GitHub or ProviderTypes.Harvest or - ProviderTypes.Kook or ProviderTypes.Kroger or ProviderTypes.Lichess or - ProviderTypes.Mastodon or ProviderTypes.Meetup or ProviderTypes.Nextcloud or - ProviderTypes.Patreon or ProviderTypes.Pipedrive or ProviderTypes.Reddit or - ProviderTypes.Smartsheet or ProviderTypes.Spotify or ProviderTypes.SubscribeStar or - ProviderTypes.Todoist or ProviderTypes.Twitter or ProviderTypes.Weibo or - ProviderTypes.Zoom + ProviderTypes.Airtable or ProviderTypes.Basecamp or ProviderTypes.Box or + ProviderTypes.Dailymotion or ProviderTypes.Deezer or ProviderTypes.Discord or + ProviderTypes.Disqus or ProviderTypes.Facebook or ProviderTypes.Gitee or + ProviderTypes.GitHub or ProviderTypes.Harvest or ProviderTypes.Kook or + ProviderTypes.Kroger or ProviderTypes.Lichess or ProviderTypes.Mastodon or + ProviderTypes.Meetup or ProviderTypes.Nextcloud or ProviderTypes.Patreon or + ProviderTypes.Pipedrive or ProviderTypes.Reddit or ProviderTypes.Smartsheet or + ProviderTypes.Spotify or ProviderTypes.SubscribeStar or ProviderTypes.Todoist or + ProviderTypes.Twitter or ProviderTypes.Weibo or ProviderTypes.Yandex or + ProviderTypes.Zoom => (string?) context.UserInfoResponse?["id"], // Bitbucket returns the user identifier as a custom "uuid" node: @@ -1920,6 +1961,27 @@ public static partial class OpenIddictClientWebIntegrationHandlers context.Request["language"] = settings.Language; } + // Yandex allows sending optional "device_id" and "device_name" parameters. + else if (context.Registration.ProviderType is ProviderTypes.Yandex) + { + var settings = context.Registration.GetYandexSettings(); + + if (!context.Properties.TryGetValue(Yandex.Properties.DeviceId, out string? identifier) || + string.IsNullOrEmpty(identifier)) + { + identifier = settings.DeviceId; + } + + if (!context.Properties.TryGetValue(Yandex.Properties.DeviceName, out string? name) || + string.IsNullOrEmpty(name)) + { + name = settings.DeviceName; + } + + context.Request["device_id"] = identifier; + context.Request["device_name"] = name; + } + // By default, Zoho doesn't return a refresh token but // allows sending an "access_type" parameter to retrieve one. else if (context.Registration.ProviderType is ProviderTypes.Zoho) @@ -2058,14 +2120,6 @@ public static partial class OpenIddictClientWebIntegrationHandlers context.RevocationRequest.ClientAssertionType = null; } - // Weibo implements a non-standard client authentication method for its endpoints that - // requires sending the token as "access_token" instead of the standard "token" parameter. - else if (context.Registration.ProviderType is ProviderTypes.Weibo) - { - context.RevocationRequest.AccessToken = context.RevocationRequest.Token; - context.RevocationRequest.Token = null; - } - return default; } } diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml index ecd4db6a..3f285fd6 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml @@ -2065,6 +2065,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +