A minimal, production-ready Spring Boot 3 project demonstrating JWT authentication end-to-end using HS256, including access tokens, refresh token rotation, and role-based access control.
- π JWT-based authentication with HS256
- π Refresh token rotation for enhanced security
- π₯ Role-based access control (RBAC)
- π« Token revocation with denylist
- β±οΈ Short-lived access tokens (5 minutes)
- π Comprehensive validation of JWT claims
- π‘οΈ Spring Security integration
- π¦ In-memory user store (easily adaptable to database)
- Java 21 or higher
- Maven 3.9+
mvn spring-boot:runThe server starts on http://localhost:8080.
GET /api/public/ping- Health check endpoint
POST /api/auth/login- Login with username and passwordPOST /api/auth/refresh- Refresh access token (rotates refresh token)POST /api/auth/logout- Revoke refresh token
GET /api/me- Get current user info (requires authentication)GET /api/admin- Admin-only endpoint (requires ADMIN role)
curl -s -X POST http://localhost:8080/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"admin123"}'Response:
{
"access_token": "eyJ0eXAiOiJKV1Q...",
"refresh_token": "eyJ0eXAiOiJKV1Q...",
"token_type": "Bearer",
"expires_in_seconds": 299
}curl -s http://localhost:8080/api/public/pingACCESS_TOKEN=eyJ...
curl -s http://localhost:8080/api/me \
-H "Authorization: Bearer ${ACCESS_TOKEN}"Call /api/admin (requires ADMIN role):
ACCESS_TOKEN=eyJ...
curl -s http://localhost:8080/api/admin \
-H "Authorization: Bearer ${ACCESS_TOKEN}"REFRESH_TOKEN=eyJ...
curl -s -X POST http://localhost:8080/api/auth/refresh \
-H 'Content-Type: application/json' \
-d '{"refresh_token":"'"${REFRESH_TOKEN}"'"}'REFRESH_TOKEN=eyJ...
curl -s -X POST http://localhost:8080/api/auth/logout \
-H 'Content-Type: application/json' \
-d '{"refresh_token":"'"${REFRESH_TOKEN}"'"}'src/main/java/com/example/jwt/
βββ api/ # REST controllers and DTOs
β βββ ApiController.java
β βββ AuthController.java
β βββ dto/
βββ config/ # Configuration classes
β βββ JwtProperties.java
β βββ SecurityConfig.java
βββ security/ # Security filters and handlers
β βββ JwtAuthFilter.java
β βββ JwtUserPrincipal.java
βββ service/ # Business logic
βββ JwtService.java
βββ TokenDenylistService.java
βββ UserService.java
- Spring Boot 3.3.2
- Spring Security 6
- Nimbus JOSE + JWT for JWT handling
- Java 21
- Maven
- β JWT signature verification (HS256)
- β Token expiration validation
- β
Issuer (
iss) and audience (aud) validation - β
Subject (
sub) validation - β Clock skew tolerance (30 seconds)
- β Refresh token rotation (old token invalidated on refresh)
- β Token revocation via denylist
- β Role-based access control
- β CORS configuration
- β Stateless authentication
- header.payload.signature
- The header and payload are Base64URL-encoded JSON, not encrypted. Never put secrets in the payload.
- The signature proves integrity and authenticity with the shared secret (HS256 in this demo).
- Stateless verification
- The server verifies the signature and claims on each request.
- No session state is required for access tokens.
- Gotchas & best practices
- Signed β encrypted: Treat the payload as public.
- Validate claims:
exp,iss,aud, andsubare enforced in code. - Short-lived access tokens + refresh tokens: Access tokens are 5 minutes; refresh tokens are 7 days.
- Revocation: Access tokens are stateless; this demo only revokes refresh tokens via a denylist.
Edit src/main/resources/application.yml to customize:
- JWT secret (minimum 32 bytes for HS256)
- Token lifetimes (access: 5 minutes, refresh: 7 days)
- Issuer and audience values
- CORS allowed origins
For demonstration purposes, two users are pre-configured:
| Username | Password | Roles |
|---|---|---|
admin |
admin123 |
USER, ADMIN |
user |
user123 |
USER |
- User logs in with credentials β receives access token + refresh token
- Access token used for API requests (short-lived: 5 minutes)
- When access token expires β use refresh token to get new tokens
- Refresh token rotation: old refresh token invalidated, new one issued
- Logout revokes the refresh token
header.payload.signature
- Header: Algorithm (HS256) and token type (JWT)
- Payload: Claims (sub, iss, aud, exp, iat, jti, roles, token_type)
- Signature: HMAC-SHA256 signature for verification
Note: JWT payload is Base64URL-encoded, not encrypted. Never store sensitive data in claims.
MIT License - feel free to use this project for learning or as a starting point for your applications.
Contributions are welcome! Please feel free to submit a Pull Request.