Skip to content

Commit 3ad2b10

Browse files
ksroda-saclaude
andcommitted
refactor: surface Duende.IdentityModel import in token-refresh step 1
The refresh step uses Duende.IdentityModel types, but the `using` lived outside any @snippet tag so the Quickstart UI never showed it. Include the imports (and rename step 1's description) so readers see the required package. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ee0f977 commit 3ad2b10

2 files changed

Lines changed: 10 additions & 9 deletions

File tree

samples/dotnet/token-refresh/src/Program.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1+
// @snippet:step1:start
2+
// @description Import the ASP.NET Core OIDC/cookie middleware plus Duende.IdentityModel (used later for the refresh-token grant) and DotNetEnv for `.env` loading
13
using System.Security.Claims;
24
using Microsoft.AspNetCore.Authentication;
35
using Microsoft.AspNetCore.Authentication.Cookies;
46
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
57
using Duende.IdentityModel.Client;
68
using DotNetEnv;
79

8-
// @snippet:step1:start
9-
// @description Load environment variables from .env (searches parent dirs so `dotnet run --project src` finds the .env at the sample root). Existing env vars win.
10+
// Searches parent dirs so `dotnet run --project src` finds the .env at the sample root. Existing env vars win.
1011
Env.TraversePath().NoClobber().Load();
1112
// @snippet:step1:end
1213

snippets.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -322,43 +322,43 @@
322322
"steps": [
323323
{
324324
"step": 1,
325-
"description": "Load environment variables from .env (searches parent dirs so `dotnet run --project src` finds the .env at the sample root). Existing env vars win.",
326-
"code": "Env.TraversePath().NoClobber().Load();",
325+
"description": "Import the ASP.NET Core OIDC/cookie middleware plus Duende.IdentityModel (used later for the refresh-token grant) and DotNetEnv for `.env` loading",
326+
"code": "using System.Security.Claims;\nusing Microsoft.AspNetCore.Authentication;\nusing Microsoft.AspNetCore.Authentication.Cookies;\nusing Microsoft.AspNetCore.Authentication.OpenIdConnect;\nusing Duende.IdentityModel.Client;\nusing DotNetEnv;\n\n// Searches parent dirs so `dotnet run --project src` finds the .env at the sample root. Existing env vars win.\nEnv.TraversePath().NoClobber().Load();",
327327
"file": "src/Program.cs",
328328
"lang": "csharp",
329-
"lines": "8-10"
329+
"lines": "1-11"
330330
},
331331
{
332332
"step": 2,
333333
"description": "Configure cookie auth + OpenID Connect; wire OnValidatePrincipal so every authenticated request can auto-refresh the access token",
334334
"code": "builder.Services.AddAuthentication(options =>\n {\n options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;\n options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;\n })\n .AddCookie(options =>\n {\n options.Events.OnValidatePrincipal = ValidatePrincipalWithRefresh;\n })\n .AddOpenIdConnect(options =>\n {\n options.Authority = \"{{ISSUER_URL}}\";\n options.ClientId = \"{{CLIENT_ID}}\";\n options.ClientSecret = \"{{CLIENT_SECRET}}\";\n options.ResponseType = \"code\";\n options.UsePkce = true;\n options.SaveTokens = true;\n options.GetClaimsFromUserInfoEndpoint = true;\n // Keep OIDC claim names (given_name, family_name, email) as-is — the default\n // middleware remaps them to long xmlsoap URIs, which makes FindFirst(\"given_name\")\n // return null in user code.\n options.MapInboundClaims = false;\n options.CallbackPath = \"/callback\";\n options.Scope.Clear();\n foreach (var scope in \"{{SCOPES}}\".Split(' ', StringSplitOptions.RemoveEmptyEntries))\n options.Scope.Add(scope);\n });\n\nbuilder.Services.AddAuthorization();",
335335
"file": "src/Program.cs",
336336
"lang": "csharp",
337-
"lines": "17-47"
337+
"lines": "18-48"
338338
},
339339
{
340340
"step": 3,
341341
"description": "Swap a refresh token for a fresh access token via the OIDC token endpoint",
342342
"code": "static async Task<TokenResponse> RefreshTokensAsync(\n IHttpClientFactory httpFactory,\n string refreshToken)\n{\n var http = httpFactory.CreateClient();\n var disco = await http.GetDiscoveryDocumentAsync(\"{{ISSUER_URL}}\");\n if (disco.IsError)\n throw new InvalidOperationException($\"Discovery failed: {disco.Error}\");\n\n return await http.RequestRefreshTokenAsync(new RefreshTokenRequest\n {\n Address = disco.TokenEndpoint,\n ClientId = \"{{CLIENT_ID}}\",\n ClientSecret = \"{{CLIENT_SECRET}}\",\n RefreshToken = refreshToken,\n ClientCredentialStyle = ClientCredentialStyle.PostBody,\n });\n}",
343343
"file": "src/Program.cs",
344344
"lang": "csharp",
345-
"lines": "54-73"
345+
"lines": "55-74"
346346
},
347347
{
348348
"step": 4,
349349
"description": "Routes: user display, sign-in, manual refresh (POST /refresh), sign-out",
350350
"code": "app.MapGet(\"/\", async (HttpContext ctx) =>\n{\n if (ctx.User.Identity?.IsAuthenticated != true)\n return Results.Content(Views.RenderSignedOutPage(), \"text/html\");\n\n var expiresAtRaw = await ctx.GetTokenAsync(\"expires_at\");\n var expiresAtDisplay = DateTimeOffset.TryParse(expiresAtRaw, out var dt)\n ? dt.ToLocalTime().ToString(\"h:mm:ss tt\")\n : \"unknown\";\n return Results.Content(Views.RenderSignedInPage(ctx.User, expiresAtDisplay), \"text/html\");\n});\n\napp.MapGet(\"/login\", () => Results.Challenge(\n new AuthenticationProperties { RedirectUri = \"/\" },\n [OpenIdConnectDefaults.AuthenticationScheme]));\n\napp.MapPost(\"/refresh\", async (HttpContext ctx, IHttpClientFactory httpFactory) =>\n{\n var authResult = await ctx.AuthenticateAsync();\n if (!authResult.Succeeded || authResult.Properties is null)\n return Results.Redirect(\"/\");\n\n var refreshToken = authResult.Properties.GetTokenValue(\"refresh_token\");\n if (refreshToken is null) return Results.Redirect(\"/\");\n\n try\n {\n var tokens = await RefreshTokensAsync(httpFactory, refreshToken);\n if (tokens.IsError)\n return Results.Content(\n Views.RenderErrorPage(tokens.Error ?? \"refresh failed\"),\n \"text/html\");\n\n UpdateAuthProperties(authResult.Properties, tokens);\n await ctx.SignInAsync(\n CookieAuthenticationDefaults.AuthenticationScheme,\n authResult.Principal!,\n authResult.Properties);\n return Results.Redirect(\"/\");\n }\n catch (Exception ex)\n {\n return Results.Content(Views.RenderErrorPage(ex.Message), \"text/html\");\n }\n});\n\napp.MapGet(\"/logout\", () => Results.SignOut(\n new AuthenticationProperties { RedirectUri = \"/\" },\n [CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectDefaults.AuthenticationScheme]));\n\nstatic void UpdateAuthProperties(AuthenticationProperties props, TokenResponse tokens)\n{\n if (tokens.AccessToken is not null)\n props.UpdateTokenValue(\"access_token\", tokens.AccessToken);\n if (tokens.RefreshToken is not null)\n props.UpdateTokenValue(\"refresh_token\", tokens.RefreshToken);\n if (tokens.IdentityToken is not null)\n props.UpdateTokenValue(\"id_token\", tokens.IdentityToken);\n if (tokens.ExpiresIn > 0)\n props.UpdateTokenValue(\n \"expires_at\",\n DateTimeOffset.UtcNow.AddSeconds(tokens.ExpiresIn).ToString(\"o\"));\n}",
351351
"file": "src/Program.cs",
352352
"lang": "csharp",
353-
"lines": "76-140"
353+
"lines": "77-141"
354354
},
355355
{
356356
"step": 5,
357357
"description": "Auto-refresh via the cookie auth OnValidatePrincipal event — runs on every authenticated request",
358358
"code": "static async Task ValidatePrincipalWithRefresh(CookieValidatePrincipalContext ctx)\n{\n var expiresAt = ctx.Properties.GetTokenValue(\"expires_at\");\n if (expiresAt is null || !DateTimeOffset.TryParse(expiresAt, out var expiry)) return;\n if (expiry - DateTimeOffset.UtcNow > TimeSpan.FromSeconds(60)) return;\n\n var refreshToken = ctx.Properties.GetTokenValue(\"refresh_token\");\n if (refreshToken is null)\n {\n ctx.RejectPrincipal();\n return;\n }\n\n var httpFactory = ctx.HttpContext.RequestServices.GetRequiredService<IHttpClientFactory>();\n try\n {\n var tokens = await RefreshTokensAsync(httpFactory, refreshToken);\n if (tokens.IsError)\n {\n ctx.RejectPrincipal();\n return;\n }\n\n UpdateAuthProperties(ctx.Properties, tokens);\n ctx.ShouldRenew = true;\n }\n catch\n {\n ctx.RejectPrincipal();\n }\n}",
359359
"file": "src/Program.cs",
360360
"lang": "csharp",
361-
"lines": "143-175"
361+
"lines": "144-176"
362362
}
363363
],
364364
"framework": "dotnet",

0 commit comments

Comments
 (0)