Authentication verifies who a user is; authorization determines what they are allowed to do. In modern enterprise applications with multiple administrative tiers, role-based access control (RBAC) must be enforced defensively on every single endpoint.
Storing authentication tokens improperly or relying solely on front-end route guards creates severe vulnerabilities. Here is how to implement a secure, stateless RBAC architecture.
1. Where Should JWTs Be Stored?
- LocalStorage / SessionStorage: Vulnerable to XSS (Cross-Site Scripting). Any malicious script or compromised npm package executing on your page can read
localStorage.getItem('token')and transmit it to an external server. - HttpOnly, Secure, SameSite Cookies: The Industry Standard. HttpOnly cookies cannot be accessed via client-side JavaScript (
document.cookie), completely mitigating XSS token theft.
2. Dual-Token Architecture: Access Token + Refresh Token Rotation
Never issue an access token with a multi-day or multi-week lifespan. If an access token is compromised, the attacker retains access until expiration.
The Dual-Token Lifecycle:
- Access Token: Short-lived (10–15 minutes). Sent in memory or authorization header.
- Refresh Token: Long-lived (7–14 days). Stored in an
HttpOnly,SameSite=Strictcookie. - Refresh Token Rotation: Each time a client requests a new access token using a refresh token, the server invalidates the used refresh token and issues a brand-new refresh token. If a stolen refresh token is used twice, the server detects token reuse and immediately invalidates all active sessions for that user.
3. Spring Security RBAC Implementation Example
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/", "/public/").permitAll()
.requestMatchers("/api/admin/").hasRole("ADMIN")
.requestMatchers("/api/manager/").hasAnyRole("ADMIN", "MANAGER")
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
```