JWT Tokens Explained
What JWT tokens are, how they work, and how to use them for API authentication without going crazy.
At some point I had to replace a session-based authentication system with something that worked across multiple nodes behind a load balancer, without a shared session store.
The problem with the old approach was that each server kept its own session state in memory. If the load balancer routed you to Server A for login, but Server B for your next request — Server B had no idea who you were. The “fix” was sticky sessions, which meant you were always pinned to one node. Take that node down for maintenance, everyone on it gets logged out. Not ideal.
JWT solved this by moving the state to the client.
What a JWT actually is
A JWT (JSON Web Token) is a string with three Base64-encoded parts separated by dots:
1
header.payload.signature
Header — declares the token type and signing algorithm:
1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}
Payload — the claims. This is the actual data you’re encoding:
1
2
3
4
5
6
{
"sub": "rahul.chandna",
"roles": ["admin", "user"],
"iat": 1707552000,
"exp": 1707555600
}
iat is “issued at” and exp is “expires at” — both Unix timestamps.
Signature — created by signing the encoded header and payload with a secret:
1
HMACSHA256(base64(header) + "." + base64(payload), secret)
This is what makes the token tamper-proof. Someone who gets hold of your token can read the payload (it’s just Base64) but can’t change it, because the signature won’t match.
I once watched a developer discover this the hard way — they tried to change their role from
usertoadminby editing the payload directly and were confused why the server rejected it. The server wasn’t storing anything. It just checked the signature. 😄
The auth flow
- User logs in with credentials
- Server validates, generates a JWT, sends it back
- Client stores it (more on this below)
- Client sends it on every request:
1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
- Server verifies the signature and checks expiry — no database lookup needed
Generating a JWT in Java
Using the java-jwt library from Auth0:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import java.util.Date;
public class JwtUtil {
private static final String SECRET = "your-256-bit-secret";
private static final long EXPIRY_MS = 3600_000; // 1 hour
public static String generateToken(String username, String[] roles) {
Algorithm algorithm = Algorithm.HMAC256(SECRET);
return JWT.create()
.withSubject(username)
.withArrayClaim("roles", roles)
.withIssuedAt(new Date())
.withExpiresAt(new Date(System.currentTimeMillis() + EXPIRY_MS))
.sign(algorithm);
}
}
Verifying a JWT in Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
public class JwtVerifier {
private static final String SECRET = "your-256-bit-secret";
public static DecodedJWT verify(String token) {
try {
Algorithm algorithm = Algorithm.HMAC256(SECRET);
JWTVerifier verifier = JWT.require(algorithm).build();
return verifier.verify(token);
} catch (JWTVerificationException e) {
throw new RuntimeException("Invalid token: " + e.getMessage());
}
}
}
Then extracting claims:
1
2
3
DecodedJWT jwt = JwtVerifier.verify(token);
String username = jwt.getSubject();
String[] roles = jwt.getClaim("roles").asArray(String.class);
Wiring it up as a JAX-RS filter
Rather than checking the token in every endpoint, put it in a filter:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Provider
@Secured
public class AuthenticationFilter implements ContainerRequestFilter {
@Override
public void filter(ContainerRequestContext ctx) {
String authHeader = ctx.getHeaderString("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
ctx.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
return;
}
String token = authHeader.substring("Bearer ".length());
try {
DecodedJWT jwt = JwtVerifier.verify(token);
// set a SecurityContext here if you need role-based access control
} catch (RuntimeException e) {
ctx.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
}
}
}
Annotate endpoints with @Secured (a custom binding annotation) to apply the filter selectively.
Things that burned me
Algorithm confusion attacks — always specify the algorithm explicitly when verifying. Never trust the alg field from the token header, because an attacker can set it to none and submit an unsigned token. The JWT.require(algorithm) call above handles this correctly.
Token storage on the client — localStorage is convenient but vulnerable to XSS. HttpOnly cookies are more secure but need CSRF protection. Neither is perfect; the right choice depends on your threat model.
Revoking tokens — you can’t. JWTs are stateless by design. If someone’s token is compromised, you can’t invalidate it until it expires. Short expiry times (15–60 minutes) combined with a refresh token mechanism is the standard approach for dealing with this.