Skip to main content
Spring Framework · Topic 22 of 25

JWT

Definition​

A JWT (JSON Web Token) is a signed string with three parts: header, claims and signature. A service checks the signature, expiry, issuer and audience, then trusts the claims. The contents are encoded, not secret.

Why it exists​

Server-side sessions need shared storage that every service can reach. A signed, self-contained token lets any service verify the caller on its own.

How it works​

Structure​

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhbGljZSIsInNjb3BlIjoib3JkZXJzLnJlYWQifQ.<signature>
header payload (claims) signature

Three Base64URL-encoded parts separated by dots:

  1. Header: {"alg":"HS256","typ":"JWT","kid":"key-2025-01"}.
  2. Payload: claims, e.g. sub (subject), iss (issuer), aud (audience), exp (expiry), nbf (not before), iat (issued at), jti (id), plus custom ones (scope, roles).
  3. Signature: sign(base64(header) + "." + base64(payload)) with a key.

The payload is encoded, not encrypted. Anyone holding the token can read it. Never put secrets or sensitive personal data in a JWS. (JWE exists for encrypted tokens but is less common.)

Signing: symmetric vs asymmetric​

HS256 (HMAC-SHA-256)RS256 / ES256 (RSA / ECDSA)
KeysOne shared secret signs and verifiesPrivate key signs, public key verifies
Who can mint tokensAnyone who can verifyOnly the holder of the private key
DistributionSecret must be shared with every verifier (risky across teams/services)Publish public keys via a JWKS endpoint (/.well-known/jwks.json); key rotation via kid
UseSingle service verifying its own tokensIdentity provider issuing tokens to many resource servers

For microservices use asymmetric keys: a compromised resource server then cannot forge tokens. Prefer ES256/RS256 with a JWKS and rotate keys periodically.

Access and refresh tokens​

  • Access token: short-lived (5-15 minutes), sent on every API call. Short life limits damage if stolen.
  • Refresh token: long-lived, used only with the authorisation server to get new access tokens. Stored more safely, never sent to resource servers.
  • Refresh token rotation: each use returns a new refresh token and invalidates the old one; reuse of an old token signals theft and revokes the family.

System design considerations​

  • Keep access tokens short-lived and rotate signing keys
  • Validate issuer, audience and algorithm explicitly
  • Plan revocation (deny list, short TTL, token introspection) up front

What a resource server must validate​

Verify everything, never just decode:

  1. Signature, using the expected algorithm and key chosen by your configuration, not by the token header.
  2. exp (not expired), nbf (valid yet), allowing a small clock skew (Spring defaults to 60 s).
  3. iss: matches the trusted issuer.
  4. aud: the token is meant for this API (prevents using a token issued for service A against service B).
  5. Application checks: scopes/roles, token type.

Spring does 1-3 by default when you set issuer-uri/jwk-set-uri; add an audience validator yourself:

@Bean
JwtDecoder jwtDecoder(@Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}") String issuer) {
NimbusJwtDecoder decoder = JwtDecoders.fromIssuerLocation(issuer);
OAuth2TokenValidator<Jwt> audience = new JwtClaimValidator<List<String>>("aud", aud -> aud != null && aud.contains("orders-api"));
decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(JwtValidators.createDefaultWithIssuer(issuer), audience));
return decoder;
}

I tested a resource server and confirmed that expired tokens, tokens from a different issuer and tokens with a tampered signature are all rejected with 401, valid tokens with the right scope get 200, and a valid token lacking the scope gets 403.

Mapping claims to authorities​

JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
JwtGrantedAuthoritiesConverter scopes = new JwtGrantedAuthoritiesConverter(); // scope/scp -> SCOPE_x
converter.setJwtGrantedAuthoritiesConverter(jwt -> {
Collection<GrantedAuthority> out = new ArrayList<>(scopes.convert(jwt));
List<String> roles = jwt.getClaimAsStringList("roles"); // custom claim
if (roles != null) roles.forEach(r -> out.add(new SimpleGrantedAuthority("ROLE_" + r)));
return out;
});

Identity providers differ in claim shapes (Keycloak nests roles under realm_access.roles), so a converter is almost always needed.

The revocation problem​

A self-contained JWT is valid until exp; you cannot "log out" a stolen token without extra state. Options:

  • Short TTLs + refresh-token revocation (the standard answer).
  • A deny-list of jti/subject checked on each request (reintroduces state/lookups, but small and cacheable).
  • Token introspection (opaque tokens): the resource server asks the auth server each time (centralised control, extra latency; cacheable). Many teams prefer opaque tokens for external clients and JWTs internally.
  • Versioning: store a per-user "tokens valid after" timestamp and compare with iat.

Where to store tokens in a browser​

LocationRisk
localStorage / sessionStorageReadable by any JavaScript: XSS steals the token
HttpOnly, Secure, SameSite cookieNot readable by JS (XSS cannot exfiltrate it), but browsers attach it automatically so you need CSRF protection
In memory (SPA variable) + refresh via HttpOnly cookieGood compromise; survives XSS less well but short-lived

The Backend-for-Frontend (BFF) pattern keeps tokens entirely on the server: the browser has only a session cookie to the BFF, which calls APIs with tokens (recommended by current OAuth security guidance for browser apps).

When to use it, and when not​

Use it when

  • Many services must verify the same identity without a shared session store
  • You issue short-lived access tokens with an identity provider
  • You need standard claims for authorisation

Think twice when

  • You need instant revocation and cannot accept the delay
  • You would store sensitive data in the payload
  • A simple server-side session fits a single web app better

JWT vs server-side sessions​

Server session (cookie + session id)JWT
StateServer-side (store/replicate)Client-side (self-contained)
RevocationImmediateHard (until expiry)
ScalingNeeds sticky/shared session storeStateless verification
SizeTiny cookieLarger per request
Best forServer-rendered web appsAPIs, microservices, third-party access

JWT is not "more secure"; it fits distributed verification. For a classic monolith web app, sessions are often simpler and safer.

Common mistakes​

Common attacks and mistakes​

IssueDefence
alg: none acceptedLibraries must reject unsigned tokens; pin allowed algorithms in configuration
Algorithm confusion (RS256 public key used as an HS256 secret)Never let the token header choose the algorithm; configure it
Weak HMAC secret256+ bits of random key from a secret store, not a dictionary word
Not checking aud/issAlways validate both
Secrets/PII in payloadPayload is readable; keep claims minimal
Long-lived access tokensShort TTL + refresh
Trusting kid/jku/x5u header URLs from the tokenResolve keys only from configured, trusted JWKS URIs
Token in URL query stringsLeaks via logs and referrers; use the Authorization header
No rotationRotate signing keys, publish old and new keys during overlap
  • Treating decode as verify.
  • No audience check; tokens usable across APIs.
  • Long-lived access tokens, no refresh rotation.
  • Sensitive data in claims.
  • Using a weak or shared HMAC secret across many services.
  • Storing tokens in localStorage without XSS defences.
  • Putting authorisation decisions only in the token, never rechecking resource ownership.

Interview questions​

1. What is a JWT and what are its parts?

A signed, base64url-encoded JSON token with header, payload (claims) and signature, verifiable without server-side state.

2. Is a JWT encrypted?

No, a standard signed JWT is only encoded; anyone can read the claims. Do not store secrets in it.

3. HS256 vs RS256?

HS256 shares one secret for signing and verifying; RS256 uses a private key to sign and a public key to verify, which is better when many services validate tokens and only the issuer should mint them.

4. What must you validate on a received JWT?

Signature with a configured algorithm/key, exp, nbf, iss, aud, then authorisation claims.

5. How do you invalidate a JWT?

You can't directly; use short expiry with refresh-token revocation, deny-lists by jti, per-user tokens valid after timestamps, or opaque tokens with introspection.

6. Where should a SPA store the token?

Preferably not in localStorage; use HttpOnly SameSite cookies (with CSRF protection) or the BFF pattern, keeping access tokens short-lived.

7. What is the alg: none attack?

Forging a token with the header alg: none and no signature; vulnerable libraries accept it. Prevent by enforcing allowed algorithms server-side.

8. What are access vs refresh tokens?

Access tokens are short-lived credentials for APIs; refresh tokens are long-lived credentials used only to obtain new access tokens, ideally rotated on each use.

9. How do microservices share JWT verification keys?

Via the identity provider's JWKS endpoint, cached with refresh on unknown kid, so key rotation needs no redeploy.

Check yourself​

Quick quiz3 questions

1A JWT is signed. Does that hide its contents?

2Which checks must a resource server perform?

3Why is revoking a JWT hard?

Key takeaways​

  • JWTs are signed, not encrypted; validate signature, expiry, issuer and audience.
  • Prefer asymmetric signing with a JWKS for multi-service setups.
  • Short access tokens plus rotated refresh tokens; plan for revocation.
  • Keep tokens out of JavaScript-readable storage where possible.

Next: OAuth2 and OIDC.

Next topicOAuth2 and OIDC

Finished this page? Track your progress (saved only in your browser).