Security with Spring Security + JWT
JWT (JSON Web Token) authentication is stateless — the server stores no session data. Each request carries a self-contained, signed token. Spring Security validates the token on every request via a custom filter inserted before the standard security chain.
Filter Chain Order
Incoming HTTP request
│
▼
JwtAuthFilter (OncePerRequestFilter)
├── Extract "Authorization: Bearer <token>" header
├── Parse JWT → extract username
├── Load UserDetails from database
├── Validate token (signature, expiry)
└── Set Authentication in SecurityContextHolder
│
▼
Spring Security filter chain
├── /api/auth/** → permitAll
├── /actuator/health → permitAll
├── /api/admin/** → hasRole("ADMIN")
└── everything else → authenticated
│
▼
Controller
JwtAuthFilter
JwtAuthFilter.java
View source ↗
public class JwtAuthFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, ...) {
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
String jwt = authHeader.substring(7);
String username = jwtService.extractUsername(jwt);
if (username != null && SecurityContext has no auth) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtService.isTokenValid(jwt, userDetails)) {
// Set authentication in SecurityContext
}
}
filterChain.doFilter(request, response);
}
}
Key Implementation Points
OncePerRequestFilterguarantees the filter runs exactly once per request, even in forward/include scenarios- If there is no Authorization header or it doesn't start with "Bearer ", the request continues unauthenticated — the security config will reject it if the endpoint requires authentication
- The filter only sets authentication if
SecurityContexthas no existing authentication — this prevents overwriting authentication set by other means
SecurityConfig — Role-Based Access
SecurityConfig.java — authorization rules
View source ↗
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**", "/actuator/health").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated())
Role Hierarchy
| Role | Capabilities |
|---|---|
USER |
Ask questions, upload documents to their own workspace, view their own history |
ADMIN |
All USER capabilities + manage all documents, view all users, access admin endpoints |
JWT is stateless — no session storage needed. Token expiry is set to 24 hours. For high-security deployments, combine short-lived access tokens (15 min) with a refresh token flow. Power RAG uses a single 24h token for simplicity.
JWT Token Structure
A JWT has three Base64url-encoded parts separated by dots:
- Header — algorithm (
HS256) and token type - Payload — claims:
sub(username),iat(issued at),exp(expiry), roles - Signature — HMAC-SHA256 of header + payload with the server secret key
The signature cannot be forged without the secret key. Tampering with the payload invalidates the signature and the token is rejected.