[{"data":1,"prerenderedAt":1562},["ShallowReactive",2],{"blog-\u002Fblog\u002Fdotnet-jwt-tokens":3},{"id":4,"title":5,"body":6,"description":1547,"difficulty":1548,"extension":1549,"framework":1550,"frameworkSlug":1551,"meta":1552,"navigation":166,"order":163,"path":1553,"qaPath":1554,"seo":1555,"stem":1556,"subtopic":1557,"topic":1558,"topicSlug":1559,"updated":1560,"__hash__":1561},"blog\u002Fblog\u002Fdotnet-jwt-tokens.md","JWT Authentication in ASP.NET Core",{"type":7,"value":8,"toc":1527},"minimark",[9,14,23,27,34,44,118,126,136,140,330,345,349,352,437,443,447,666,672,676,679,796,799,803,806,912,916,923,1069,1075,1079,1207,1211,1214,1223,1273,1279,1285,1288,1292,1297,1304,1314,1318,1321,1326,1335,1339,1347,1359,1363,1366,1371,1386,1390,1397,1410,1493,1497,1523],[10,11,13],"h2",{"id":12},"why-jwt-knowledge-matters-in-net-interviews","Why JWT knowledge matters in .NET interviews",[15,16,17,18,22],"p",{},"JWTs are the default credential format for REST APIs, microservices, and SPAs. Interviewers\ntest this area because the failure modes are severe — a misconfigured ",[19,20,21],"code",{},"TokenValidationParameters","\ncan let attackers forge identities, replay tokens across services, or exploit expired credentials.\nThis article covers the full lifecycle: structure, validation, generation, refresh, and the\nsecurity pitfalls that show up in real CVEs.",[10,24,26],{"id":25},"what-a-jwt-actually-is","What a JWT actually is",[15,28,29,30,33],{},"A JWT is three Base64URL-encoded JSON objects joined by dots: ",[19,31,32],{},"header.payload.signature",".",[35,36,41],"pre",{"className":37,"code":39,"language":40},[38],"language-text","eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\n.eyJzdWIiOiI0MiIsImVtYWlsIjoiYWxpY2VAZXhhbXBsZS5jb20iLCJyb2xlIjoiQWRtaW4iLCJleHAiOjE3MTk5MzkyMDB9\n.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c\n","text",[19,42,39],{"__ignoreMap":43},"",[45,46,47,60],"table",{},[48,49,50],"thead",{},[51,52,53,57],"tr",{},[54,55,56],"th",{},"Part",[54,58,59],{},"Contains",[61,62,63,79,110],"tbody",{},[51,64,65,69],{},[66,67,68],"td",{},"Header",[66,70,71,74,75,78],{},[19,72,73],{},"alg"," (HS256, RS256, …) and ",[19,76,77],{},"typ"," (\"JWT\")",[51,80,81,84],{},[66,82,83],{},"Payload",[66,85,86,87,90,91,90,94,90,97,90,100,90,103,90,106,109],{},"Claims — registered (",[19,88,89],{},"sub",", ",[19,92,93],{},"iss",[19,95,96],{},"aud",[19,98,99],{},"exp",[19,101,102],{},"iat",[19,104,105],{},"nbf",[19,107,108],{},"jti",") + application-specific",[51,111,112,115],{},[66,113,114],{},"Signature",[66,116,117],{},"Cryptographic proof that header + payload haven't been tampered with",[15,119,120,121,125],{},"The payload is ",[122,123,124],"strong",{},"encoded, not encrypted",". Anyone can decode it. Only the key holder can produce a\nvalid signature. This means:",[127,128,129,133],"ul",{},[130,131,132],"li",{},"Integrity is guaranteed (tampered payload → invalid signature).",[130,134,135],{},"Confidentiality is not — never put passwords, SSNs, or payment data in claims.",[10,137,139],{"id":138},"configuring-jwt-bearer-authentication","Configuring JWT bearer authentication",[35,141,145],{"className":142,"code":143,"language":144,"meta":43,"style":43},"language-csharp shiki shiki-themes github-light github-dark","\u002F\u002F NuGet: Microsoft.AspNetCore.Authentication.JwtBearer\nvar key = Encoding.UTF8.GetBytes(builder.Configuration[\"Jwt:Secret\"]!);\n\nbuilder.Services\n    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)\n    .AddJwtBearer(options =>\n    {\n        options.TokenValidationParameters = new TokenValidationParameters\n        {\n            ValidateIssuer           = true,\n            ValidIssuer              = builder.Configuration[\"Jwt:Issuer\"],\n\n            ValidateAudience         = true,\n            ValidAudience            = builder.Configuration[\"Jwt:Audience\"],\n\n            ValidateLifetime         = true,\n            ClockSkew                = TimeSpan.FromSeconds(30),\n\n            ValidateIssuerSigningKey = true,\n            IssuerSigningKey         = new SymmetricSecurityKey(key),\n\n            RequireExpirationTime    = true,\n            RequireSignedTokens      = true, \u002F\u002F never set to false\n        };\n    });\n\nbuilder.Services.AddAuthorization();\n\nvar app = builder.Build();\napp.UseAuthentication();\napp.UseAuthorization();\n","csharp",[19,146,147,155,161,168,174,180,186,192,198,204,210,216,221,227,233,238,244,250,255,261,267,272,278,284,290,296,301,307,312,318,324],{"__ignoreMap":43},[148,149,152],"span",{"class":150,"line":151},"line",1,[148,153,154],{},"\u002F\u002F NuGet: Microsoft.AspNetCore.Authentication.JwtBearer\n",[148,156,158],{"class":150,"line":157},2,[148,159,160],{},"var key = Encoding.UTF8.GetBytes(builder.Configuration[\"Jwt:Secret\"]!);\n",[148,162,164],{"class":150,"line":163},3,[148,165,167],{"emptyLinePlaceholder":166},true,"\n",[148,169,171],{"class":150,"line":170},4,[148,172,173],{},"builder.Services\n",[148,175,177],{"class":150,"line":176},5,[148,178,179],{},"    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)\n",[148,181,183],{"class":150,"line":182},6,[148,184,185],{},"    .AddJwtBearer(options =>\n",[148,187,189],{"class":150,"line":188},7,[148,190,191],{},"    {\n",[148,193,195],{"class":150,"line":194},8,[148,196,197],{},"        options.TokenValidationParameters = new TokenValidationParameters\n",[148,199,201],{"class":150,"line":200},9,[148,202,203],{},"        {\n",[148,205,207],{"class":150,"line":206},10,[148,208,209],{},"            ValidateIssuer           = true,\n",[148,211,213],{"class":150,"line":212},11,[148,214,215],{},"            ValidIssuer              = builder.Configuration[\"Jwt:Issuer\"],\n",[148,217,219],{"class":150,"line":218},12,[148,220,167],{"emptyLinePlaceholder":166},[148,222,224],{"class":150,"line":223},13,[148,225,226],{},"            ValidateAudience         = true,\n",[148,228,230],{"class":150,"line":229},14,[148,231,232],{},"            ValidAudience            = builder.Configuration[\"Jwt:Audience\"],\n",[148,234,236],{"class":150,"line":235},15,[148,237,167],{"emptyLinePlaceholder":166},[148,239,241],{"class":150,"line":240},16,[148,242,243],{},"            ValidateLifetime         = true,\n",[148,245,247],{"class":150,"line":246},17,[148,248,249],{},"            ClockSkew                = TimeSpan.FromSeconds(30),\n",[148,251,253],{"class":150,"line":252},18,[148,254,167],{"emptyLinePlaceholder":166},[148,256,258],{"class":150,"line":257},19,[148,259,260],{},"            ValidateIssuerSigningKey = true,\n",[148,262,264],{"class":150,"line":263},20,[148,265,266],{},"            IssuerSigningKey         = new SymmetricSecurityKey(key),\n",[148,268,270],{"class":150,"line":269},21,[148,271,167],{"emptyLinePlaceholder":166},[148,273,275],{"class":150,"line":274},22,[148,276,277],{},"            RequireExpirationTime    = true,\n",[148,279,281],{"class":150,"line":280},23,[148,282,283],{},"            RequireSignedTokens      = true, \u002F\u002F never set to false\n",[148,285,287],{"class":150,"line":286},24,[148,288,289],{},"        };\n",[148,291,293],{"class":150,"line":292},25,[148,294,295],{},"    });\n",[148,297,299],{"class":150,"line":298},26,[148,300,167],{"emptyLinePlaceholder":166},[148,302,304],{"class":150,"line":303},27,[148,305,306],{},"builder.Services.AddAuthorization();\n",[148,308,310],{"class":150,"line":309},28,[148,311,167],{"emptyLinePlaceholder":166},[148,313,315],{"class":150,"line":314},29,[148,316,317],{},"var app = builder.Build();\n",[148,319,321],{"class":150,"line":320},30,[148,322,323],{},"app.UseAuthentication();\n",[148,325,327],{"class":150,"line":326},31,[148,328,329],{},"app.UseAuthorization();\n",[15,331,332,333,336,337,340,341,344],{},"The middleware reads ",[19,334,335],{},"Authorization: Bearer \u003Ctoken>"," from every request, validates the token\nagainst these parameters, and if valid, populates ",[19,338,339],{},"HttpContext.User"," with the ",[19,342,343],{},"ClaimsPrincipal","\nbuilt from the payload claims.",[10,346,348],{"id":347},"tokenvalidationparameters-what-each-setting-prevents","TokenValidationParameters — what each setting prevents",[15,350,351],{},"Every parameter you skip is a potential attack vector:",[45,353,354,364],{},[48,355,356],{},[51,357,358,361],{},[54,359,360],{},"Parameter",[54,362,363],{},"What it prevents",[61,365,366,376,393,403,413,427],{},[51,367,368,373],{},[66,369,370],{},[19,371,372],{},"ValidateIssuer",[66,374,375],{},"Tokens from untrusted issuers being accepted",[51,377,378,383],{},[66,379,380],{},[19,381,382],{},"ValidateAudience",[66,384,385,386,389,390],{},"Tokens issued for ",[19,387,388],{},"api1"," being replayed at ",[19,391,392],{},"api2",[51,394,395,400],{},[66,396,397],{},[19,398,399],{},"ValidateLifetime",[66,401,402],{},"Expired tokens being accepted indefinitely",[51,404,405,410],{},[66,406,407],{},[19,408,409],{},"ValidateIssuerSigningKey",[66,411,412],{},"Forged tokens with a different key",[51,414,415,420],{},[66,416,417],{},[19,418,419],{},"RequireSignedTokens",[66,421,422,423,426],{},"The ",[19,424,425],{},"alg=none"," attack (unsigned tokens accepted)",[51,428,429,434],{},[66,430,431],{},[19,432,433],{},"ClockSkew",[66,435,436],{},"Legitimate tokens rejected due to minor server clock drift",[15,438,439,440,442],{},"Never skip ",[19,441,382],{},". It's the most commonly missed and enables cross-API token replay —\na token legitimately issued for your mobile API being used against your admin API.",[10,444,446],{"id":445},"generating-tokens","Generating tokens",[35,448,450],{"className":142,"code":449,"language":144,"meta":43,"style":43},"public class JwtTokenService\n{\n    private readonly IConfiguration _config;\n    public JwtTokenService(IConfiguration config) => _config = config;\n\n    public string GenerateAccessToken(User user)\n    {\n        var key = new SymmetricSecurityKey(\n            Encoding.UTF8.GetBytes(_config[\"Jwt:Secret\"]!));\n\n        var claims = new List\u003CClaim>\n        {\n            new Claim(JwtRegisteredClaimNames.Sub,   user.Id.ToString()),\n            new Claim(JwtRegisteredClaimNames.Email, user.Email),\n            new Claim(JwtRegisteredClaimNames.Jti,   Guid.NewGuid().ToString()), \u002F\u002F unique ID\n            new Claim(ClaimTypes.Role,               user.Role),\n        };\n\n        var token = new JwtSecurityToken(\n            issuer:             _config[\"Jwt:Issuer\"],\n            audience:           _config[\"Jwt:Audience\"],\n            claims:             claims,\n            notBefore:          DateTime.UtcNow,\n            expires:            DateTime.UtcNow.AddMinutes(15), \u002F\u002F short-lived\n            signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256));\n\n        return new JwtSecurityTokenHandler().WriteToken(token);\n    }\n}\n\n\u002F\u002F Login endpoint:\n[HttpPost(\"login\")]\npublic async Task\u003CIActionResult> Login([FromBody] LoginDto dto)\n{\n    var user = await _userService.ValidateAsync(dto.Email, dto.Password);\n    if (user is null) return Unauthorized();\n\n    return Ok(new\n    {\n        accessToken = _tokenService.GenerateAccessToken(user),\n        expiresIn   = 900,\n    });\n}\n",[19,451,452,457,462,467,472,476,481,485,490,495,499,504,508,513,518,523,528,532,536,541,546,551,556,561,566,571,575,580,585,590,594,599,605,611,616,622,628,633,639,644,650,656,661],{"__ignoreMap":43},[148,453,454],{"class":150,"line":151},[148,455,456],{},"public class JwtTokenService\n",[148,458,459],{"class":150,"line":157},[148,460,461],{},"{\n",[148,463,464],{"class":150,"line":163},[148,465,466],{},"    private readonly IConfiguration _config;\n",[148,468,469],{"class":150,"line":170},[148,470,471],{},"    public JwtTokenService(IConfiguration config) => _config = config;\n",[148,473,474],{"class":150,"line":176},[148,475,167],{"emptyLinePlaceholder":166},[148,477,478],{"class":150,"line":182},[148,479,480],{},"    public string GenerateAccessToken(User user)\n",[148,482,483],{"class":150,"line":188},[148,484,191],{},[148,486,487],{"class":150,"line":194},[148,488,489],{},"        var key = new SymmetricSecurityKey(\n",[148,491,492],{"class":150,"line":200},[148,493,494],{},"            Encoding.UTF8.GetBytes(_config[\"Jwt:Secret\"]!));\n",[148,496,497],{"class":150,"line":206},[148,498,167],{"emptyLinePlaceholder":166},[148,500,501],{"class":150,"line":212},[148,502,503],{},"        var claims = new List\u003CClaim>\n",[148,505,506],{"class":150,"line":218},[148,507,203],{},[148,509,510],{"class":150,"line":223},[148,511,512],{},"            new Claim(JwtRegisteredClaimNames.Sub,   user.Id.ToString()),\n",[148,514,515],{"class":150,"line":229},[148,516,517],{},"            new Claim(JwtRegisteredClaimNames.Email, user.Email),\n",[148,519,520],{"class":150,"line":235},[148,521,522],{},"            new Claim(JwtRegisteredClaimNames.Jti,   Guid.NewGuid().ToString()), \u002F\u002F unique ID\n",[148,524,525],{"class":150,"line":240},[148,526,527],{},"            new Claim(ClaimTypes.Role,               user.Role),\n",[148,529,530],{"class":150,"line":246},[148,531,289],{},[148,533,534],{"class":150,"line":252},[148,535,167],{"emptyLinePlaceholder":166},[148,537,538],{"class":150,"line":257},[148,539,540],{},"        var token = new JwtSecurityToken(\n",[148,542,543],{"class":150,"line":263},[148,544,545],{},"            issuer:             _config[\"Jwt:Issuer\"],\n",[148,547,548],{"class":150,"line":269},[148,549,550],{},"            audience:           _config[\"Jwt:Audience\"],\n",[148,552,553],{"class":150,"line":274},[148,554,555],{},"            claims:             claims,\n",[148,557,558],{"class":150,"line":280},[148,559,560],{},"            notBefore:          DateTime.UtcNow,\n",[148,562,563],{"class":150,"line":286},[148,564,565],{},"            expires:            DateTime.UtcNow.AddMinutes(15), \u002F\u002F short-lived\n",[148,567,568],{"class":150,"line":292},[148,569,570],{},"            signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256));\n",[148,572,573],{"class":150,"line":298},[148,574,167],{"emptyLinePlaceholder":166},[148,576,577],{"class":150,"line":303},[148,578,579],{},"        return new JwtSecurityTokenHandler().WriteToken(token);\n",[148,581,582],{"class":150,"line":309},[148,583,584],{},"    }\n",[148,586,587],{"class":150,"line":314},[148,588,589],{},"}\n",[148,591,592],{"class":150,"line":320},[148,593,167],{"emptyLinePlaceholder":166},[148,595,596],{"class":150,"line":326},[148,597,598],{},"\u002F\u002F Login endpoint:\n",[148,600,602],{"class":150,"line":601},32,[148,603,604],{},"[HttpPost(\"login\")]\n",[148,606,608],{"class":150,"line":607},33,[148,609,610],{},"public async Task\u003CIActionResult> Login([FromBody] LoginDto dto)\n",[148,612,614],{"class":150,"line":613},34,[148,615,461],{},[148,617,619],{"class":150,"line":618},35,[148,620,621],{},"    var user = await _userService.ValidateAsync(dto.Email, dto.Password);\n",[148,623,625],{"class":150,"line":624},36,[148,626,627],{},"    if (user is null) return Unauthorized();\n",[148,629,631],{"class":150,"line":630},37,[148,632,167],{"emptyLinePlaceholder":166},[148,634,636],{"class":150,"line":635},38,[148,637,638],{},"    return Ok(new\n",[148,640,642],{"class":150,"line":641},39,[148,643,191],{},[148,645,647],{"class":150,"line":646},40,[148,648,649],{},"        accessToken = _tokenService.GenerateAccessToken(user),\n",[148,651,653],{"class":150,"line":652},41,[148,654,655],{},"        expiresIn   = 900,\n",[148,657,659],{"class":150,"line":658},42,[148,660,295],{},[148,662,664],{"class":150,"line":663},43,[148,665,589],{},[15,667,668,669,671],{},"Always include ",[19,670,108],{}," (JWT ID) for auditability and revocation support. Keep access tokens short-lived\n(≤ 15 minutes) — a stolen short-lived token is far less dangerous than a stolen 24-hour token.",[10,673,675],{"id":674},"symmetric-vs-asymmetric-signing","Symmetric vs asymmetric signing",[15,677,678],{},"The signing algorithm determines who can verify tokens and what key material is needed.",[35,680,682],{"className":142,"code":681,"language":144,"meta":43,"style":43},"\u002F\u002F Symmetric (HS256) — one shared secret, used to both sign and verify:\nvar key  = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret));\nvar cred = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);\n\u002F\u002F Simple — one key to manage\n\u002F\u002F Every service that validates tokens needs the secret — leaked secret compromises all\n\n\u002F\u002F Asymmetric (RS256) — private key signs, public key verifies:\nvar rsa        = RSA.Create();\nrsa.ImportFromPem(File.ReadAllText(\"private.pem\"));\nvar privateKey = new RsaSecurityKey(rsa);\nvar cred       = new SigningCredentials(privateKey, SecurityAlgorithms.RsaSha256);\n\u002F\u002F Public key can be shared freely — only the auth server needs the private key\n\u002F\u002F Scale: add a new microservice without sharing any secret\n\u002F\u002F Larger tokens; key rotation is more complex\n\n\u002F\u002F Resource server validates with public key only:\nvar rsaPublic  = RSA.Create();\nrsaPublic.ImportFromPem(File.ReadAllText(\"public.pem\"));\noptions.TokenValidationParameters.IssuerSigningKey = new RsaSecurityKey(rsaPublic);\n\n\u002F\u002F Auto-discovery via JWKS endpoint (identity providers like Azure AD, Auth0):\noptions.Authority = \"https:\u002F\u002Fauth.myapp.com\";\n\u002F\u002F Middleware fetches public keys from \u002F.well-known\u002Fopenid-configuration → jwks_uri and caches them\n",[19,683,684,689,694,699,704,709,713,718,723,728,733,738,743,748,753,757,762,767,772,777,781,786,791],{"__ignoreMap":43},[148,685,686],{"class":150,"line":151},[148,687,688],{},"\u002F\u002F Symmetric (HS256) — one shared secret, used to both sign and verify:\n",[148,690,691],{"class":150,"line":157},[148,692,693],{},"var key  = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret));\n",[148,695,696],{"class":150,"line":163},[148,697,698],{},"var cred = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);\n",[148,700,701],{"class":150,"line":170},[148,702,703],{},"\u002F\u002F Simple — one key to manage\n",[148,705,706],{"class":150,"line":176},[148,707,708],{},"\u002F\u002F Every service that validates tokens needs the secret — leaked secret compromises all\n",[148,710,711],{"class":150,"line":182},[148,712,167],{"emptyLinePlaceholder":166},[148,714,715],{"class":150,"line":188},[148,716,717],{},"\u002F\u002F Asymmetric (RS256) — private key signs, public key verifies:\n",[148,719,720],{"class":150,"line":194},[148,721,722],{},"var rsa        = RSA.Create();\n",[148,724,725],{"class":150,"line":200},[148,726,727],{},"rsa.ImportFromPem(File.ReadAllText(\"private.pem\"));\n",[148,729,730],{"class":150,"line":206},[148,731,732],{},"var privateKey = new RsaSecurityKey(rsa);\n",[148,734,735],{"class":150,"line":212},[148,736,737],{},"var cred       = new SigningCredentials(privateKey, SecurityAlgorithms.RsaSha256);\n",[148,739,740],{"class":150,"line":218},[148,741,742],{},"\u002F\u002F Public key can be shared freely — only the auth server needs the private key\n",[148,744,745],{"class":150,"line":223},[148,746,747],{},"\u002F\u002F Scale: add a new microservice without sharing any secret\n",[148,749,750],{"class":150,"line":229},[148,751,752],{},"\u002F\u002F Larger tokens; key rotation is more complex\n",[148,754,755],{"class":150,"line":235},[148,756,167],{"emptyLinePlaceholder":166},[148,758,759],{"class":150,"line":240},[148,760,761],{},"\u002F\u002F Resource server validates with public key only:\n",[148,763,764],{"class":150,"line":246},[148,765,766],{},"var rsaPublic  = RSA.Create();\n",[148,768,769],{"class":150,"line":252},[148,770,771],{},"rsaPublic.ImportFromPem(File.ReadAllText(\"public.pem\"));\n",[148,773,774],{"class":150,"line":257},[148,775,776],{},"options.TokenValidationParameters.IssuerSigningKey = new RsaSecurityKey(rsaPublic);\n",[148,778,779],{"class":150,"line":263},[148,780,167],{"emptyLinePlaceholder":166},[148,782,783],{"class":150,"line":269},[148,784,785],{},"\u002F\u002F Auto-discovery via JWKS endpoint (identity providers like Azure AD, Auth0):\n",[148,787,788],{"class":150,"line":274},[148,789,790],{},"options.Authority = \"https:\u002F\u002Fauth.myapp.com\";\n",[148,792,793],{"class":150,"line":280},[148,794,795],{},"\u002F\u002F Middleware fetches public keys from \u002F.well-known\u002Fopenid-configuration → jwks_uri and caches them\n",[15,797,798],{},"Use symmetric (HS256) for a single service that issues and verifies its own tokens. Use asymmetric\n(RS256 or ES256) in microservice architectures where multiple services verify but only one issues.",[10,800,802],{"id":801},"claim-mapping-gotchas","Claim mapping gotchas",[15,804,805],{},"ASP.NET Core maps standard JWT claim names to long WS-Federation URIs by default — a frequent\nsource of confusion in interviews.",[35,807,809],{"className":142,"code":808,"language":144,"meta":43,"style":43},"\u002F\u002F Default: JWT \"sub\" → ClaimTypes.NameIdentifier (long URI)\n\u002F\u002F Reading after default mapping:\nvar userId = User.FindFirstValue(ClaimTypes.NameIdentifier); \u002F\u002F works\n\n\u002F\u002F Disable mapping to use short JWT names directly:\nJwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();\n\u002F\u002F OR per handler (.NET 6+):\noptions.MapInboundClaims = false;\n\n\u002F\u002F Now read by short name:\nvar userId = User.FindFirstValue(\"sub\");\nvar email  = User.FindFirstValue(\"email\");\n\n\u002F\u002F Tell the framework which claim is the \"name\" and which is \"role\":\noptions.TokenValidationParameters.NameClaimType = JwtRegisteredClaimNames.Sub;\noptions.TokenValidationParameters.RoleClaimType = \"role\";\n\n\u002F\u002F Role arrays — the handler splits them automatically:\n\u002F\u002F Payload: { \"role\": [\"Admin\", \"Editor\"] }\n\u002F\u002F → two separate ClaimTypes.Role claims in ClaimsPrincipal\nvar roles = User.FindAll(\"role\").Select(c => c.Value).ToList();\n",[19,810,811,816,821,826,830,835,840,845,850,854,859,864,869,873,878,883,888,892,897,902,907],{"__ignoreMap":43},[148,812,813],{"class":150,"line":151},[148,814,815],{},"\u002F\u002F Default: JWT \"sub\" → ClaimTypes.NameIdentifier (long URI)\n",[148,817,818],{"class":150,"line":157},[148,819,820],{},"\u002F\u002F Reading after default mapping:\n",[148,822,823],{"class":150,"line":163},[148,824,825],{},"var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); \u002F\u002F works\n",[148,827,828],{"class":150,"line":170},[148,829,167],{"emptyLinePlaceholder":166},[148,831,832],{"class":150,"line":176},[148,833,834],{},"\u002F\u002F Disable mapping to use short JWT names directly:\n",[148,836,837],{"class":150,"line":182},[148,838,839],{},"JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();\n",[148,841,842],{"class":150,"line":188},[148,843,844],{},"\u002F\u002F OR per handler (.NET 6+):\n",[148,846,847],{"class":150,"line":194},[148,848,849],{},"options.MapInboundClaims = false;\n",[148,851,852],{"class":150,"line":200},[148,853,167],{"emptyLinePlaceholder":166},[148,855,856],{"class":150,"line":206},[148,857,858],{},"\u002F\u002F Now read by short name:\n",[148,860,861],{"class":150,"line":212},[148,862,863],{},"var userId = User.FindFirstValue(\"sub\");\n",[148,865,866],{"class":150,"line":218},[148,867,868],{},"var email  = User.FindFirstValue(\"email\");\n",[148,870,871],{"class":150,"line":223},[148,872,167],{"emptyLinePlaceholder":166},[148,874,875],{"class":150,"line":229},[148,876,877],{},"\u002F\u002F Tell the framework which claim is the \"name\" and which is \"role\":\n",[148,879,880],{"class":150,"line":235},[148,881,882],{},"options.TokenValidationParameters.NameClaimType = JwtRegisteredClaimNames.Sub;\n",[148,884,885],{"class":150,"line":240},[148,886,887],{},"options.TokenValidationParameters.RoleClaimType = \"role\";\n",[148,889,890],{"class":150,"line":246},[148,891,167],{"emptyLinePlaceholder":166},[148,893,894],{"class":150,"line":252},[148,895,896],{},"\u002F\u002F Role arrays — the handler splits them automatically:\n",[148,898,899],{"class":150,"line":257},[148,900,901],{},"\u002F\u002F Payload: { \"role\": [\"Admin\", \"Editor\"] }\n",[148,903,904],{"class":150,"line":263},[148,905,906],{},"\u002F\u002F → two separate ClaimTypes.Role claims in ClaimsPrincipal\n",[148,908,909],{"class":150,"line":269},[148,910,911],{},"var roles = User.FindAll(\"role\").Select(c => c.Value).ToList();\n",[10,913,915],{"id":914},"refresh-tokens-and-token-rotation","Refresh tokens and token rotation",[15,917,918,919,922],{},"Access tokens are short-lived. When they expire, the client uses a long-lived ",[122,920,921],{},"refresh token","\nto get a new access token without prompting the user again.",[35,924,926],{"className":142,"code":925,"language":144,"meta":43,"style":43},"\u002F\u002F Login — return both tokens:\n[HttpPost(\"login\")]\npublic async Task\u003CIActionResult> Login([FromBody] LoginDto dto)\n{\n    var user = await _userService.ValidateAsync(dto.Email, dto.Password);\n    if (user is null) return Unauthorized();\n\n    var accessToken  = _tokenService.GenerateAccessToken(user);\n    var refreshToken = await _refreshStore.CreateAsync(user.Id);\n    \u002F\u002F refreshToken = random opaque string stored in DB with 30-day expiry\n\n    return Ok(new { accessToken, refreshToken, expiresIn = 900 });\n}\n\n\u002F\u002F Refresh — rotate the refresh token on every use:\n[HttpPost(\"refresh\")]\npublic async Task\u003CIActionResult> Refresh([FromBody] RefreshDto dto)\n{\n    var stored = await _refreshStore.GetAsync(dto.RefreshToken);\n    if (stored is null || stored.IsRevoked || stored.ExpiresAt \u003C DateTime.UtcNow)\n        return Unauthorized(\"Invalid or expired refresh token\");\n\n    \u002F\u002F Rotate — invalidate the used token, issue a new one:\n    await _refreshStore.RevokeAsync(dto.RefreshToken);\n\n    var user         = await _userService.GetByIdAsync(stored.UserId);\n    var accessToken  = _tokenService.GenerateAccessToken(user!);\n    var newRefresh   = await _refreshStore.CreateAsync(user!.Id);\n\n    return Ok(new { accessToken, refreshToken = newRefresh, expiresIn = 900 });\n}\n",[19,927,928,933,937,941,945,949,953,957,962,967,972,976,981,985,989,994,999,1004,1008,1013,1018,1023,1027,1032,1037,1041,1046,1051,1056,1060,1065],{"__ignoreMap":43},[148,929,930],{"class":150,"line":151},[148,931,932],{},"\u002F\u002F Login — return both tokens:\n",[148,934,935],{"class":150,"line":157},[148,936,604],{},[148,938,939],{"class":150,"line":163},[148,940,610],{},[148,942,943],{"class":150,"line":170},[148,944,461],{},[148,946,947],{"class":150,"line":176},[148,948,621],{},[148,950,951],{"class":150,"line":182},[148,952,627],{},[148,954,955],{"class":150,"line":188},[148,956,167],{"emptyLinePlaceholder":166},[148,958,959],{"class":150,"line":194},[148,960,961],{},"    var accessToken  = _tokenService.GenerateAccessToken(user);\n",[148,963,964],{"class":150,"line":200},[148,965,966],{},"    var refreshToken = await _refreshStore.CreateAsync(user.Id);\n",[148,968,969],{"class":150,"line":206},[148,970,971],{},"    \u002F\u002F refreshToken = random opaque string stored in DB with 30-day expiry\n",[148,973,974],{"class":150,"line":212},[148,975,167],{"emptyLinePlaceholder":166},[148,977,978],{"class":150,"line":218},[148,979,980],{},"    return Ok(new { accessToken, refreshToken, expiresIn = 900 });\n",[148,982,983],{"class":150,"line":223},[148,984,589],{},[148,986,987],{"class":150,"line":229},[148,988,167],{"emptyLinePlaceholder":166},[148,990,991],{"class":150,"line":235},[148,992,993],{},"\u002F\u002F Refresh — rotate the refresh token on every use:\n",[148,995,996],{"class":150,"line":240},[148,997,998],{},"[HttpPost(\"refresh\")]\n",[148,1000,1001],{"class":150,"line":246},[148,1002,1003],{},"public async Task\u003CIActionResult> Refresh([FromBody] RefreshDto dto)\n",[148,1005,1006],{"class":150,"line":252},[148,1007,461],{},[148,1009,1010],{"class":150,"line":257},[148,1011,1012],{},"    var stored = await _refreshStore.GetAsync(dto.RefreshToken);\n",[148,1014,1015],{"class":150,"line":263},[148,1016,1017],{},"    if (stored is null || stored.IsRevoked || stored.ExpiresAt \u003C DateTime.UtcNow)\n",[148,1019,1020],{"class":150,"line":269},[148,1021,1022],{},"        return Unauthorized(\"Invalid or expired refresh token\");\n",[148,1024,1025],{"class":150,"line":274},[148,1026,167],{"emptyLinePlaceholder":166},[148,1028,1029],{"class":150,"line":280},[148,1030,1031],{},"    \u002F\u002F Rotate — invalidate the used token, issue a new one:\n",[148,1033,1034],{"class":150,"line":286},[148,1035,1036],{},"    await _refreshStore.RevokeAsync(dto.RefreshToken);\n",[148,1038,1039],{"class":150,"line":292},[148,1040,167],{"emptyLinePlaceholder":166},[148,1042,1043],{"class":150,"line":298},[148,1044,1045],{},"    var user         = await _userService.GetByIdAsync(stored.UserId);\n",[148,1047,1048],{"class":150,"line":303},[148,1049,1050],{},"    var accessToken  = _tokenService.GenerateAccessToken(user!);\n",[148,1052,1053],{"class":150,"line":309},[148,1054,1055],{},"    var newRefresh   = await _refreshStore.CreateAsync(user!.Id);\n",[148,1057,1058],{"class":150,"line":314},[148,1059,167],{"emptyLinePlaceholder":166},[148,1061,1062],{"class":150,"line":320},[148,1063,1064],{},"    return Ok(new { accessToken, refreshToken = newRefresh, expiresIn = 900 });\n",[148,1066,1067],{"class":150,"line":326},[148,1068,589],{},[15,1070,1071,1074],{},[122,1072,1073],{},"Token rotation"," is critical for detecting theft: if an attacker uses a stolen refresh token\nbefore the legitimate client does, the legitimate client's next refresh attempt fails — alerting\nyou that the token was compromised.",[10,1076,1078],{"id":1077},"handling-expiration","Handling expiration",[35,1080,1082],{"className":142,"code":1081,"language":144,"meta":43,"style":43},"\u002F\u002F Server-side — the middleware rejects expired tokens automatically with 401:\n\u002F\u002F WWW-Authenticate: Bearer error=\"invalid_token\", error_description=\"The token is expired\"\n\n\u002F\u002F Customize the 401 response body:\noptions.Events = new JwtBearerEvents\n{\n    OnChallenge = ctx =>\n    {\n        ctx.HandleResponse();\n        ctx.Response.StatusCode  = 401;\n        ctx.Response.ContentType = \"application\u002Fjson\";\n        return ctx.Response.WriteAsJsonAsync(new\n        {\n            error   = \"token_expired\",\n            message = \"Access token expired. Use the refresh endpoint.\",\n        });\n    },\n};\n\n\u002F\u002F Client-side — proactively refresh before expiry to avoid a failed request:\nvar handler  = new JwtSecurityTokenHandler();\nvar jwtToken = handler.ReadJwtToken(accessToken);\nif (jwtToken.ValidTo \u003C DateTime.UtcNow.AddSeconds(30))\n{\n    accessToken = await _authClient.RefreshAsync(refreshToken);\n}\n",[19,1083,1084,1089,1094,1098,1103,1108,1112,1117,1121,1126,1131,1136,1141,1145,1150,1155,1160,1165,1170,1174,1179,1184,1189,1194,1198,1203],{"__ignoreMap":43},[148,1085,1086],{"class":150,"line":151},[148,1087,1088],{},"\u002F\u002F Server-side — the middleware rejects expired tokens automatically with 401:\n",[148,1090,1091],{"class":150,"line":157},[148,1092,1093],{},"\u002F\u002F WWW-Authenticate: Bearer error=\"invalid_token\", error_description=\"The token is expired\"\n",[148,1095,1096],{"class":150,"line":163},[148,1097,167],{"emptyLinePlaceholder":166},[148,1099,1100],{"class":150,"line":170},[148,1101,1102],{},"\u002F\u002F Customize the 401 response body:\n",[148,1104,1105],{"class":150,"line":176},[148,1106,1107],{},"options.Events = new JwtBearerEvents\n",[148,1109,1110],{"class":150,"line":182},[148,1111,461],{},[148,1113,1114],{"class":150,"line":188},[148,1115,1116],{},"    OnChallenge = ctx =>\n",[148,1118,1119],{"class":150,"line":194},[148,1120,191],{},[148,1122,1123],{"class":150,"line":200},[148,1124,1125],{},"        ctx.HandleResponse();\n",[148,1127,1128],{"class":150,"line":206},[148,1129,1130],{},"        ctx.Response.StatusCode  = 401;\n",[148,1132,1133],{"class":150,"line":212},[148,1134,1135],{},"        ctx.Response.ContentType = \"application\u002Fjson\";\n",[148,1137,1138],{"class":150,"line":218},[148,1139,1140],{},"        return ctx.Response.WriteAsJsonAsync(new\n",[148,1142,1143],{"class":150,"line":223},[148,1144,203],{},[148,1146,1147],{"class":150,"line":229},[148,1148,1149],{},"            error   = \"token_expired\",\n",[148,1151,1152],{"class":150,"line":235},[148,1153,1154],{},"            message = \"Access token expired. Use the refresh endpoint.\",\n",[148,1156,1157],{"class":150,"line":240},[148,1158,1159],{},"        });\n",[148,1161,1162],{"class":150,"line":246},[148,1163,1164],{},"    },\n",[148,1166,1167],{"class":150,"line":252},[148,1168,1169],{},"};\n",[148,1171,1172],{"class":150,"line":257},[148,1173,167],{"emptyLinePlaceholder":166},[148,1175,1176],{"class":150,"line":263},[148,1177,1178],{},"\u002F\u002F Client-side — proactively refresh before expiry to avoid a failed request:\n",[148,1180,1181],{"class":150,"line":269},[148,1182,1183],{},"var handler  = new JwtSecurityTokenHandler();\n",[148,1185,1186],{"class":150,"line":274},[148,1187,1188],{},"var jwtToken = handler.ReadJwtToken(accessToken);\n",[148,1190,1191],{"class":150,"line":280},[148,1192,1193],{},"if (jwtToken.ValidTo \u003C DateTime.UtcNow.AddSeconds(30))\n",[148,1195,1196],{"class":150,"line":286},[148,1197,461],{},[148,1199,1200],{"class":150,"line":292},[148,1201,1202],{},"    accessToken = await _authClient.RefreshAsync(refreshToken);\n",[148,1204,1205],{"class":150,"line":298},[148,1206,589],{},[10,1208,1210],{"id":1209},"revoking-jwts","Revoking JWTs",[15,1212,1213],{},"Because JWTs are stateless, revoking one before expiry requires infrastructure. Three patterns:",[15,1215,1216,1219,1220,1222],{},[122,1217,1218],{},"1. Server-side blocklist (Redis\u002Fcache)"," — add the ",[19,1221,108],{}," to a blocklist on logout; check it on\nevery request. Accurate but adds latency per request.",[35,1224,1226],{"className":142,"code":1225,"language":144,"meta":43,"style":43},"options.Events = new JwtBearerEvents\n{\n    OnTokenValidated = async ctx =>\n    {\n        var jti      = ctx.Principal!.FindFirstValue(JwtRegisteredClaimNames.Jti);\n        var blocklist = ctx.HttpContext.RequestServices.GetRequiredService\u003CTokenBlocklist>();\n        if (jti is not null && await blocklist.IsRevokedAsync(jti))\n            ctx.Fail(\"Token revoked\");\n    },\n};\n",[19,1227,1228,1232,1236,1241,1245,1250,1255,1260,1265,1269],{"__ignoreMap":43},[148,1229,1230],{"class":150,"line":151},[148,1231,1107],{},[148,1233,1234],{"class":150,"line":157},[148,1235,461],{},[148,1237,1238],{"class":150,"line":163},[148,1239,1240],{},"    OnTokenValidated = async ctx =>\n",[148,1242,1243],{"class":150,"line":170},[148,1244,191],{},[148,1246,1247],{"class":150,"line":176},[148,1248,1249],{},"        var jti      = ctx.Principal!.FindFirstValue(JwtRegisteredClaimNames.Jti);\n",[148,1251,1252],{"class":150,"line":182},[148,1253,1254],{},"        var blocklist = ctx.HttpContext.RequestServices.GetRequiredService\u003CTokenBlocklist>();\n",[148,1256,1257],{"class":150,"line":188},[148,1258,1259],{},"        if (jti is not null && await blocklist.IsRevokedAsync(jti))\n",[148,1261,1262],{"class":150,"line":194},[148,1263,1264],{},"            ctx.Fail(\"Token revoked\");\n",[148,1266,1267],{"class":150,"line":200},[148,1268,1164],{},[148,1270,1271],{"class":150,"line":206},[148,1272,1169],{},[15,1274,1275,1278],{},[122,1276,1277],{},"2. Short access tokens + refresh token revocation"," — access tokens expire in ≤ 15 minutes; on\nlogout, revoke only the refresh token in the database. Stolen access tokens are usable for at most\n15 minutes — acceptable risk for most apps.",[15,1280,1281,1284],{},[122,1282,1283],{},"3. Reference tokens"," — store an opaque ID in the token; validate against a database on every\nrequest. Full immediate revocation but eliminates the stateless benefit entirely.",[15,1286,1287],{},"For most applications, pattern 2 is the right trade-off.",[10,1289,1291],{"id":1290},"security-vulnerabilities-and-how-to-prevent-them","Security vulnerabilities and how to prevent them",[1293,1294,1296],"h3",{"id":1295},"_1-algnone-attack","1. alg=none attack",[15,1298,1299,1300,1303],{},"An attacker creates a token with ",[19,1301,1302],{},"\"alg\": \"none\""," and no signature. If the server accepts it, any\npayload is trusted.",[15,1305,1306,1309,1310,1313],{},[122,1307,1308],{},"Prevention:"," ",[19,1311,1312],{},"RequireSignedTokens = true"," (the default — never override it).",[1293,1315,1317],{"id":1316},"_2-algorithm-confusion-rs256-hs256","2. Algorithm confusion (RS256 → HS256)",[15,1319,1320],{},"The server's RSA public key is used as the HMAC secret. If the server accepts HS256 when it expects\nRS256, the attacker can forge tokens using the public key (which is, by definition, public).",[15,1322,1323,1325],{},[122,1324,1308],{}," Restrict accepted algorithms explicitly.",[35,1327,1329],{"className":142,"code":1328,"language":144,"meta":43,"style":43},"options.TokenValidationParameters.ValidAlgorithms = new[] { SecurityAlgorithms.RsaSha256 };\n",[19,1330,1331],{"__ignoreMap":43},[148,1332,1333],{"class":150,"line":151},[148,1334,1328],{},[1293,1336,1338],{"id":1337},"_3-cross-api-token-replay","3. Cross-API token replay",[15,1340,1341,1342,1344,1345,33],{},"A token legitimately issued for ",[19,1343,388],{}," is used at ",[19,1346,392],{},[15,1348,1349,1309,1351,1354,1355,1358],{},[122,1350,1308],{},[19,1352,1353],{},"ValidateAudience = true"," with ",[19,1356,1357],{},"ValidAudience"," set to the current service's\naudience identifier.",[1293,1360,1362],{"id":1361},"_4-weak-hmac-secrets","4. Weak HMAC secrets",[15,1364,1365],{},"Short or guessable secrets can be brute-forced offline. An attacker who captures any valid token\ncan run dictionary attacks against the signature.",[15,1367,1368,1370],{},[122,1369,1308],{}," Use a cryptographically random secret of at least 256 bits (32 bytes).",[35,1372,1374],{"className":142,"code":1373,"language":144,"meta":43,"style":43},"var secret = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));\n\u002F\u002F Store in Azure Key Vault \u002F AWS Secrets Manager — not in appsettings.json.\n",[19,1375,1376,1381],{"__ignoreMap":43},[148,1377,1378],{"class":150,"line":151},[148,1379,1380],{},"var secret = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));\n",[148,1382,1383],{"class":150,"line":157},[148,1384,1385],{},"\u002F\u002F Store in Azure Key Vault \u002F AWS Secrets Manager — not in appsettings.json.\n",[1293,1387,1389],{"id":1388},"_5-insecure-client-storage","5. Insecure client storage",[15,1391,1392,1393,1396],{},"Storing JWTs in ",[19,1394,1395],{},"localStorage"," makes them readable by any JavaScript on the page — one XSS\nvulnerability anywhere on the site steals every user's token.",[15,1398,1399,1401,1402,1405,1406,1409],{},[122,1400,1308],{}," Store tokens in ",[19,1403,1404],{},"HttpOnly, Secure, SameSite=Strict"," cookies. JavaScript can't\nread HttpOnly cookies. Use CSRF tokens or ",[19,1407,1408],{},"SameSite=Strict"," to defend against CSRF.",[35,1411,1413],{"className":142,"code":1412,"language":144,"meta":43,"style":43},"Response.Cookies.Append(\"access_token\", token, new CookieOptions\n{\n    HttpOnly = true,\n    Secure   = true,\n    SameSite = SameSiteMode.Strict,\n    Expires  = DateTimeOffset.UtcNow.AddMinutes(15),\n});\n\n\u002F\u002F Read from cookie in the middleware:\noptions.Events = new JwtBearerEvents\n{\n    OnMessageReceived = ctx =>\n    {\n        ctx.Token = ctx.Request.Cookies[\"access_token\"];\n        return Task.CompletedTask;\n    },\n};\n",[19,1414,1415,1420,1424,1429,1434,1439,1444,1449,1453,1458,1462,1466,1471,1475,1480,1485,1489],{"__ignoreMap":43},[148,1416,1417],{"class":150,"line":151},[148,1418,1419],{},"Response.Cookies.Append(\"access_token\", token, new CookieOptions\n",[148,1421,1422],{"class":150,"line":157},[148,1423,461],{},[148,1425,1426],{"class":150,"line":163},[148,1427,1428],{},"    HttpOnly = true,\n",[148,1430,1431],{"class":150,"line":170},[148,1432,1433],{},"    Secure   = true,\n",[148,1435,1436],{"class":150,"line":176},[148,1437,1438],{},"    SameSite = SameSiteMode.Strict,\n",[148,1440,1441],{"class":150,"line":182},[148,1442,1443],{},"    Expires  = DateTimeOffset.UtcNow.AddMinutes(15),\n",[148,1445,1446],{"class":150,"line":188},[148,1447,1448],{},"});\n",[148,1450,1451],{"class":150,"line":194},[148,1452,167],{"emptyLinePlaceholder":166},[148,1454,1455],{"class":150,"line":200},[148,1456,1457],{},"\u002F\u002F Read from cookie in the middleware:\n",[148,1459,1460],{"class":150,"line":206},[148,1461,1107],{},[148,1463,1464],{"class":150,"line":212},[148,1465,461],{},[148,1467,1468],{"class":150,"line":218},[148,1469,1470],{},"    OnMessageReceived = ctx =>\n",[148,1472,1473],{"class":150,"line":223},[148,1474,191],{},[148,1476,1477],{"class":150,"line":229},[148,1478,1479],{},"        ctx.Token = ctx.Request.Cookies[\"access_token\"];\n",[148,1481,1482],{"class":150,"line":235},[148,1483,1484],{},"        return Task.CompletedTask;\n",[148,1486,1487],{"class":150,"line":240},[148,1488,1164],{},[148,1490,1491],{"class":150,"line":246},[148,1492,1169],{},[10,1494,1496],{"id":1495},"recap","Recap",[15,1498,1499,1500,1502,1503,1505,1506,1509,1510,1512,1513,1516,1517,1519,1520,33],{},"JWTs are signed bearer credentials — integrity-guaranteed but not secret. Configure\n",[19,1501,21],{}," with all five validations: issuer, audience, lifetime, signing key, and\n",[19,1504,419],{},". Generate tokens with ",[19,1507,1508],{},"JwtSecurityTokenHandler",", always including ",[19,1511,108],{}," and a\nshort expiry. Use symmetric signing for single-service scenarios, asymmetric for multi-service. Pair\naccess tokens (≤ 15 min) with refresh tokens and rotate the refresh token on every use to detect\ntheft. Store tokens in ",[19,1514,1515],{},"HttpOnly"," cookies on the client to defeat XSS. Never disable\n",[19,1518,419],{},", never skip audience validation, and never store secrets in ",[19,1521,1522],{},"appsettings.json",[1524,1525,1526],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":43,"searchDepth":157,"depth":157,"links":1528},[1529,1530,1531,1532,1533,1534,1535,1536,1537,1538,1539,1546],{"id":12,"depth":157,"text":13},{"id":25,"depth":157,"text":26},{"id":138,"depth":157,"text":139},{"id":347,"depth":157,"text":348},{"id":445,"depth":157,"text":446},{"id":674,"depth":157,"text":675},{"id":801,"depth":157,"text":802},{"id":914,"depth":157,"text":915},{"id":1077,"depth":157,"text":1078},{"id":1209,"depth":157,"text":1210},{"id":1290,"depth":157,"text":1291,"children":1540},[1541,1542,1543,1544,1545],{"id":1295,"depth":163,"text":1296},{"id":1316,"depth":163,"text":1317},{"id":1337,"depth":163,"text":1338},{"id":1361,"depth":163,"text":1362},{"id":1388,"depth":163,"text":1389},{"id":1495,"depth":157,"text":1496},"How JWT authentication works in ASP.NET Core — token structure, validation parameters, generating and rotating tokens, symmetric vs asymmetric signing, and the security mistakes interviewers use to filter candidates.","medium","md",".NET Core","dotnet",{},"\u002Fblog\u002Fdotnet-jwt-tokens","\u002Fdotnet\u002Fsecurity\u002Fjwt-tokens",{"title":5,"description":1547},"blog\u002Fdotnet-jwt-tokens","JWT Tokens","Security","security","2026-06-23","AXYHN7RHSNirEnLJJwRdcJRugYUw3qpHEQ4FRr-cdVU",1782244086381]