-
Notifications
You must be signed in to change notification settings - Fork 551
Add etsy oauth provider #1126
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
base: dev
Are you sure you want to change the base?
Add etsy oauth provider #1126
Conversation
- Added `EtsyAuthenticationDefaults` default value fields like `EtsyBaseUri` and `UserDetailsPath` - Added xml docs onto all `EtsyAuthenticationDefaults` fields - Added `EtsyAuthenticationOptions.IncludeDetailedUserInfo` Property - removed `EtsyPostConfigureOptions` as Etsy does not have dynamic endpoints or domain like GitHub - integrated validation logik into `EtsyAuthenticationOptions.Validate` override, like used for issue 610 fix
…d IncludeDetailedUserInfo Handler
…aming for the app Access level
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR adds a new Etsy OAuth 2.0 authentication provider to the AspNet.Security.OAuth.Providers library. The implementation supports Etsy's Authorization Code flow with PKCE, personal access type (public client), and optional detailed user information fetching.
- Implements complete OAuth 2.0 flow with PKCE requirement
- Supports optional detailed user profile enrichment via configurable flag
- Includes comprehensive validation for required scopes and configuration
Reviewed Changes
Copilot reviewed 11 out of 12 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
src/AspNet.Security.OAuth.Etsy/EtsyAuthenticationOptions.cs |
Configuration options with validation logic for Etsy OAuth requirements |
src/AspNet.Security.OAuth.Etsy/EtsyAuthenticationHandler.cs |
OAuth handler implementing token exchange and user info retrieval |
src/AspNet.Security.OAuth.Etsy/EtsyAuthenticationExtensions.cs |
Extension methods for registering Etsy authentication |
src/AspNet.Security.OAuth.Etsy/EtsyAuthenticationDefaults.cs |
Default endpoint URLs and configuration values |
src/AspNet.Security.OAuth.Etsy/EtsyAuthenticationConstants.cs |
Claim types and scope constants |
src/AspNet.Security.OAuth.Etsy/EtsyAuthenticationAccessType.cs |
Enum defining access types (Personal) |
test/AspNet.Security.OAuth.Providers.Tests/Etsy/EtsyTests.cs |
Integration tests for authentication flow and claims |
test/AspNet.Security.OAuth.Providers.Tests/Etsy/EtsyAuthenticationOptionsTests.cs |
Unit tests for options validation |
test/AspNet.Security.OAuth.Providers.Tests/Etsy/bundle.json |
Test fixture data for HTTP interception |
docs/etsy.md |
Comprehensive usage documentation with examples |
AspNet.Security.OAuth.Providers.sln |
Solution file updated with new project references |
Comments suppressed due to low confidence (2)
src/AspNet.Security.OAuth.Etsy/EtsyAuthenticationHandler.cs:51
- This assignment to userId is useless, since its value is never read.
var userId = meRoot.GetProperty("user_id").GetInt64();
src/AspNet.Security.OAuth.Etsy/EtsyAuthenticationHandler.cs:52
- This assignment to shopId is useless, since its value is never read.
var shopId = meRoot.GetProperty("shop_id").GetInt64();
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Scope.Add(Scopes.ShopsRead); | ||
|
|
||
| // Map basic user claims | ||
| ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "user_id"); |
Copilot
AI
Nov 7, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The NameIdentifier claim is being mapped to "user_id" but according to the test data and API documentation, it should be mapped to "shop_id" (line 26 of EtsyTests.cs expects shop_id as NameIdentifier). The current mapping creates inconsistency where NameIdentifier represents the user_id instead of shop_id.
| ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "user_id"); | |
| ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "shop_id"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the identifyer should be the user_id in my opinion reading the usage in auth docs of them and their reference, so the solution should be opposite of what copilot assumed 👍
But now I have the small problem, that it errors me with duplicate mappings, so I checked how its done in the existing providers, where there is no duplicate adding (removed them in my local code already) but what if the user not notices we did already add them by default? The Etsy "UserInfoEndpoint" is not like in other API's, where you would directly call the getUser Endpoint and not have the getMe inbetween🤔
@martincostello can I have your opinion to this?
My solution idea would be, to provide a DetailedUserInfoClaims IReadOnlyList<string> in the Defaults class maybe, so the caller could use a quick & clean loop to get them all and I can also use it for testing or similar 🤔
Similar means like, I could also assume that if he sets the IncludeDetailedUserInfo option to true, I would add those claims in my handler with loop, but this would have the downside (if this would be the default behaviour) that the caller might not want/need the first/last name or the image, then just the email 🤷 then the cookie would maybe get bigger than required.
How critical is this, so what should we do?
- Linking assumed
UserInfoEndpointgetMeat reference with onlyshop_idanduser_id - Linking assumed
DetailedUserInfoEndpointgetUserat reference withfirst_name,last_name,primary_email,image_url_75x75and the identifieruser_id
But I dont think this should be a always done request, because it's requiring theemail_rScope and the endpoint description lets me assume we are not guaranteed that the client aggrees with letting us get it 🤔
against this, the call to getUserAdress is defintly caller task to do, not from the provider in my opinion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@martincostello While validating the claim output, I identified two potential enhancements that might improve alignment with Etsy’s API conventions.
Since I’m still new to the claims-mapping aspects of ASP.NET Core, I’d appreciate your assessment on whether either approach could introduce complications in claim processing.
The getMe endpoint returns:
{
"user_id": 1,
"shop_id": 1
}Option 1 — Expose urn:etsy:user_id as an additional optional claim
Map it to the same value currently used for nameIdentifier. This would mirror Etsy’s terminology without altering existing logic.
Option 2 — Derive nameIdentifier from a combination of user_id and shop_id
Since both values are always present, even with our minimal configuration setup using IncludeDetailedUserInfo = false a combined identifier might avoid ambiguity between the two identifiers.
The current implementation behaves correctly based on tests and Mvc.Client. These ideas are non-breaking suggestions, provided they don’t conflict with the claims pipeline.
| /// <returns>A JSON document containing the detailed user information.</returns> | ||
| protected virtual async Task<JsonDocument> GetDetailedUserInfoAsync([NotNull] OAuthTokenResponse tokens) | ||
| { | ||
| using var userRequest = new HttpRequestMessage(HttpMethod.Get, EtsyAuthenticationDefaults.EtsyBaseUri + EtsyAuthenticationDefaults.UserDetailsPath); |
Copilot
AI
Nov 7, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing {user_id} parameter substitution in the URL. The UserDetailsPath contains a placeholder {user_id} but it's concatenated directly to the base URI without replacing it with the actual user ID value. The code should extract the user_id from the /me response and substitute it into the URL template.
| using var userRequest = new HttpRequestMessage(HttpMethod.Get, EtsyAuthenticationDefaults.EtsyBaseUri + EtsyAuthenticationDefaults.UserDetailsPath); | |
| // Step 1: Get /me to extract user_id | |
| using var meRequest = new HttpRequestMessage(HttpMethod.Get, EtsyAuthenticationDefaults.EtsyBaseUri + "/me"); | |
| meRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); | |
| meRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken); | |
| meRequest.Headers.Add("x-api-key", Options.ClientId); | |
| using var meResponse = await Backchannel.SendAsync(meRequest, HttpCompletionOption.ResponseHeadersRead, Context.RequestAborted); | |
| if (!meResponse.IsSuccessStatusCode) | |
| { | |
| await Log.UserProfileErrorAsync(Logger, meResponse, Context.RequestAborted); | |
| throw new HttpRequestException("An error occurred while retrieving user info from Etsy /me endpoint."); | |
| } | |
| using var meJson = JsonDocument.Parse(await meResponse.Content.ReadAsStringAsync(Context.RequestAborted)); | |
| string? userId = null; | |
| if (meJson.RootElement.TryGetProperty("user_id", out var userIdElement)) | |
| { | |
| userId = userIdElement.GetString(); | |
| } | |
| if (string.IsNullOrEmpty(userId)) | |
| { | |
| throw new HttpRequestException("Could not extract user_id from Etsy /me response."); | |
| } | |
| // Step 2: Substitute {user_id} in UserDetailsPath | |
| string userDetailsPath = EtsyAuthenticationDefaults.UserDetailsPath.Replace("{user_id}", userId, StringComparison.Ordinal); | |
| using var userRequest = new HttpRequestMessage(HttpMethod.Get, EtsyAuthenticationDefaults.EtsyBaseUri + userDetailsPath); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@martincostello this comment should not longer apply, what do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would assume, as tests are passing and the Auth flow did work with the Mvc.Client on the "living object" (real API) this suggestion is not needed like this and fine with the current setup.
@martincostello would you agree with this?
src/AspNet.Security.OAuth.Etsy/AspNet.Security.OAuth.Etsy.csproj
Outdated
Show resolved
Hide resolved
test/AspNet.Security.OAuth.Providers.Tests/Etsy/EtsyAuthenticationOptionsTests.cs
Outdated
Show resolved
Hide resolved
| }, | ||
| { | ||
| "comment": "Etsy /v3/application/users/{user_id} endpoint - returns detailed user information", | ||
| "uri": "https://openapi.etsy.com/v3/application/users/123456", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on a previous comment, I don't see how this URL would match and thus the test would fail.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@martincostello reviewing it currently along the comment of copilot: #1126 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@martincostello I never worked with such .json file using tests before, but seeing the test results, it seems that my code gets the /123456 as it should, but setting the uri in this bundle.json to the formatting string does result in the error:
Microsoft.AspNetCore.Authentication.AuthenticationFailureException : An error was encountered while handling the remote login.
---- JustEat.HttpClientInterception.HttpRequestNotInterceptedException : No HTTP response is configured for GET https://openapi.etsy.com/v3/application/users/123456.
at Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler`1.HandleRequestAsync()
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.TestHost.HttpContextBuilder.<>c__DisplayClass23_0.<<SendAsync>g__RunRequestAsync|0>d.MoveNext()
at Microsoft.AspNetCore.TestHost.ClientHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
at AspNet.Security.OAuth.Infrastructure.LoopbackRedirectHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) (file:///c%3A/Users/Sonja/source/repos/AspNet.Security.OAuth.Providers/test/AspNet.Security.OAuth.Providers.Tests/Infrastructure/LoopbackRedirectHandler.cs#L77,0)
at System.Net.Http.HttpClient.<SendAsync>g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken)
at AspNet.Security.OAuth.OAuthTests`1.AuthenticateUserAsync(WebApplicationFactory`1 server) (file:///c%3A/Users/Sonja/source/repos/AspNet.Security.OAuth.Providers/test/AspNet.Security.OAuth.Providers.Tests/OAuthTests%601.cs#L256,0)
at AspNet.Security.OAuth.Providers.Tests.Etsy.EtsyTests.Includes_Detailed_Claims_When_IncludeDetailedUserInfo_Is_True() (file:///c%3A/Users/Sonja/source/repos/AspNet.Security.OAuth.Providers/test/AspNet.Security.OAuth.Providers.Tests/Etsy/EtsyTests.cs#L80,0)
at JustEat.HttpClientInterception.InterceptingHttpMessageHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
at Microsoft.Extensions.Http.Logging.LoggingScopeHttpMessageHandler.<SendCoreAsync>g__Core|4_0(HttpRequestMessage request, Boolean useAsync, CancellationToken cancellationToken)
at System.Net.Http.HttpClient.<SendAsync>g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken)
at AspNet.Security.OAuth.Etsy.EtsyAuthenticationHandler.GetDetailedUserInfoAsync(OAuthTokenResponse tokens, Int64 userId) (file:///c%3A/Users/Sonja/source/repos/AspNet.Security.OAuth.Providers/src/AspNet.Security.OAuth.Etsy/EtsyAuthenticationHandler.cs#L117,0)
at AspNet.Security.OAuth.Etsy.EtsyAuthenticationHandler.CreateTicketAsync(ClaimsIdentity identity, AuthenticationProperties properties, OAuthTokenResponse tokens) (file:///c%3A/Users/Sonja/source/repos/AspNet.Security.OAuth.Providers/src/AspNet.Security.OAuth.Etsy/EtsyAuthenticationHandler.cs#L70,0)
at Microsoft.AspNetCore.Authentication.OAuth.OAuthHandler`1.HandleRemoteAuthenticateAsync()
at Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler`1.HandleRequestAsync()
as soon as I did just insert the number back in there it was fine 🤔 do you know more about this maybe?
I would assume this value in bundle.json needs to be this way, reading this report.
Co-authored-by: Martin Costello <martin@martincostello.com>
Co-authored-by: Martin Costello <martin@martincostello.com>
Co-authored-by: Martin Costello <martin@martincostello.com>
there is no endpoint this could get from and as its in the table format in etsy api, it gets also not inserted into the api spec .json for CLI tools like Kiota
…y CA1863 suggestions, apply xml docs changes
…Configuration and add Extension for simpler ImageUriClaim
…d code changes in Etsy Provider
…k to defaults in handler
- openapi instead of just api - reverted formated string in bundle.json
… const strings could be used in attributes, not static readonly strings
User side Post configure is later than provider side post configure. this causes the Scope not to be evaluated and the Mappings are not applyed automatically HACK: add them all manually for the test postConfigure until decision is made this could be put in validate override or always consumer side?
…elf for detailed user info if he wants to use `PostConfigure` himself, which runs after the Provider side
TODO: Should be checked, we still need the full methods when using the attribute LoggerMessage. Maybe we can delete the non attribute using method then?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
Copilot reviewed 16 out of 17 changed files in this pull request and generated 8 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
ad60b5b to
36cacb2
Compare
|
Thanks for responding to the various comments - I'll take a look at this again when I get the time. Shipping the release for .NET 10 was the main priority this week. |
|
@martincostello thanks for your response and time! I am not sure in this, because I have had lots of trouble with implementing oAuth in there, because of Antiforgery and Cors 😓 the Official MS Docs for oAuth2 are horribly lacking on guidiance when there is no exiting provider like Google or something OIDC that supports discovery endpoint, which the Etsy API doesn't have 👀 We do have the Auth Code Exchange implemented here, which is against the Cross Origin Forgery Attacking 🤔 And in the Authentication/Authorization docs for minimal api here:
Its not told in our Readme files, if we do need Cookie to be added, or if the oAuth Base Extension our Providers here are building on do this already for us by default. In case we need the dev user (like me then) to add this for e.g. have something like the Refreshing of the Token implemented (which I seen in none of the existing Providers here (?), so did not add it in the Etsy Provider too) eventually it would be usefull to add a Link to the Cookie Auth ms docs eventually to the OIDC docs which contains a auth flow diagram with BUT while on the social login providers docs do link to the External Providers Page, which also links to this repo (thats how I found it 😉) this would be missing information about Refreshing Tokens too 🤷 If you would like me to open another issue / PR for this to improve these spots, let me know 👍 |
|
You should be able to use our MVC sample App to test the changes locally. |
|
@martincostello Hey, I am on trying the provider with the [HttpPost("~/signin")]
public async Task<IActionResult> SignIn([FromForm] string provider)
{
// Note: the "provider" parameter corresponds to the external
// authentication provider chosen by the user agent.
if (string.IsNullOrWhiteSpace(provider))
{
return BadRequest();
}
if (!await HttpContext.IsProviderSupportedAsync(provider))
{
return BadRequest();
}
// Instruct the middleware corresponding to the requested external identity
// provider to redirect the user agent to its own authorization endpoint.
// Note: the authenticationScheme parameter must match the value configured in Startup.cs
return Challenge(new AuthenticationProperties { RedirectUri = "/" }, provider);
}Could you maybe take a quick look and let me know if I just misunderstood this and the CallbackPath set in Options can be used as expected?
the Value I would like to set is e.g. |
|
|
|
@martincostello 🎉 🥳 Worked!!! just that the |

This pull request adds initial support for Etsy OAuth authentication to the codebase. It introduces the core implementation for an ASP.NET Core authentication provider, including configuration, constants, and documentation. The changes provide all the necessary building blocks and guidance for integrating Etsy login into an ASP.NET Core application.
Etsy OAuth Provider Implementation
AspNet.Security.OAuth.Etsy.csprojto define the package and its dependencies for the Etsy authentication provider.EtsyAuthenticationAccessTypeenum to distinguish between personal and commercial access types for Etsy apps.EtsyAuthenticationConstantsclass with claim and scope constants for mapping Etsy user data and permissions.EtsyAuthenticationDefaultswith all the required default values for endpoints, scheme names, and callback paths used by the Etsy authentication middleware.EtsyAuthenticationExtensionsto make it easy to add Etsy authentication to an ASP.NET Core application via the authentication builder.Documentation
docs/etsy.md, detailing configuration, required/optional settings, claims mapping, and sample code for integrating Etsy OAuth authentication.Note
The AccessType enum does so far only have one member
Personalbecause I only have this level of App registration and can not see or tell if the commercial Access Apps do somehow support confidential clients against the guidelines of the Etsy Authentication API. I asked their support some while ago which did tell that theshared secretwould be used at refresh, but there is no note at all for this in their docs, so this provider is adhering to the existing docs and oAuth standards RFC 6749 and does treat it as Public Client.Recognizing the #610 Issue in the past, I directly implemented the same fix up from the start, because even if the user sets the value, we don't have to require it, so the provider should also not fail because of not setting this.
Closes: #1082