@@ -17,10 +17,10 @@ Etsy's OAuth implementation uses Authorization Code with PKCE and issues refresh
1717 - [ Manually Added Claims] ( #manually-added-claims )
1818 - [ Advanced Configuration] ( #advanced-configuration )
1919 - [ Accessing claims (Minimal API Sample)] ( #accessing-claims-minimal-api-sample )
20- - [ Directly in Program.cs] ( #directly-in-programcs )
21- - [ Feature-style typed Minimal API endpoints with MapGroup] ( #feature-style-typed-minimal-api-endpoints-with-mapgroup )
20+ - [ Minimalistic directly in Program.cs] ( #minimalistic-directly-in-programcs )
21+ - [ Extended in a Feature-style Minimal API with endpoints using MapGroup] ( #extended-in-a-feature-style-minimal-api-with-endpoints-using-mapgroup )
22+ - [ Define record types for Typed Results] ( #define-record-types-for-typed-results )
2223 - [ Extension class anywhere in your project] ( #extension-class-anywhere-in-your-project )
23- - [ Sample record types] ( #sample-record-types )
2424 - [ Register the endpoints in Program.cs] ( #register-the-endpoints-in-programcs )
2525
2626## Quick Links
@@ -210,6 +210,7 @@ Here is a comprehensive `appsettings.json` example covering supported options an
210210 "AuthorizationEndpoint" : " https://www.etsy.com/oauth/connect" ,
211211 "TokenEndpoint" : " https://openapi.etsy.com/v3/public/oauth/token" ,
212212 "UserInformationEndpoint" : " https://openapi.etsy.com/v3/application/users/me" ,
213+ "CallbackPath" : " /signin/etsy" ,
213214 "SaveTokens" : true ,
214215 "Scopes" : [ " shops_r" , " email_r" ]
215216 }
@@ -229,54 +230,67 @@ If you bind then from configuration, set the options in code, for example:
229230builder .Services .AddAuthentication (options =>
230231{
231232 options .DefaultScheme = CookieAuthenticationDefaults .AuthenticationScheme ;
233+ // If you only have Etsy as external provider you can apply it as default challenge scheme
232234 options .DefaultChallengeScheme = EtsyAuthenticationDefaults .AuthenticationScheme ;
233235})
234- .AddCookie ()
236+ .AddCookie (options =>
237+ {
238+ options .LoginPath = " /signin" ;
239+ options .LogoutPath = " /signout" ;
240+ })
235241.AddEtsy (options =>
236242{
237- var section = builder .Configuration .GetSection (" Etsy" );
238- options .ClientId = section .GetValue <string ?>(" ClientId" )
239- ?? throw new InvalidOperationException (" Etsy:ClientId configuration value is missing." );
243+ var section = builder .Configuration .GetSection (" Etsy" ).Get <EtsyAuthenticationOptions >()! ;
244+ if (section is not EtsyAuthenticationOptions
245+ // Check if the values from appsettings.json has been properly overridden
246+ || section .ClientId is " client-id-from-user-secrets" )
247+ {
248+ throw new InvalidOperationException (" Etsy configuration section is missing or invalid." );
249+ }
240250
241- // For the Detailed User Info Endpoint and SaveTokens are default values pre-set, but you can override them here
242- options .IncludeDetailedUserInfo = section .GetValue <bool >(" IncludeDetailedUserInfo" );
243- options .SaveTokens = section .GetValue <bool >(" SaveTokens" );
251+ options .ClientId = section .ClientId ;
252+ // Optional: The Etsy App registration provides the `Shared Secret` but it's not documented to be used/required for PKCE flows.
253+ options .ClientSecret = section .ClientSecret ;
254+ // Optional: Include detailed user info and auto-mapped claims to get e.g. email, first and last name
255+ options .IncludeDetailedUserInfo = section .IncludeDetailedUserInfo ;
244256
245- // Use the defaults from EtsyAuthenticationDefaults, if you want to repeat them here, but they are set automatically
257+ // Optional: Override the defaults from EtsyAuthenticationDefaults with your own values (not recommended! Will potentially break the handler)
258+ // Here we just re-assign the defaults for demonstration
246259 options .AuthorizationEndpoint = EtsyAuthenticationDefaults .AuthorizationEndpoint ;
247260 options .TokenEndpoint = EtsyAuthenticationDefaults .TokenEndpoint ;
248261 options .UserInformationEndpoint = EtsyAuthenticationDefaults .UserInformationEndpoint ;
249262
250- // Apply scopes from config if present
251- var scopes = section .Get < string []>( " Scopes " ) ;
263+ // Optional: Override SaveTokens setting from configuration (not recommended to disable! as Etsy API uses refresh tokens)
264+ options . SaveTokens = section .SaveTokens ;
252265
253- if (scopes is not null )
266+ // Optional: Add scopes from configuration
267+ foreach (var scope in section .Scopes )
254268 {
255- foreach (var scope in scopes )
256- {
257- options .Scope .Add (scope );
258- }
269+ options .Scope .Add (scope );
259270 }
260271
261- // Optionally Map the image claim
272+ // Optional: Or add scopes manually with provided constants
273+ options .Scope .Add (EtsyAuthenticationConstants .Scopes .TransactionsRead );
274+
275+ // Optional: Map the image claim
262276 options .ClaimActions .MapImageClaim ();
263277
264278 // Map other Claims
265- options .ClaimActions .MapJsonKey (" urn:etsy:listingsWrite" , " listings_w " );
279+ options .ClaimActions .MapJsonKey (" urn:etsy:listingsWrite" , EtsyAuthenticationConstants . Claims . ListingsWrite );
266280})
267281```
268282
269283## Accessing claims (Minimal API Sample)
270284
271285If you want to access the claims provided by the Etsy provider, you can set up some Minimal API endpoints like this:
272286
273- ### Directly in Program.cs
287+ ### Minimalistic directly in Program.cs
274288
275289``` csharp
276290using AspNet .Security .OAuth .Etsy ;
277291using System .Security .Claims ;
278292
279- app .MapGet (" /profile" , (ClaimsPrincipal user ) =>
293+ app .MapGet (" /etsy/ profile" , (ClaimsPrincipal user ) =>
280294{
281295 var userId = user .FindFirstValue (ClaimTypes .NameIdentifier );
282296 var shopId = user .FindFirstValue (EtsyAuthenticationConstants .Claims .ShopId );
@@ -286,10 +300,43 @@ app.MapGet("/profile", (ClaimsPrincipal user) =>
286300 var imageUrl = user .FindFirstValue (EtsyAuthenticationConstants .Claims .ImageUrl );
287301
288302 return Results .Ok (new { userId , shopId , email , firstName , lastName , imageUrl });
289- }).RequireAuthorization ();
303+ })
304+ .RequireAuthorization ()
305+ .WithName (" EtsyProfile" )
306+ .WithSummary (" Get authenticated user's Etsy profile information" );
307+ ```
308+
309+ ### Extended in a Feature-style Minimal API with endpoints using MapGroup
310+
311+ This sample assumes you not only have Etsy as external provider and use cookie authentication for session management.
312+
313+ #### Define record types for Typed Results
314+
315+ Before we can start, we need some record types to hold the user profile and token information.
316+
317+ The following ones are created from the json-objects returned by Etsy's API.
318+
319+ ``` csharp
320+ public sealed record UserInfo
321+ {
322+ public required string UserId { get ; init ; }
323+ public required string ShopId { get ; init ; }
324+ public string ? Email { get ; init ; }
325+ public string ? FirstName { get ; init ; }
326+ public string ? LastName { get ; init ; }
327+ public string ? ImageUrl { get ; init ; }
328+ }
329+
330+ public sealed record TokenInfo
331+ {
332+ public string ? AccessToken { get ; init ; }
333+ public string ? RefreshToken { get ; init ; }
334+ public string ? ExpiresAt { get ; init ; }
335+ }
290336```
291337
292- ### Feature-style typed Minimal API endpoints with MapGroup
338+ > [ !NOTE]
339+ > Make sure to add proper JSON serialization attributes if you use System.Text.Json or Newtonsoft.Json to serialize those records to JSON in the HTTP responses.
293340
294341#### Extension class anywhere in your project
295342
@@ -336,12 +383,11 @@ public static class EtsyAuthEndpoints
336383
337384 private static Results <ChallengeHttpResult , RedirectHttpResult > SignInAsync (string ? returnUrl )
338385 => TypedResults .Challenge (
339- new AuthenticationProperties { RedirectUri = returnUrl ?? " /" },
340- new [] { EtsyAuthenticationDefaults .AuthenticationScheme });
386+ new AuthenticationProperties { RedirectUri = returnUrl ?? " /" }, EtsyAuthenticationDefaults .AuthenticationScheme );
341387
342388 private static async Task <RedirectHttpResult > SignOutAsync (HttpContext context )
343389 {
344- await context .SignOutAsync (CookieAuthenticationDefaults .AuthenticationScheme );
390+ await context .SignOutAsync (new AuthenticationProperties { RedirectUri = " / " }, CookieAuthenticationDefaults .AuthenticationScheme );
345391 return TypedResults .Redirect (" /" );
346392 }
347393
@@ -371,32 +417,13 @@ public static class EtsyAuthEndpoints
371417
372418 return TypedResults .Ok (tokenInfo );
373419 }
374- ```
375-
376- #### Sample record types
377-
378- ```csharp
379- public sealed record UserInfo
380- {
381- public required string UserId { get ; init ; }
382- public required string ShopId { get ; init ; }
383- public string ? Email { get ; init ; }
384- public string ? FirstName { get ; init ; }
385- public string ? LastName { get ; init ; }
386- public string ? ImageUrl { get ; init ; }
387- }
388-
389- public sealed record TokenInfo
390- {
391- public string ? AccessToken { get ; init ; }
392- public string ? RefreshToken { get ; init ; }
393- public string ? ExpiresAt { get ; init ; }
394- }
395420}
396421```
397422
398423#### Register the endpoints in Program.cs
399424
425+ Now that we have defined the extension method to map the Etsy authentication endpoints, we need to register them in our ` Program.cs ` file.
426+
400427``` csharp
401428using MyApi .Features .Authorization ;
402429app .MapEtsyAuth ();
0 commit comments