JWT Authentication
Reviewed & published by Brayan K
A JSON Web Token is a signed, self-contained credential of the form header.payload.signature that lets a server authenticate every request statelessly — no session storage required.
Part of the free Java course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
Before You Start
You should know the basics of Spring Security — authentication, authorization, and the filter chain. A grasp of HTTP headers and Base64 encoding will help.
What You'll Learn
- The three parts of a JWT (header.payload.signature)
- Why the payload is encoded, not encrypted
- How the HMAC signature proves integrity
- Stateless authentication and why it scales
- Sending tokens as Authorization: Bearer ...
- Validating tokens in a Spring Security filter
The Big Idea — A Tamper-Proof Festival Wristband
💡 Analogy: A JWT is like a festival wristband. When you check in, staff give you a wristband printed with your name and access level (the payload) and seal it with a special tamper-evident strip (the signature). After that, gate staff don't look you up in any database — they just glance at the wristband, confirm the seal is intact, and wave you through. That is stateless auth: the credential carries everything. But note: anyone can read what's printed on it, so it holds no secrets — and whoever physically holds the band gets in, which is why you don't lose it and why it expires.
Self-contained, verifiable by anyone with the key, readable by everyone — exactly a signed JWT.
1️⃣ The Structure — header.payload.signature
Three dot-separated Base64URL segments. The first two are just encoded JSON — let's decode them to prove the payload is not secret.
import java.util.Base64;
public class Main {
// A JWT is THREE Base64URL parts joined by dots: header.payload.signature.
// The header and payload are just ENCODED (not encrypted) JSON — let's split
// a sample token and decode the first two parts to prove anyone can read them.
public static void main(String[] args) {
// header={"alg":"HS256","typ":"JWT"} payload={"sub":"alice","role":"ADMIN"}
String jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
+ ".eyJzdWIiOiJhbGljZSIsInJvbGUiOiJBRE1JTiJ9"
+ ".s1gnatureBytesGoHere";
String[] parts = jwt.split("\\.");
System.out.println("segments: " + parts.length);
Base64.Decoder dec = Base64.getUrlDecoder();
String header = new String(dec.decode(parts[0]));
String payload = new String(dec.decode(parts[1]));
System.out.println("header : " + header);
System.out.println("payload: " + payload);
System.out.println("payload was secret? false (only Base64-encoded)");
}
}2️⃣ The Signature — Proving Integrity
The signature is an HMAC over header.payload using a secret key (HS256 = HMAC-SHA256). Recompute it and you can detect any tampering — change one byte of the payload and verification fails.
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
public class Main {
// The SIGNATURE is an HMAC over "header.payload" using a secret key. It
// proves the token was not tampered with. We compute it here to show the
// exact idea Spring/JJWT use under the hood (HS256 = HMAC-SHA256).
static String b64url(byte[] b) {
return Base64.getUrlEncoder().withoutPadding().encodeToString(b);
}
static String sign(String signingInput, String secret) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA256"));
return b64url(mac.doFinal(signingInput.getBytes("UTF-8")));
}
public static void main(String[] args) throws Exception {
String header = b64url("{\"alg\":\"HS256\",\"typ\":\"JWT\"}".getBytes("UTF-8"));
String payload = b64url("{\"sub\":\"alice\"}".getBytes("UTF-8"));
String secret = "super-secret-signing-key";
String input = header + "." + payload;
String sig = sign(input, secret);
String token = input + "." + sig;
System.out.println("token has 3 parts? " + (token.split("\\.").length == 3));
// Verify: recompute the signature and compare.
boolean valid = sign(input, secret).equals(sig);
boolean tampered = sign(header + "." + b64url("{\"sub\":\"mallory\"}".getBytes("UTF-8")), secret).equals(sig);
System.out.println("genuine token verifies? " + valid);
System.out.println("tampered payload verifies? " + tampered);
}
}3️⃣ Validating Tokens in Spring Security
In Spring you add a custom OncePerRequestFilter to the chain. It reads the Authorization: Bearer ... header, validates the token, and populates the SecurityContext — with sessions set to STATELESS.
// client sends on every request:
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhbGljZSJ9.<sig>// SPRING SECURITY: validating a JWT on every request (needs Spring + a JWT
// library such as JJWT). The filter runs ONCE per request, extracts the Bearer
// token, validates it, and sets the Authentication — making auth STATELESS.
public class JwtAuthFilter extends OncePerRequestFilter {
private final JwtService jwt; // wraps signing/parsing with the secret key
public JwtAuthFilter(JwtService jwt) { this.jwt = jwt; }
@Override
protected void doFilterInternal(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain)
throws ServletException, IOException {
String header = req.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
String token = header.substring(7); // strip "Bearer "
if (jwt.isValid(token)) { // checks signature + exp
String username = jwt.getSubject(token); // the "sub" claim
var auth = new UsernamePasswordAuthenticationToken(
username, null, jwt.getAuthorities(token));
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
chain.doFilter(req, res); // continue the chain
}
}
// Registered in the SecurityFilterChain BEFORE the username/password filter:
// http.sessionManagement(s -> s.sessionCreationPolicy(STATELESS))
// .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);🧠 Quick Recall
Predict before you peek.
Answer: header, payload, and signature, joined by dots and Base64URL-encoded.
Answer: yes — the payload is only encoded, not encrypted, so anyone can decode it. Keep secrets out of it.
Answer: in the HTTP header Authorization: Bearer <token>.
🎯 YOUR TURN — Validate Token Shape
Write isWellFormed(token) that returns true only for exactly three non-empty dot-separated parts.
public class Main {
// YOUR TURN: split a JWT and report whether it is well-formed.
// A well-formed JWT has exactly THREE non-empty dot-separated parts.
// Write isWellFormed(String token) returning true/false.
//
// Expected output:
// eyJ.eyJ.sig -> true
// eyJ.eyJ -> false
// static boolean isWellFormed(String token) { ... }
public static void main(String[] args) {
// System.out.println("eyJ.eyJ.sig -> " + isWellFormed("eyJ.eyJ.sig"));
// System.out.println("eyJ.eyJ -> " + isWellFormed("eyJ.eyJ"));
}
}🧩 MINI-CHALLENGE — Decode and Check Expiry
Base64URL-decode a payload, read its exp, and print ACTIVE or EXPIRED for two different "now" values.
import java.util.Base64;
import java.util.Map;
public class Main {
// MINI-CHALLENGE: decode a JWT payload and check expiry.
// Given the payload part of a token (Base64URL JSON with an "exp" Unix
// second), decode it, parse out exp, and decide if it is expired relative
// to a supplied "now". Print ACTIVE or EXPIRED.
//
// Expected output:
// token at now=1000 -> ACTIVE (exp=2000)
// token at now=3000 -> EXPIRED
public static void main(String[] args) {
// payload = {"sub":"alice","exp":2000}
String payloadB64 = Base64.getUrlEncoder().withoutPadding()
.encodeToString("{\"sub\":\"alice\",\"exp\":2000}".getBytes());
// 1. decode payloadB64 to a String
// 2. extract the exp value (simple parse is fine)
// 3. compare with now (1000, then 3000) and print ACTIVE / EXPIRED
}
}Common Errors
- ❌ Putting secrets in the payload. It's only encoded, so anyone reads it. Fix: store only non-sensitive claims.
- ❌ Not verifying the signature. Trusting decoded claims invites forgery. Fix: always validate before trusting.
- ❌ Ignoring exp. Expired tokens get accepted forever. Fix: reject tokens past their expiry.
- ❌ Forgetting STATELESS session policy. Spring may still create sessions. Fix: set SessionCreationPolicy.STATELESS.
- ❌ Long-lived access tokens. A stolen token works for ages. Fix: short expiry + refresh tokens.
- ❌ Sending tokens over plain HTTP. They can be sniffed. Fix: require HTTPS.
Pro Tips
- 💡 Use a vetted library (e.g. JJWT or Spring's resource-server support) rather than hand-rolling JWT parsing.
- 💡 Validate issuer and audience claims too, not just the signature, to limit token reuse.
- 💡 Keep access tokens short and pair them with refresh tokens for a good UX/security balance.
- 💡 Add the JWT filter before UsernamePasswordAuthenticationFilter so authentication is set early.
📋 Quick Reference
| Concept | Syntax / idea | Note |
|---|---|---|
| structure | header.payload.signature | 3 Base64URL parts |
| header | alg, typ | e.g. HS256 |
| payload | claims: sub, exp, iat | Encoded, not secret |
| signature | HMAC over header.payload | Integrity check |
| subject | "sub" | The user |
| expiry | "exp" | Unix seconds |
| transport | Authorization: Bearer ... | Per request |
| stateless | no server session | Scales well |
| spring filter | OncePerRequestFilter | Validates token |
| policy | STATELESS | No HTTP session |
📚 Resources / Keep Learning
- 📘 RFC 7519 — JSON Web Token (JWT)
- 📗 JWT support — Spring Security docs
- 📗 Introduction to JSON Web Tokens (jwt.io)
🎉 Lesson Complete!
You now understand a JWT's header.payload.signature structure, why the payload is readable but tamper-evident, how stateless authentication works, and how a Spring Security filter validates a Bearer token on every request.
Practice quiz
How many parts does a JWT have, and how are they separated?
- Three parts joined by dots: header.payload.signature
- Two parts joined by a colon
- One Base64 string
- Four parts joined by slashes
Answer: Three parts joined by dots: header.payload.signature. A JWT is header.payload.signature — three Base64URL segments separated by periods.
What does the JWT signature provide?
- Encryption of the payload
- Integrity — proof the token was not tampered with and came from a trusted issuer
- Compression
- The user's password
Answer: Integrity — proof the token was not tampered with and came from a trusted issuer. The signature lets the server verify the token's integrity and authenticity; a signed JWT is not encrypted by default.
Is the payload of a standard (signed, not encrypted) JWT secret?
- Yes, it is encrypted
- Yes, only the server can read it
- No, it is only Base64URL-encoded and anyone can read it
- It is hashed irreversibly
Answer: No, it is only Base64URL-encoded and anyone can read it. A signed JWT's claims are merely Base64URL-encoded, so never put secrets in the payload — anyone can decode it.
What does 'stateless authentication' mean with JWTs?
- The server stores every session in memory
- Tokens never expire
- The client stores nothing
- The server keeps no session; the self-contained token carries the identity each request
Answer: The server keeps no session; the self-contained token carries the identity each request. The token itself holds the claims, so the server needn't store server-side session state — it just verifies the signature.
How is a JWT typically sent on each request?
- As a URL query parameter named jwt
- In the Authorization header as 'Bearer <token>'
- In a cookie named PHPSESSID only
- In the request body as XML
Answer: In the Authorization header as 'Bearer <token>'. The convention is the HTTP header: Authorization: Bearer eyJ... so a filter can extract and validate it.
In Spring Security, where do you validate the JWT on incoming requests?
- In the controller method body
- In application.properties
- In a custom filter added to the SecurityFilterChain (e.g. a OncePerRequestFilter)
- In the database trigger
Answer: In a custom filter added to the SecurityFilterChain (e.g. a OncePerRequestFilter). A custom authentication filter reads the Bearer token, validates it, and sets the Authentication in the SecurityContext.
Which JWT claim conventionally marks the subject (the user)?
- sub
- alg
- color
- ttl
Answer: sub. The 'sub' (subject) registered claim identifies the principal; 'exp' is expiry, 'iat' is issued-at.
Which claim makes a token expire?
- sub
- iss
- exp
- jti
Answer: exp. 'exp' (expiration time) is a Unix timestamp after which the token must be rejected as expired.
A key advantage of stateless JWT auth for scaling is...
- Tokens are encrypted by default
- It removes the need for HTTPS
- Any server instance can verify a request without a shared session store
- Tokens can never be stolen
Answer: Any server instance can verify a request without a shared session store. Because verification needs only the signing key, requests can hit any node without sticky sessions or a central session DB.
Why should JWTs be sent only over HTTPS and kept short-lived?
- To make them longer
- Because a stolen Bearer token grants access until it expires
- To enable Base64 encoding
- Because HTTP is faster
Answer: Because a stolen Bearer token grants access until it expires. A signed token is a bearer credential: whoever holds it is trusted, so transport encryption and short expiry limit the damage of theft.
Continue this course
- Previous: Spring Security Basics
- Next: Testing with @SpringBootTest & MockMvc — @SpringBootTest, @WebMvcTest, MockMvc and @MockBean
- Quick reference: Java cheat sheet
Frequently asked questions
What are the three parts of a JWT?
A JSON Web Token is three Base64URL-encoded segments joined by periods: header.payload.signature. The header declares the token type and signing algorithm (for example {"alg":"HS256","typ":"JWT"}). The payload holds the claims — statements about the user such as sub (subject), exp (expiry), and any custom data like roles. The signature is computed over the header and payload using a secret or private key, and it lets the server confirm the token has not been altered and came from a trusted issuer.
Is the data inside a JWT encrypted?
No — by default a JWT is signed, not encrypted. The header and payload are only Base64URL-encoded, which is trivially reversible: anyone who holds the token can decode and read every claim. The signature protects integrity (you can detect tampering) and authenticity (it came from someone with the key), but it does not provide confidentiality. The practical rule is: never put secrets or sensitive personal data in a JWT payload, and always send tokens over HTTPS.
What makes JWT authentication 'stateless'?
With server-side sessions, the server stores session data and the client holds only a session ID; every request requires a lookup in that session store. A JWT is self-contained — it carries the user's identity and claims inside the token itself. So the server keeps no per-user session: on each request it simply verifies the signature and reads the claims. This statelessness is what makes JWTs scale well, because any server instance can validate a request using only the signing key, with no shared session store needed.
How is a JWT sent and validated in Spring Security?
The client sends the token in the HTTP Authorization header as 'Bearer <token>'. On the server, you add a custom filter to the SecurityFilterChain — typically a OncePerRequestFilter placed before the username/password filter. That filter extracts the token after 'Bearer ', validates its signature and expiry, reads the subject and authorities, and sets a UsernamePasswordAuthenticationToken in the SecurityContextHolder. You also set the session creation policy to STATELESS so Spring does not create an HTTP session.
What are the security risks of Bearer tokens, and how do I mitigate them?
A signed JWT is a bearer credential: whoever holds it is treated as the user until it expires. If stolen, it can be replayed. Mitigations include: always transmit over HTTPS so tokens can't be sniffed; keep access tokens short-lived (minutes) and use refresh tokens for renewal; validate the signature, issuer, and expiry on every request; and avoid storing tokens where untrusted JavaScript can read them. Because revoking a stateless token before expiry is hard, short lifetimes and a deny-list for critical cases are common patterns.