// Security · 03
JSON Web Tokens
Signed, self-contained claims the server can verify without a lookup.
How it fits together
- Identity provider creates Client app (1 issue JWT)
- Client app uses / calls Orders API (2 Bearer token)
- Forged token fails / error path Orders API (3 rejected 401)
Spot it
- Token still accepted after the user was disabled
- Token from one service accepted by another
- Secrets or personal data visible when decoded
It costs
Verification is cheap and stateless, but a token stays valid until it expires.
In code
using Microsoft.IdentityModel.Tokens;public static class AccessTokenRules{ public static TokenValidationParameters For(SecurityKey issuerKey) => new() { ValidateIssuerSigningKey = true, IssuerSigningKey = issuerKey, ValidAlgorithms = [SecurityAlgorithms.RsaSha256], ValidateIssuer = true, ValidIssuer = "https://login.example.com/", ValidateAudience = true, ValidAudience = "orders-api", ValidateLifetime = true, RequireExpirationTime = true, ClockSkew = TimeSpan.FromSeconds(60), };}Go deeperThe full explanation and what to ask for · 1 min readExpandCollapse
A JWT is three base64url parts: a header (algorithm and key id), a payload (claims such as sub, iss, aud and exp) and a signature over the first two. The payload is signed, not encrypted, so anyone holding the token can read it. Never put secrets or personal data in claims. Encrypted tokens (JWE) exist, but they are rarely the right answer.
Validation is the security. Verify the signature with a key you trust, taken from the issuer published key set and never from the token itself. Pin the expected algorithm, so a token cannot choose its own check (alg none, or HS256 keyed with a public key). Then check iss, aud, exp and nbf. Checking only the signature is the common mistake: a valid token issued for a different API passes it.
Revocation is the cost. A self-contained token cannot be taken back before exp, so keep access tokens short, minutes not hours, and put long-lived trust in a refresh token the server can revoke. If you need instant revocation, check a deny-list or session state on each call, which gives back some of the statelessness. When NOT to use JWTs: a session cookie on a single backend is simpler and revocable at once.
Ask for it
- header.payload.signature
- alg and kid
- iss, aud, exp, nbf
- clock skew
- refresh token
- JWS vs JWE
Validate access tokens on every API call: accept only RS256 signatures from the issuer keys (matched by kid), require iss https://login.example.com/ and aud orders-api, check exp and nbf with 60s skew, reject alg none. Lifetime 10 min; refresh tokens rotate and can be revoked. No secrets in claims.