RizTech Academy logo
RizTech Academy
SecurityLesson 3 of 545 min

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, an exp (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

  1. 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.
  2. Store. The client stores the token (in a mobile app's secure storage; for a browser, carefully — see the mistakes lesson).
  3. Send on every request. The client sends the token in the Authorization header: Authorization: Bearer <token>.
  4. Verify per request. On each request, a Spring Security filter reads the token, verifies the signature and expiry, and if valid, populates the SecurityContext with the user and roles — so the rest of the request is authenticated. No database lookup, no session.
  5. 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

  1. 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.
  2. 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 the Bearer header.
  3. Tamper with one character of a token's payload and confirm the request is rejected (signature no longer matches).
  4. Let a short-lived token expire and confirm the next request is rejected; obtain a new one via a refresh token.
  5. Reason about how you would revoke a token if a user is banned — and why that reintroduces server-side state.
  6. For Dakiya's mobile-app clients vs a hypothetical same-site staff dashboard, decide JWT vs session cookie and justify each.

Official documentation

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