Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Yandex and VK ID web providers #2244

Merged
merged 24 commits into from
Feb 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
fef62b1
1. Add Yandex and VkId driver to WebProviders list
Jan 16, 2025
3bd47c8
1. Attach device_id for VkId web provider throw authorization proccess
Jan 16, 2025
4295844
1. Add comments for added code
Jan 20, 2025
5717822
1. Change documentation language path to english
Jan 20, 2025
01d8295
Update src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegr…
t1moH1ch Jan 21, 2025
5de9da4
Update src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegr…
t1moH1ch Jan 21, 2025
cdd3b49
Update src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegr…
t1moH1ch Jan 21, 2025
17e8ac9
Update src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegr…
t1moH1ch Jan 21, 2025
924a61b
Update src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegr…
t1moH1ch Jan 21, 2025
9ccc2f1
Add VkId web provider to non-standard unwrap userinfo method
Feb 4, 2025
e34fb14
1. Add test data for Yandex and VK ID web providers to the sanbox con…
Feb 5, 2025
d37dc0a
Merge branch 'dev' into dev
t1moH1ch Feb 5, 2025
0dd3c37
add context request null check
Feb 10, 2025
bb0b75c
changed grant types location for yandex web provider
Feb 10, 2025
f937100
Update the VK ID provider to support attaching the device identifier …
kevinchalet Feb 10, 2025
da82d11
Tweak the device identifier attachment logic
kevinchalet Feb 10, 2025
04e66fe
Add an event handler responsible for mapping non-standard revocation …
kevinchalet Feb 10, 2025
dee55da
Fix the invalid provider identifier
kevinchalet Feb 10, 2025
ac02954
Flow the device identifier to Yandex's revocation endpoint
kevinchalet Feb 10, 2025
d9732c8
Update the Yandex provider to allow specifying a device_id/device_name
kevinchalet Feb 10, 2025
78925d4
add device id and device name to yandex registration
Feb 10, 2025
beb9538
add revocation support for VK ID web provider
Feb 10, 2025
f407ff1
Revert "add device id and device name to yandex registration"
Feb 10, 2025
b246954
Remove the VK ID/Yandex credentials
kevinchalet Feb 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/OpenIddict.Abstractions/OpenIddictResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1707,6 +1707,9 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId
<data name="ID0459" xml:space="preserve">
<value>A token must be specified when using revocation.</value>
</data>
<data name="ID0467" xml:space="preserve">
<value>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.</value>
</data>
<data name="ID2000" xml:space="preserve">
<value>The security token is missing.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public static class Revocation
/*
* Revocation request preparation:
*/
MapNonStandardRequestParameters.Descriptor,
OverrideHttpMethod.Descriptor,
AttachBearerAccessToken.Descriptor,

Expand All @@ -30,6 +31,43 @@ public static class Revocation
NormalizeContentType.Descriptor
]);

/// <summary>
/// Contains the logic responsible for mapping non-standard request parameters
/// to their standard equivalent for the providers that require it.
/// </summary>
public sealed class MapNonStandardRequestParameters : IOpenIddictClientHandler<PrepareRevocationRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<PrepareRevocationRequestContext>()
.UseSingletonHandler<MapNonStandardRequestParameters>()
.SetOrder(int.MinValue + 100_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();

/// <inheritdoc/>
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;
}
}

/// <summary>
/// Contains the logic responsible for overriding the HTTP method for the providers that require it.
/// </summary>
Expand Down Expand Up @@ -61,7 +99,6 @@ public ValueTask HandleAsync(PrepareRevocationRequestContext context)

request.Method = context.Registration.ProviderType switch
{

ProviderTypes.Zendesk => HttpMethod.Delete,

_ => request.Method
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,12 @@ public ValueTask HandleAsync(PrepareUserInfoRequestContext context)
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;
}
}
Expand Down Expand Up @@ -406,8 +412,8 @@ public ValueTask HandleAsync(ExtractUserInfoResponseContext context)
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"))),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,25 @@ public ValueTask HandleAsync(ProcessAuthenticationContext context)
}
}

// 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)
Expand All @@ -407,7 +426,7 @@ public ValueTask HandleAsync(ProcessAuthenticationContext context)
}

// 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,
Expand Down Expand Up @@ -640,6 +659,23 @@ public ValueTask HandleAsync(ProcessAuthenticationContext context)
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;
}
}
Expand Down Expand Up @@ -1363,6 +1399,9 @@ public ValueTask HandleAsync(ProcessAuthenticationContext context)
// 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)
});

Expand All @@ -1375,8 +1414,8 @@ ProviderTypes.Lichess or ProviderTypes.Mastodon or ProviderTypes.Mixclou
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"]}",
Expand Down Expand Up @@ -1423,7 +1462,8 @@ when context.TokenResponse?["associated_user"]?["first_name"] is not null &&
=> $"{(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:
Expand Down Expand Up @@ -1451,7 +1491,8 @@ ProviderTypes.Spotify or ProviderTypes.StackExchange or ProviderTypes.Zoom
{
// 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:
Expand All @@ -1462,16 +1503,16 @@ ProviderTypes.ArcGisOnline or ProviderTypes.Trakt
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:
Expand Down Expand Up @@ -1920,6 +1961,27 @@ public ValueTask HandleAsync(ProcessChallengeContext context)
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)
Expand Down Expand Up @@ -2058,14 +2120,6 @@ public ValueTask HandleAsync(ProcessRevocationContext context)
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;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2065,6 +2065,31 @@
</Environment>
</Provider>

<!--
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
██ ███ █ █▀▄████▄ ▄██ ▄▄▀██
███ █ ██ ▄▀██████ ███ ██ ██
███▄▀▄██ ██ ████▀ ▀██ ▀▀ ██
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->

<Provider Name="VkId" Id="9bf89c19-401b-4076-893e-a4136e719432"
Documentation="https://id.vk.com/about/business/go/docs/en/vkid/latest/oauth-vk">
<Environment Issuer="https://id.vk.com/">
<Configuration AuthorizationEndpoint="https://id.vk.com/authorize"
RevocationEndpoint="https://id.vk.com/oauth2/revoke"
TokenEndpoint="https://id.vk.com/oauth2/auth"
UserInfoEndpoint="https://id.vk.com/oauth2/user_info">
<CodeChallengeMethod Value="S256" />

<GrantType Value="authorization_code" />
<GrantType Value="refresh_token" />
</Configuration>
</Environment>

<Property Name="DeviceId" DictionaryKey=".device_id" />
</Provider>

<!--
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
██ ███ ██ ▄▄▄██ ▄▄▀██ ▄▄▄█▄▀█▀▄██
Expand Down Expand Up @@ -2193,6 +2218,35 @@
<Provider Name="Yahoo" Id="874d78ec-3d79-4492-ab79-76a7dd7fa0b5"
Documentation="https://developer.yahoo.com/oauth2/guide/openid_connect/">
<Environment Issuer="https://api.login.yahoo.com/" />
</Provider>

<!--
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
██ ███ █ ▄▄▀██ ▀██ ██ ▄▄▀██ ▄▄▄██▄▀█▀▄██
██▄▀▀▀▄█ ▀▀ ██ █ █ ██ ██ ██ ▄▄▄████ ████
████ ███ ██ ██ ██▄ ██ ▀▀ ██ ▀▀▀██▀▄█▄▀██
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->

<Provider Name="Yandex" Id="313298d4-d210-4541-a348-96ced013dab1" Documentation="https://yandex.ru/dev/id/doc/en/">
<Environment Issuer="https://oauth.yandex.ru/">
<Configuration AuthorizationEndpoint="https://oauth.yandex.ru/authorize"
RevocationEndpoint="https://oauth.yandex.ru/revoke_token"
TokenEndpoint="https://oauth.yandex.ru/token"
UserInfoEndpoint="https://login.yandex.ru/info">
<GrantType Value="authorization_code" />
<GrantType Value="refresh_token" />
</Configuration>
</Environment>

<Property Name="DeviceId" DictionaryKey=".device_id" />
<Property Name="DeviceName" DictionaryKey=".device_name" />

<Setting PropertyName="DeviceId" ParameterName="identifier" Type="String" Required="false"
Description="Gets or sets the optional device identifier that will be attached to authorization requests" />

<Setting PropertyName="DeviceName" ParameterName="name" Type="String" Required="false"
Description="Gets or sets the optional device name that will be attached to authorization requests" />
</Provider>

<!--
Expand Down