JWT authentication end to end
A stateless REST API — one consumed by a mobile app or a separate frontend — usually authenticates with a JWT (JSON Web Token): the client logs in once, receives a signed token, and sends it on every subsequent request instead of a username and password. This lesson explains what a JWT is, the end-to-end flow, and how it fits Spring Security's filter chain — with a clear-eyed account of the trade-offs, because JWT is both widely used and widely misused.
What a JWT is
A JWT is a compact, signed token carrying claims about the user. It has three parts, separated by dots:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzdGFmZiIsInJvbGUiOiJTVEFGRiIsImV4cCI6MTcwMH0.<signature>
header payload (claims) signature
- Header — the signing algorithm.
- Payload (claims) — data about the user: a
sub(subject, e.g. the username), roles, anexp(expiry), and whatever else you put in. This is only Base64-encoded, not encrypted — anyone can read it. Never put secrets in a JWT payload. - Signature — the header and payload signed with a secret (or private key). This is the crucial part: it lets the server verify the token was issued by it and not tampered with, without storing anything.
The key property: a JWT is self-contained and verifiable. The server does not look the token up in a database; it verifies the signature with its secret. If the signature is valid, the claims are trustworthy (the server signed them); if anyone alters a claim, the signature no longer matches and the token is rejected. That is what makes JWT stateless — no server-side session storage.
The end-to-end flow
- Login. The client sends username and password once to a login endpoint. The server authenticates them
(BCrypt
matches, previous lesson) and, on success, issues a JWT — signing a token with the user's identity and roles as claims and an expiry. - Store. The client stores the token (in a mobile app's secure storage; for a browser, carefully — see the mistakes lesson).
- Send on every request. The client sends the token in the
Authorizationheader:Authorization: Bearer <token>. - Verify per request. On each request, a Spring Security filter reads the token, verifies the
signature and expiry, and if valid, populates the
SecurityContextwith the user and roles — so the rest of the request is authenticated. No database lookup, no session. - Expire and refresh. The token expires (short-lived — minutes to an hour). Many designs add a long-lived refresh token to obtain a new access token without re-logging-in.
The whole point: after login, the server is stateless — it authenticates each request purely by verifying the token's signature, storing nothing between requests.
Fitting it into Spring Security
JWT slots into the filter chain from the security-basics lesson. You configure the chain to be stateless and add a JWT filter that runs before the standard authentication filter:
http
.csrf(c -> c.disable()) // stateless token API — no CSRF cookie
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(a -> a
.requestMatchers("/api/auth/**").permitAll() // login/refresh are public
.anyRequest().authenticated())
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
SessionCreationPolicy.STATELESS tells Spring Security not to create or use an HTTP session — each request
stands alone, authenticated by its token. The jwtAuthFilter (you write it, or use a library) does step 4:
extract the Bearer token, verify it, and set the SecurityContext. For issuing and verifying tokens, use
a well-tested library — jjwt or Nimbus (Spring's own spring-boot-starter-oauth2-resource-server
supports JWT verification with little code) — rather than hand-rolling the crypto. This is the standard
setup: stateless session policy, a JWT filter, public auth endpoints, everything else authenticated.
The honest trade-offs: JWT is not free
JWT is popular, and often reached for reflexively. Be clear about the trade-offs, because its main weakness is real:
- You cannot easily revoke a JWT. Because the server stores nothing and trusts the signature, a valid, unexpired token works until it expires — even if the user logs out, is banned, or the token is stolen. There is no server-side "session" to delete. Mitigations (short expiry + refresh tokens, a server-side denylist of revoked tokens) exist, but a denylist reintroduces the server-side state JWT was meant to avoid. This is JWT's fundamental cost.
- The payload is readable. Base64, not encrypted — never put anything sensitive in it.
- A session cookie is often simpler. For a same-site frontend, a plain server session (a cookie) is simpler, revocable (delete the session), and perfectly fine — you do not need JWT. JWT earns its place for stateless, cross-client APIs (mobile apps, third-party consumers, microservices) where server-side sessions are awkward.
The honest guidance: use JWT when you genuinely need stateless authentication across clients you do not control; for a same-site web app, a session cookie is simpler and revocable. Do not choose JWT because it is fashionable — choose it when statelessness is worth losing easy revocation. Knowing why you chose it is what separates using JWT well from cargo-culting it.
Check your work
What a JWT is. A compact, signed token of three parts: header (algorithm), payload (readable Base64
claims — sub, roles, exp; no secrets), signature (verifies issuer + integrity). Self-contained and
verified by signature, not a database lookup — hence stateless.
The flow. Login → server authenticates and issues a signed token → client stores it → sends
Authorization: Bearer <token> each request → a filter verifies signature+expiry and sets the
SecurityContext → token expires (short-lived), refresh token for renewal.
In Spring Security. SessionCreationPolicy.STATELESS, a JWT filter added before the auth filter, public
auth endpoints, everything else authenticated; use a library (jjwt/Nimbus/oauth2-resource-server), do not
hand-roll crypto.
The trade-offs. Cannot easily revoke (valid until expiry — mitigate with short expiry/refresh/denylist, which adds back state); payload is readable (no secrets); a session cookie is simpler and revocable for a same-site app. Use JWT for stateless cross-client APIs, deliberately.
Practice
- Decode a sample JWT (paste it into jwt.io or Base64-decode the parts) and read the header and payload; confirm the payload is plainly readable — and reflect on why no secrets go there.
- Wire a stateless
SecurityFilterChain(STATELESS, public/api/auth/**, everything else authenticated) and a JWT filter (using a library); log in, get a token, and call a protected endpoint with theBearerheader. - Tamper with one character of a token's payload and confirm the request is rejected (signature no longer matches).
- Let a short-lived token expire and confirm the next request is rejected; obtain a new one via a refresh token.
- Reason about how you would revoke a token if a user is banned — and why that reintroduces server-side state.
- For Dakiya's mobile-app clients vs a hypothetical same-site staff dashboard, decide JWT vs session cookie and justify each.
Official documentation
- Spring Security — OAuth2 Resource Server (JWT) — Verifying JWTs with little code.
- jjwt library — Issuing and verifying JWTs in Java.
- Spring Security — Session management — Stateless policy and sessions.
Next: roles, authorities and method security.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship