On this page
JWT Basics
JSON Web Tokens (JWT) are compact, self-contained tokens for securely transmitting claims between parties. They are the standard bearer token format in OAuth 2.0 and OpenID Connect.
Token Structure
header.payload.signature
Each part is Base64URL-encoded JSON:
Header:
{ "alg": "RS256", "typ": "JWT" }
Payload (claims):
{
"sub": "user123",
"iss": "https://auth.example.com",
"aud": "my-api",
"exp": 1700000000,
"iat": 1699996400,
"roles": ["USER", "ADMIN"]
}
Signature: RSASHA256(base64(header) + "." + base64(payload), privateKey)
Standard Claims
| Claim | Meaning |
|---|---|
sub |
Subject (user ID) |
iss |
Issuer |
aud |
Audience |
exp |
Expiration time |
iat |
Issued at |
nbf |
Not before |
Creating JWT with JJWT
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
@Service
public class JwtService {
private final SecretKey key = Keys.hmacShaKeyFor(
Decoders.BASE64.decode(System.getenv("JWT_SECRET")));
public String generateToken(UserDetails user) {
return Jwts.builder()
.subject(user.getUsername())
.claim("roles", user.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).toList())
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + 3600_000))
.signWith(key)
.compact();
}
public String extractUsername(String token) {
return parseClaims(token).getSubject();
}
public boolean isValid(String token, UserDetails user) {
Claims claims = parseClaims(token);
return claims.getSubject().equals(user.getUsername())
&& claims.getExpiration().after(new Date());
}
private Claims parseClaims(String token) {
return Jwts.parser()
.verifyWith(key)
.build()
.parseSignedClaims(token)
.getPayload();
}
}
RS256 (Asymmetric Signing)
For multi-service architectures, use RSA key pairs:
// Sign with private key (Authorization Server)
Jwts.builder().subject("user123").signWith(privateKey, Jwts.SIG.RS256).compact();
// Verify with public key (Resource Server)
Jwts.parser().verifyWith(publicKey).build().parseSignedClaims(token);
Publish public key via JWKS endpoint: /.well-known/jwks.json
Validation Checklist
Always validate:
- Signature — token not tampered with
- Expiration (
exp) — token not expired - Issuer (
iss) — from trusted source - Audience (
aud) — intended for this service - Not before (
nbf) — if present
JWT vs Session
| Aspect | JWT | Session |
|---|---|---|
| Storage | Client-side | Server-side |
| Scalability | Stateless, no server store | Requires session store |
| Revocation | Hard (until expiry) | Easy (delete session) |
| Size | Larger (in every request) | Small (session ID only) |
| Best for | Microservices, SPAs | Traditional web apps |
Best Practices
- Use RS256 for distributed systems; HS256 only for single-service apps
- Keep access tokens short-lived (15–60 minutes)
- Never store sensitive data in JWT payload — it is Base64, not encrypted
- Use refresh tokens for long-lived sessions
- Implement token blocklist (Redis) for logout/revocation when needed
- Validate all standard claims on every request