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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+