Skip to content

// Security · 03

JSON Web Tokens

Login succeedsleads toBearer headerSigned tokenleads toAPI verifies it

Signed, self-contained claims the server can verify without a lookup.

How it fits together

JSON Web Tokens structure«external»Identity providersigns with private key«client»Client appholds access tokenOrders APIverify signaturecheck iss, aud, expForged tokenalg none or edited claims1 issue JWT2 Bearer token3 rejected 401
createsuses / callsfails / error path· numbered steps replay in order
  • 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 readExpand

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.