From ccd2281b5584d640bace1912a88da2c3c701bcd7 Mon Sep 17 00:00:00 2001 From: DevTKSS Date: Mon, 24 Nov 2025 18:18:29 +0100 Subject: [PATCH 1/6] feat(etsy): Add Etsy Web Provider --- ...ctClientWebIntegrationHandlers.Userinfo.cs | 9 +++ .../OpenIddictClientWebIntegrationHandlers.cs | 7 ++- ...penIddictClientWebIntegrationProviders.xml | 57 +++++++++++++++++++ 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs index 6ab14581..7ef40cad 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs @@ -124,6 +124,15 @@ public static partial class OpenIddictClientWebIntegrationHandlers request.Headers.Add("X-API-Key", settings.ApplicationKey); } + // Etsy requires sending the client identifier 'client_id' gotten from Authorization Code exchange aka x-api-key in the Headers + // AND the AccessToken aka 'oAuth2' in Etsy Docs with the shops_r Scope in the Authorization Bearer Header. + else if (context.Registration.ProviderType is ProviderTypes.Etsy) + { + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", + request.Headers.Authorization?.Parameter); + request.Headers.Add("x-api-key", context.Registration.ClientId); + } + // Notion requires sending an explicit API version (which is statically set // to the last version known to be supported by the OpenIddict integration). else if (context.Registration.ProviderType is ProviderTypes.Notion) diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs index 1a0db913..066bc7eb 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs @@ -1520,9 +1520,10 @@ public static partial class OpenIddictClientWebIntegrationHandlers context.MergedPrincipal.SetClaim(ClaimTypes.NameIdentifier, issuer: issuer, value: context.Registration.ProviderType switch { // These providers return the user identifier as a custom "user_id" node: - ProviderTypes.Amazon or ProviderTypes.HubSpot or - ProviderTypes.StackExchange or ProviderTypes.Typeform or - ProviderTypes.VkId + ProviderTypes.Amazon or ProviderTypes.Etsy or + ProviderTypes.HubSpot or 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: diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml index 2a44dd9c..db056ac9 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml @@ -766,6 +766,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - @@ -782,20 +782,18 @@ - + + + - - - - @@ -819,6 +817,7 @@ + From 6ba191c5cc70da401183403679a979a2705a3c1c Mon Sep 17 00:00:00 2001 From: DevTKSS Date: Tue, 25 Nov 2025 18:27:51 +0100 Subject: [PATCH 3/6] chore(UserInfo): Use OverrideUserInfoEndpoint to extract user_id from AccessToken and call getUsers instead of getMe chore: Add Claims for name and Email --- .../OpenIddictClientWebIntegrationHandlers.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs index 066bc7eb..4044cd81 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs @@ -1051,6 +1051,13 @@ public static partial class OpenIddictClientWebIntegrationHandlers left : new Uri("https://api.dailymotion.com/user", UriKind.Absolute), right: new Uri(identifier, UriKind.Relative)), + // Etsy's userinfo endpoint requires sending the user identifier in the URI path, which can be gotten from one of the tokens returned by the Token endpoint. + ProviderTypes.Etsy when (string?) context.TokenResponse?["access_token"] is string accessToken + && accessToken.Split(['.'],3).First() is string userId // TODO: Check if string type is correct because Etsy Reference states for this as Path Parameter https://developers.etsy.com/documentation/reference#operation/getUser + => OpenIddictHelpers.CreateAbsoluteUri( + left : new Uri("https://openapi.etsy.com/v3/application/users/", UriKind.Absolute), + right: new Uri(userId, UriKind.Relative)), + // HubSpot doesn't have a static userinfo endpoint but allows retrieving basic information // by using an access token info endpoint that requires sending the token in the URI path. ProviderTypes.HubSpot when @@ -1407,6 +1414,9 @@ public static partial class OpenIddictClientWebIntegrationHandlers ?.Select(parameter => (string?) parameter["email"]) ?.FirstOrDefault(), + // Etsy returns the email address as a custom "primary_email" node: + ProviderTypes.Etsy => (string?) context.UserInfoResponse?["primary_email"], + // HubSpot returns the email address as a custom "user" node: ProviderTypes.HubSpot => (string?) context.UserInfoResponse?["user"], @@ -1442,7 +1452,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers => (string?) context.UserInfoResponse?["username"], // 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 + ProviderTypes.Basecamp or ProviderTypes.Etsy 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"]}", From 6e0d780aef98be154b2fc6290714f428b476441c Mon Sep 17 00:00:00 2001 From: DevTKSS Date: Tue, 25 Nov 2025 18:33:31 +0100 Subject: [PATCH 4/6] chore: Remove additional Authorization Bearer Header setting, as this is already added by default --- .../OpenIddictClientWebIntegrationHandlers.Userinfo.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs index efd8b771..3d6d21d6 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs @@ -124,12 +124,9 @@ public static partial class OpenIddictClientWebIntegrationHandlers request.Headers.Add("X-API-Key", settings.ApplicationKey); } - // Etsy requires sending the client identifier 'client_id' gotten from Authorization Code exchange aka x-api-key in the Headers - // AND the AccessToken aka 'oAuth2' in Etsy Docs with the shops_r Scope in the Authorization Bearer Header. + // Etsy requires sending the client identifier 'client_id' gotten from Authorization Code exchange aka x-api-key in the Headers (AttachAccessTokenParameter sets Authorization Bearer header already by default). else if (context.Registration.ProviderType is ProviderTypes.Etsy) { - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", - request.Headers.Authorization?.Parameter); request.Headers.Add("x-api-key", context.Registration.ClientId); } From 75cfe76fb7ea6c2e83eacfc47bb326200fb0fd62 Mon Sep 17 00:00:00 2001 From: DevTKSS Date: Tue, 25 Nov 2025 18:37:08 +0100 Subject: [PATCH 5/6] chore: Update ToDo Comments and Provider.xml along switch to getUser Endpoint for UserInfo, remove UserInfoEndpoint from settings as its now overridden chore: Adjust Scopes Required state in Provider.xml --- ...ctClientWebIntegrationHandlers.Userinfo.cs | 8 ++++--- ...penIddictClientWebIntegrationProviders.xml | 23 ++++++++++--------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs index 3d6d21d6..ccf86938 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs @@ -545,16 +545,18 @@ public static partial class OpenIddictClientWebIntegrationHandlers // Note: Etsy doesn't returns a standard "name" containing first + last Name claim formatted in the JSON object. else if (context.Registration.ProviderType is ProviderTypes.Etsy) { + // TODO: Check if https://github.com/DevTKSS/openiddict-core/blob/ccd2281b5584d640bace1912a88da2c3c701bcd7/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs?plain=1#L1455-L1459 makes this obsolete string? firstName = (string?) context.Response["first_name"]; string? lastName = (string?) context.Response["last_name"]; - // user_id gets returned as integer but OpenIddict expects a string - // TODO: Check if this is needed as for calling the getUser Endpoint we already require user_id as parameter + // user_id gets returned as integer but OpenIddict might expect it a string? + // TODO: Check if this is obsolete from https://github.com/DevTKSS/openiddict-core/blob/ccd2281b5584d640bace1912a88da2c3c701bcd7/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs?plain=1#L1532-L1538 context.Response[Claims.Subject] = context.Response["user_id"]; // Claims are not giving a user_id by default and client_Id is alredy used for x-api-key context.Response[Claims.Name] = $"{firstName} {lastName}"; context.Response[Claims.FamilyName] = lastName; context.Response[Claims.GivenName] = firstName; - // Mapping Email and Picture claims + // TODO: Check if this is still needed or obsolete from https://github.com/DevTKSS/openiddict-core/blob/ccd2281b5584d640bace1912a88da2c3c701bcd7/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs?plain=1#L1417-L1419 context.Response[Claims.Email] = context.Response["primary_email"]; + // Picture claims context.Response[Claims.Picture] = context.Response["image_url_75x75"]; } return ValueTask.CompletedTask; diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml index 056d2a65..cc4619b1 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml @@ -777,10 +777,12 @@ + TokenEndpoint="https://openapi.etsy.com/v3/public/oauth/token"> + @@ -790,10 +792,13 @@ - - - - + + + + @@ -814,10 +819,6 @@ - - - - From 3a647d2243223ca240ab3c782d0cebdc0f1fd0be Mon Sep 17 00:00:00 2001 From: DevTKSS Date: Wed, 26 Nov 2025 14:58:16 +0100 Subject: [PATCH 6/6] chore: Remove Scopes and explicit Claim Mappings --- ...ctClientWebIntegrationHandlers.Userinfo.cs | 17 ----------------- ...penIddictClientWebIntegrationProviders.xml | 19 ------------------- 2 files changed, 36 deletions(-) diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs index ccf86938..a0abc6b4 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs @@ -542,23 +542,6 @@ public static partial class OpenIddictClientWebIntegrationHandlers } } - // Note: Etsy doesn't returns a standard "name" containing first + last Name claim formatted in the JSON object. - else if (context.Registration.ProviderType is ProviderTypes.Etsy) - { - // TODO: Check if https://github.com/DevTKSS/openiddict-core/blob/ccd2281b5584d640bace1912a88da2c3c701bcd7/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs?plain=1#L1455-L1459 makes this obsolete - string? firstName = (string?) context.Response["first_name"]; - string? lastName = (string?) context.Response["last_name"]; - // user_id gets returned as integer but OpenIddict might expect it a string? - // TODO: Check if this is obsolete from https://github.com/DevTKSS/openiddict-core/blob/ccd2281b5584d640bace1912a88da2c3c701bcd7/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs?plain=1#L1532-L1538 - context.Response[Claims.Subject] = context.Response["user_id"]; // Claims are not giving a user_id by default and client_Id is alredy used for x-api-key - context.Response[Claims.Name] = $"{firstName} {lastName}"; - context.Response[Claims.FamilyName] = lastName; - context.Response[Claims.GivenName] = firstName; - // TODO: Check if this is still needed or obsolete from https://github.com/DevTKSS/openiddict-core/blob/ccd2281b5584d640bace1912a88da2c3c701bcd7/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs?plain=1#L1417-L1419 - context.Response[Claims.Email] = context.Response["primary_email"]; - // Picture claims - context.Response[Claims.Picture] = context.Response["image_url_75x75"]; - } return ValueTask.CompletedTask; } } diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml index cc4619b1..cc6d6cc0 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml @@ -799,25 +799,6 @@ - - - - - - - - - - - - - - - - - - -