openiddict-core icon indicating copy to clipboard operation
openiddict-core copied to clipboard

feat(etsy): Add Etsy Web Provider

Open DevTKSS opened this issue 1 month ago β€’ 8 comments

This PR should be adding Etsy as OAuth2 Web Provider

PR Requires help

Known Problems / open tasks / decisions

  • [ ] Evaluate changing the setting for the UserInfoEndpoint in Etsy Provider:
    • [ ] from getMe, which would be commonly the url we are used to call for this task /users/me but does not return regular User Information we might be expecting like a Name or email

      • [x] map shop_id Parameter returned by this endpoint -> if we use this Endpoint we only return the user_id and the shop_id will be automatically returned somewhere/somehow 🀷 switched to getUser, so if this doesn't get decided to get reverted, we dont care about shop_id
    • [ ] to getUser Endpoint which returns those parameters but doesn't need additional Mapping in e.g. MapNonStandardResponseParameters()

    • required additional scope email_r which is not guaranteed to be granted by the consumer! Referring to the Endpoint description

      Retrieves a user profile based on a unique user ID. Access is limited to profiles of the authenticated user or linked buyers. For the primary_email field, specific app-based permissions are required and granted case-by-case.

    • json response parameters (Claims to be added?):

      • user_id
      • primary_email
      • first_name
      • last_name
      • image_url_75x75
    • Path Parameters:

      • user_id (Type:<int64>) - Please check out the TODO's in UserInfo and Handler Claims added for this. Not sure if this is now redundant and we can remove most of them from UserInfo Partial Class πŸ€”
    • Uri: https://openapi.etsy.com/v3/application/users/{user_id} - Removed from xml file, now using OverrideUserInfoEndpoint

    • Maybe we need to add Etsy Provider to the OverrideUserInfoRetrieval List here too, please check for this πŸ‘

[!TIP] The user_id parameter can also be extracted from the access_token or refresh_token parameter after Authentication Code Exchange, not only by calling the getMe Endpoint, which provides us the unique shop_id parameter used for other endpoints Etsy Docs provided Sample response after code exchange:

{
    "access_token": "12345678.O1zLuwveeKjpIqCQFfmR-PaMMpBmagH6DljRAkK9qt05OtRKiANJOyZlMx3WQ_o2FdComQGuoiAWy3dxyGI4Ke_76PR",
    "token_type": "Bearer",
    "expires_in": 3600,
    "refresh_token": "12345678.JNGIJtvLmwfDMhlYoOJl8aLR1BWottyHC6yhNcET-eC7RogSR5e1GTIXGrgrelWZalvh3YvvyLfKYYqvymd-u37Sjtx"
}
  • [ ] Scopes Are not be generated in ScopesSupported Collection in the Configuration, not even the default ones (?) image

    image image

Other Points to check

Additional Information for Reviewer

  • [x] Setting up Header + Authorization Header appropriately

  • [ ] Etsy does not make use of the Shared Secret aka Client Secret, but the Registration provides one, the client is treatened as public client + requires interactive, no implicit or non-interactive flow is supported!

    • Generated Configuration states that we support this for Device, Introspection, Revocation Endpoint Auth Methods, which are NOT supported at all from this API πŸ€” image
  • [x] Do the both App Registration kinds in Etsy require different Envirionments? both are using same uri's

    • Personal Access
    • Commercial Access
  • [ ] Etsy Auth does not support a Logout as such, but the generator seems to accepts a Logout Uri? image

@kevinchalet could you please help me set this up correctly?

DevTKSS avatar Nov 24 '25 18:11 DevTKSS

Hey,

Thanks for your PR!

Add the getUser Endpoint Call, to get complete expected UserInfo. This is no contained getMe response parameter then instead requires a 2nd call!

Only a single userinfo call made by OpenIddict is supported: any additional API call is the responsibility of the developers, who can implement that in their authorization controller (e.g to persist any additional information in the authentication cookie before it is returned).

How is the Provider Id meant to be generated? - gets linted, and GUID generator in VS2026 does not satisfy the requirements

It must be generated randomly. Make sure it respects the same exact format as other providers (no braces, all lowercase, etc.).

CodeChallengeMethod is stated to be required in Configuration tag, but gets linted.

When you have multiple nodes under Configuration, make sure you list them in the alphabetical order. The schema warning should go away.

It did not get completly clear, how to add the shop_id Claim into the getMe (/users/me) User Info Endpoint Claim Context, so I was only able to add the Provider for the user_id registration here: src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs

All the claims returned by the provider are returned by OpenIddict so you don't need to do anything special for that shop_id thing. Only the user identifier/name/email claims get a special treatment and are mapped to their WS-Federation equivalent (BCL ClaimTypes class) to make working with multiple providers (who all use their own non-standard claims) easier.

Secondary, the Header + Authorization Header Setup is not documented, please check if this is correct this way: src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs (see diff) or if one of them is needed to be moved in another method.

We can't realistically list all the workarounds possible in the documentation (otherwise it would become unreadable 🀣). Luckily, most providers don't need to change that.

What you've done looks good πŸ‘πŸ»

! Etsy does not make use of the Shared Secret aka Client Secret, but the Registration provides one, the client is treatened as public client + requires interactive, no implicit or non-interactive flow is supported!

That's fine: if the service doesn't currently support client authentication, the user will not have to call SetClientSecret() and no client secret will be sent. Should they add client support authentication support in the future, no change will be required on our side as all APIs will already be available to support that.

Does that require Environments to be defined for this? There is no Development Environment API in etsy.

If the URLs are exactly the same, then no, it's not necessary/useful. A single Production environment is always created by OpenIddict.

Hope I answered all your questions πŸ˜ƒ

kevinchalet avatar Nov 25 '25 09:11 kevinchalet

@kevinchalet I updated the OP above with Task bullet points, so you can see where are still open/unclear points πŸ‘

When you have multiple nodes under Configuration, make sure you list them in the alphabetical order. The schema warning should go away.

thanks, worked πŸ‘

possible a nice small information in docs would be good to add for future contributors? for example the current note could be considerable to be updated like this:

[!NOTE] When the provider is known to support Proof Key for Code Exchange (PKCE), a <CodeChallengeMethod> node MUST be added under <Configuration> to ensure the OpenIddict client will send appropriate code_challenge/code_challenge_method parameters: ... Code Sample left out for brevity ... In case you might need to add other tags like GrantType's mentioned before, make sure to sort the nested Properties in ascending alphabetical order.

  • Changed the endpoint used for User Info call to getUser instead of getMe and extracting the required user_id from the access_token parameter, so we would be able to return actual expected User Info (updated/added on the OP List)

Referring to your response:

Only a single userinfo call made by OpenIddict is supported: any additional API call is the responsibility of the developers, who can implement that in their authorization controller (e.g to persist any additional information in the authentication cookie before it is returned).

DevTKSS avatar Nov 25 '25 18:11 DevTKSS

trying out in e.g. the OpenIdDict.Sandbox.Console.Client I would see the Scopes not beeing generated while we do list them in the xml file as something that should be fixed if possible. I am not really sure for what they chould otherwhile be meant when not giving us the option to get them generated as const string or similar while registration of the Provider in the end πŸ€” maybe you can tell?

@kevinchalet Seems like something gets messed up with the Request πŸ€” I did add the url defined in the AddRegistration( for https://localhost:44395/ in the valid callback url in my app registration at Etsy API, but while I am send successfully to the browser and can loop through social login, the redirect meant to be executed afterwards is failing, because from what I am seeing the redirect_url is loosing the 2nd 4 at the start πŸ€” (replaced client_id value with placeholder, I was able verify that this was correctly provided πŸ‘ )

OpenIdDict.Sandbox.Console.Client
https://www.etsy.com/oauth/connect?client_id={correct-client-id}&redirect_uri=http%3A%2F%2Flocalhost%3A49152%2Fcallback%2Fetsy&response_type=code&scope=shops_r+email_r&code_challenge=R2Dn8pQSqYf_MfhgLG9pETvEvMTWzZuyIJ6qBBBwjw0&code_challenge_method=S256&state=8lUjo7nznfqFqpC_49AmhMtAQPl3TQqQZtAj8ezrh_U
image

What doesn't fit:

  • redirect_url should be something like this: https://localhost:44395/ + callback/etsy

Etsy Auth guide Reference for this: https://developers.etsy.com/documentation/essentials/authentication#step-1-request-an-authorization-code

OpenIddict.Sandbox.AspNetCore.Client is failing too
https://www.etsy.com/oauth/connect?client_id={valid-client-id}&redirect_uri=https%3A%2F%2Flocalhost%3A44381%2Fcallback%2Fetsy&response_type=code&scope=shops_r%20email_r&code_challenge=NW7DOvyu4RVlls-C5PcJrToyzh5TG8GJJ8JswTSMMKY&code_challenge_method=S256&state=mxKc2tx6dlWcgvu9rm4Vp5hnY8AETJhryOwXqeLQbXQ
this would be a valid approach using 5001 port from the login page at etsy auth
https://www.etsy.com/oauth2/signin?from_page=%2Foauth%2Fconnect%3Fclient_id%3D{valid-client-id}%26scope%3Dshops_r%2520email_r%26response_type%3Dcode%26redirect_uri%3Dhttps%253A%252F%252Flocalhost%253A5001%252Fetsy%252Fcallback%26code_challenge%3DLVTddG56qoZT26KTP64y5r_hPuvwjmrEi-N7KHndslk%26code_challenge_method%3DS256%26state%3DCfDJ8Igeu-mhL-BPi5Zj_sG7stEdBFQEo3jyXbSAgBj4332XrgHlIftisfTb3XX49tGjjCpyxTrYGqrPlCNplqGEyKpeK27qLXvnPU0v2a1nKnSnYJ9fTg_zedqaWBB-RgrXf3aQ3hCOw7c2ttORzBeo3jL65UhabE4GyEEI9AYd2g6Hnhv5SiJAQ9hBiUNX37sMLlD9HD6Pn8QzTLPknZmukRy2ve0F3ZxEisbkYdHwID797cmD22veibPtGQikmBglZW572vCTCuzzJcBwBCvdOftxVEK3BygsiiogJHYA4Tax&lp=1&show_social_sign_in=1&is_from_etsyapp=0&initial_state=sign-in&client_id={valid-client-id}

as they are internally redirecting, the important thing to notice is that the port is completely provided after the %253A which seems to stand for the encoded : maybe. the CallbackPath for that sample has been /etsy/callback so switched positions.

DevTKSS avatar Nov 25 '25 18:11 DevTKSS

trying out in e.g. the OpenIdDict.Sandbox.Console.Client I would see the Scopes not beeing generated while we do list them in the xml file as something that should be fixed if possible. I am not really sure for what they chould otherwhile be meant when not giving us the option to get them generated as const string or similar while registration of the Provider in the end πŸ€” maybe you can tell?

We don't generate constants for scopes (some services have an insane amount of scopes so it would be a lot of work to maintain that in the long term): the scopes nodes in the XML file are exclusively used for providers that either require at least one scope to be set (Default="true") or need a specific scope to access the userinfo (Required="true").

What doesn't fit:

* `redirect_url` should be something like this: `https://localhost:44395/` + `callback/etsy`

The console app is a public client meant to be used on a desktop machine: as such, it requires using either HTTP or a custom URI scheme. If the service absolutely requires using HTTPS, use the web client app instead and use https://localhost:44381/callback/etsy as the redirect URI.

kevinchalet avatar Nov 26 '25 10:11 kevinchalet

@kevinchalet πŸ€” I am a bit wondering now. I tryed using OpenIddict.Sandbox.AspNetCore.Client (=> your meant Web Client ?) and in the Startup.cs it uses

image

which does match the url I registred in the API I am calling. so I was wondering, why this also doesnt worked (as mentioned in my last comment) and I think I might found the issue exactly as you told, in the launchSettings.json is instead this url set: image

but, correct me if I am wrong, shouldn't this be the same url in both places? Or does this somehow redirect this internally?

by the way, refering to your response:

The console app is a public client meant to be used on a desktop machine: as such, it requires using either HTTP or a custom URI scheme.

okay, for localhost I could change this in the registration, but generally, if https scheme is not supported in a console Client, then why is this set up like this? If it is known to not work like this? I am searching for the reason that I seem to miss πŸ˜… πŸ€”

image

so seems like this is coming from the .SetAllowedEmbeddedWebServerPorts(49152) ! but for what exactly do we then use the Issuer Url?

And I think the response does not contain all expected contents πŸ€” I am getting just one token value but should get two: access_token and refresh_token

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Claim type                      β”‚ Claim value type                β”‚ Claim value                    β”‚ Claim issuer    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ as                              β”‚ http://www.w3.org/2001/XMLSchem β”‚ https://www.etsy.com/          β”‚ LOCAL AUTHORITY β”‚
β”‚                                 β”‚ a#string                        β”‚                                β”‚                 β”‚
β”‚ oi_reg_id                       β”‚ http://www.w3.org/2001/XMLSchem β”‚ <realistic-token-value> β”‚ LOCAL AUTHORITY β”‚
β”‚                                 β”‚ a#string                        β”‚ aqqD8YNVg4Dws                  β”‚                 β”‚
β”‚ oi_prvd_name                    β”‚ http://www.w3.org/2001/XMLSchem β”‚ Etsy                           β”‚ LOCAL AUTHORITY β”‚
β”‚                                 β”‚ a#string                        β”‚                                β”‚                 β”‚
β”‚ http://schemas.microsoft.com/ac β”‚ http://www.w3.org/2001/XMLSchem β”‚ Etsy                           β”‚ LOCAL AUTHORITY β”‚
β”‚ cesscontrolservice/2010/07/clai β”‚ a#string                        β”‚                                β”‚                 β”‚
β”‚ ms/identityprovider             β”‚                                 β”‚                                β”‚                 β”‚

but expected would be anything like this as response from UserInfo getUser:

{
  "user_id": 1,
  "primary_email": "[email protected]",
  "first_name": "string",
  "last_name": "string",
  "image_url_75x75": "string"
}

or as mentioned 2 Tokens if the output should show this.

Could you please check whats wrong?


Tryed as 2nd with the OpenIddict.Sandbox.AspNetCore.Client and while the authentication from what I am seeing should be fine in the response, its throwing an Exception for the Transportstream had been closed?

dbug: HttpsConnectionAdapter[1]
      Failed to authenticate HTTPS connection.
      System.IO.IOException: Fehler bei Authentifizierung, da die Gegenseite den Transportstream geschlossen hat.
         bei System.Net.Security.SslState.InternalEndProcessAuthentication(LazyAsyncResult lazyResult)
         bei System.Net.Security.SslState.EndProcessAuthentication(IAsyncResult result)
         bei System.Threading.Tasks.TaskFactory`1.FromAsyncCoreLogic(IAsyncResult iar, Func`2 endFunction, Action`1 endAction, Task`1 promise, Boolean requiresSynchronization)
      --- Ende der StapelΓΌberwachung vom vorhergehenden Ort, an dem die Ausnahme ausgelΓΆst wurde ---
         bei System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
         bei System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
         bei Microsoft.AspNetCore.Server.Kestrel.Https.Internal.HttpsConnectionAdapter.<InnerOnConnectionAsync>d__10.MoveNext()

the response we are ending up here with is the code + state parameter to our https://localhost:44381/callback/etsy url as configured in launchSettings.json so it should now have the correct things for the exchange...

but while the application did only ended up with the "Something went really wrong" error page, reading the console output, the auth flow was correctly looped through after this. just that the UI is getting stuck on the Error Page. seems like there is some Response redirecting to this wrong?

image

openIdDictLog-Error-Page.txt

DevTKSS avatar Nov 26 '25 20:11 DevTKSS

@kevinchalet I might found the reason for this problem with the strange behaviour. Would you mind take a look, if this needs adjustments?

Up from .NET10.0 they indroduced Breaking changes for Cookie Auth redirect, which do now return with 401/403 instead of beeing redirected to SignIn Page:

  • https://learn.microsoft.com/en-us/aspnet/core/security/authentication/cookie?view=aspnetcore-10.0
  • https://learn.microsoft.com/en-us/aspnet/core/security/authentication/api-endpoint-auth?view=aspnetcore-10.0

Could this be the reason for the wrong Asp Net Client page I am getting?

DevTKSS avatar Dec 02 '25 13:12 DevTKSS

@kevinchalet πŸ€” I am a bit wondering now. I tryed using OpenIddict.Sandbox.AspNetCore.Client (=> your meant Web Client ?) and in the Startup.cs it uses image

which does match the url I registred in the API I am calling. so I was wondering, why this also doesnt worked (as mentioned in my last comment) and I think I might found the issue exactly as you told, in the launchSettings.json is instead this url set: image

but, correct me if I am wrong, shouldn't this be the same url in both places? Or does this somehow redirect this internally?

The manual client registration - configured in both the console and web clients - is exclusively meant to be used with the local OIDC server contained in the OpenIddict.Sandbox.AspNetCore.Server project: it's not something you're using when testing one of the two clients against a provider like Etsy, which is a completely separate thing.

okay, for localhost I could change this in the registration, but generally, if https scheme is not supported in a console Client, then why is this set up like this? If it is known to not work like this? I am searching for the reason that I seem to miss πŸ˜… πŸ€”

The issuer - which is basically the identity URI of the remote OAuth 2.0/OIDC server - can be an HTTPS URI even on localhost (obviously, for development scenarios only πŸ˜„).

In practice, the redirect_uri - which is a different thing - cannot be an HTTPS URI pointing to localhost because no certificate authority will ever give you a TLS certificate valid for localhost, so if you use HTTPS for your "callback handler server", your users would always get a security warning for no security improvement at all, since localhost traffic (aka loopback) isn't supposed to leave your machine anyway πŸ˜„

I am getting just one token value but should get two: access_token and refresh_token

Note: tokens are not claims and not seeing them in the console app is expected (some tokens can be quite large and it would be completely unreadable).

Could you please check whats wrong?

Are you sure the userinfo request is sent? The response should be logged in the .NET logs.

the response we are ending up here with is the code + state parameter to our https://localhost:44381/callback/etsy url as configured in launchSettings.json so it should now have the correct things for the exchange...

Note that the callback URL is supposed to be callback/login/etsy, not callback/etsy as AuthenticationController.LogInCallback only answers to requests sent to ~/callback/login/{provider}:

// Note: this controller uses the same callback action for all providers
// but for users who prefer using a different action per provider,
// the following action can be split into separate actions.
[HttpGet("~/callback/login/{provider}"), HttpPost("~/callback/login/{provider}"), IgnoreAntiforgeryToken]
public async Task<ActionResult> LogInCallback()
{
    // ...
}

Since you're using the wrong address, ASP.NET Core is likely returning a 404 response, which is caught by the status code pages middleware, hence the generic error you're getting.

kevinchalet avatar Dec 08 '25 19:12 kevinchalet

@kevinchalet

The manual client registration - configured in both the console and web clients - is exclusively meant to be used with the local OIDC server contained in the OpenIddict.Sandbox.AspNetCore.Server project: it's not something you're using when testing one of the two clients against a provider like Etsy, which is a completely separate thing.

oh, okay, seems like I misunderstood this from the (assuming you refer to those?) comments in the Client Projects: // Add a client registration matching the client application definition in the server project. πŸ€” Can you tell if I got it right now? So the sandbox projects are specifically meant to test the different core Project client options mentioned in the Docs against a demo Server only?

The issuer - which is basically the identity URI of the remote OAuth 2.0/OIDC server - can be an HTTPS URI even on localhost (obviously, for development scenarios only πŸ˜„).

In practice, the redirect_uri - which is a different thing - cannot be an HTTPS URI pointing to localhost because no certificate authority will ever give you a TLS certificate valid for localhost, so if you use HTTPS for your "callback handler server", your users would always get a security warning for no security improvement at all, since localhost traffic (aka loopback) isn't supposed to leave your machine anyway πŸ˜„

Yes, that redirect to localhost should only be done via development environment makes defintly sense to me πŸ‘ I am thinking about using Azure App Services for example, to host my future ASP.NET Core project on, but I am not sure, except from using the not very secure Client Credentials Flow between my Client and the Server, how to manage this in that case if not using a localhost callback from my Server back to the client πŸ€” I researched the MS Docs for this, but without requiring the user to have a Microsoft Account or beeing in a Company tenant itself, I was unable to find a way to do this secure. I am still hoping to find anyone who knows about this.

Note: tokens are not claims and not seeing them in the console app is expected (some tokens can be quite large and it would be completely unreadable).

especially if we using JWT tokens, their representative un-hashed version was very large in the samples I seen so far. Storing the real tokens would be one job of my Server API and that should (just like with the Client applications here) then send JWT back to my client app πŸ‘

Are you sure the userinfo request is sent? The response should be logged in the .NET logs.

Where can I find them, to check? I did not see them until now, but possibly just missed the right location. Usually %TEMP% or %APPDATA% ? Would have a look then, if I know where πŸ‘

Note that the callback URL is supposed to be callback/login/etsy, not callback/etsy as AuthenticationController.LogInCallback only answers to requests sent to ~/callback/login/{provider}:

I think this is "just" a result of missassumtion number one 😁 I readed from this client integration repo-fork of the openIdDict repo, but not sure if he was successfull and if yes, why he didnt send a PR to add it... possibly because Uno Mobile Target requires that damn Xamarin version he told me I need to override to use it with the SystemIntegration of this repo πŸ€” but I think I will try his integration out as soon as it succeeds on a other client πŸ‘

  • https://github.com/xperiandri/openiddict-core/tree/uno_sandbox
  • https://github.com/xperiandri/openiddict-core/tree/uno_token_store
  • https://github.com/xperiandri/openiddict-core/tree/uno
  • https://github.com/xperiandri/openiddict-core/tree/uno_WinAppSDK

DevTKSS avatar Dec 09 '25 17:12 DevTKSS