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:

  1. Signature — token not tampered with
  2. Expiration (exp) — token not expired
  3. Issuer (iss) — from trusted source
  4. Audience (aud) — intended for this service
  5. 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