Developer-kit spring-boot-security-jwt
Provides JWT authentication and authorization patterns for Spring Boot 3.5.x covering token generation with JJWT, Bearer/cookie authentication, database/OAuth2 integration, and RBAC/permission-based access control using Spring Security 6.x. Use when implementing authentication or authorization in Spring Boot applications.
git clone https://github.com/giuseppe-trisciuoglio/developer-kit
T=$(mktemp -d) && git clone --depth=1 https://github.com/giuseppe-trisciuoglio/developer-kit "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/developer-kit-java/skills/spring-boot-security-jwt" ~/.claude/skills/giuseppe-trisciuoglio-developer-kit-spring-boot-security-jwt && rm -rf "$T"
plugins/developer-kit-java/skills/spring-boot-security-jwt/SKILL.mdSpring Boot JWT Security
JWT authentication and authorization patterns for Spring Boot 3.5.x using Spring Security 6.x and JJWT. Covers token generation, validation, refresh strategies, RBAC/ABAC, and OAuth2 integration.
Overview
This skill provides implementation patterns for stateless JWT authentication in Spring Boot applications. It covers the complete authentication flow including token generation with JJWT 0.12.6, Bearer/cookie-based authentication, refresh token rotation, and method-level authorization with
@PreAuthorize expressions.
Key capabilities:
- Access and refresh token generation with configurable expiration
- Bearer token and HttpOnly cookie authentication strategies
- Integration with Spring Data JPA and OAuth2 providers
- RBAC with role/permission-based
rules@PreAuthorize - Token revocation and blacklisting for logout/rotation
When to Use
Activate when user requests involve:
- "Implement JWT authentication", "secure REST API with tokens"
- "Spring Security 6.x configuration", "SecurityFilterChain setup"
- "Role-based access control", "RBAC",
`@PreAuthorize` - "Refresh token", "token rotation", "token revocation"
- "OAuth2 integration", "social login", "Google/GitHub auth"
- "Stateless authentication", "SPA backend security"
- "JWT filter", "OncePerRequestFilter", "Bearer token"
- "Cookie-based JWT", "HttpOnly cookie"
- "Permission-based access control", "custom PermissionEvaluator"
Quick Reference
Dependencies (JJWT 0.12.6)
| Artifact | Scope |
|---|---|
| compile |
| compile |
| compile |
| runtime |
| runtime |
| test |
See references/jwt-quick-reference.md for Maven and Gradle snippets.
Key Configuration Properties
| Property | Example Value | Notes |
|---|---|---|
| | Min 256 bits, never hardcode |
| | 15 min in milliseconds |
| | 7 days in milliseconds |
| | Validated on every token |
| | For cookie-based auth |
| | Always true in production |
| | Always true with HTTPS |
Authorization Annotations
| Annotation | Example |
|---|---|
| Role check |
| Permission check |
| Domain object check |
| Spring bean check |
Instructions
Step 1 — Add Dependencies
Include
spring-boot-starter-security, spring-boot-starter-oauth2-resource-server, and the three JJWT artifacts in your build file. See references/jwt-quick-reference.md for exact Maven/Gradle snippets.
Step 2 — Configure application.yml
jwt: secret: ${JWT_SECRET:change-me-min-32-chars-in-production} access-token-expiration: 900000 refresh-token-expiration: 604800000 issuer: my-app cookie-name: jwt-token cookie-http-only: true cookie-secure: false # true in production
See references/jwt-complete-configuration.md for the full properties reference.
Step 3 — Implement JwtService
Core operations: generate access token, generate refresh token, extract username, validate token.
@Service public class JwtService { public String generateAccessToken(UserDetails userDetails) { return Jwts.builder() .subject(userDetails.getUsername()) .issuer(issuer) .issuedAt(new Date()) .expiration(new Date(System.currentTimeMillis() + accessTokenExpiration)) .claim("authorities", getAuthorities(userDetails)) .signWith(getSigningKey()) .compact(); } public boolean isTokenValid(String token, UserDetails userDetails) { try { String username = extractUsername(token); return username.equals(userDetails.getUsername()) && !isTokenExpired(token); } catch (JwtException e) { return false; } } }
See references/jwt-complete-configuration.md for the complete JwtService including key management and claim extraction.
Step 4 — Create JwtAuthenticationFilter
Extend
OncePerRequestFilter to extract a JWT from the Authorization: Bearer header (or HttpOnly cookie), validate it, and set the SecurityContext.
@Component public class JwtAuthenticationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String authHeader = request.getHeader("Authorization"); if (authHeader == null || !authHeader.startsWith("Bearer ")) { chain.doFilter(request, response); return; } String jwt = authHeader.substring(7); String username = jwtService.extractUsername(jwt); if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = userDetailsService.loadUserByUsername(username); if (jwtService.isTokenValid(jwt, userDetails)) { UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authToken); } } chain.doFilter(request, response); } }
See references/configuration.md for the cookie-based variant.
Step 5 — Configure SecurityFilterChain
@Configuration @EnableWebSecurity @EnableMethodSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http .csrf(AbstractHttpConfigurer::disable) .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/auth/**", "/swagger-ui/**").permitAll() .anyRequest().authenticated() ) .authenticationProvider(authenticationProvider) .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) .build(); } }
See references/jwt-complete-configuration.md for CORS, logout handler, and OAuth2 login integration.
Step 6 — Create Authentication Endpoints
Expose
/register, /authenticate, /refresh, and /logout via @RestController. Return accessToken + refreshToken in the response body (and optionally set an HttpOnly cookie).
See references/examples.md for the complete
AuthenticationController and AuthenticationService.
Step 7 — Implement Refresh Token Strategy
Store refresh tokens in the database with
user_id, expiry_date, revoked, and expired columns. On /refresh, verify the stored token, revoke it, and issue a new pair (token rotation).
See references/token-management.md for
RefreshToken entity, rotation logic, and Redis-based blacklisting.
Step 8 — Add Authorization Rules
Use
@EnableMethodSecurity and @PreAuthorize annotations for fine-grained control:
@PreAuthorize("hasRole('ADMIN')") public Page<UserResponse> getAllUsers(Pageable pageable) { ... } @PreAuthorize("hasPermission(#documentId, 'Document', 'READ')") public Document getDocument(Long documentId) { ... }
See references/authorization-patterns.md for RBAC entity model,
PermissionEvaluator, and ABAC patterns.
Step 9 — Write Security Tests
@SpringBootTest @AutoConfigureMockMvc class AuthControllerTest { @Test void shouldDenyAccessWithoutToken() throws Exception { mockMvc.perform(get("/api/orders")) .andExpect(status().isUnauthorized()); } @Test @WithMockUser(roles = "ADMIN") void shouldAllowAdminAccess() throws Exception { mockMvc.perform(get("/api/admin/users")) .andExpect(status().isOk()); } }
See references/testing.md and references/jwt-testing-guide.md for full test suites, Testcontainers setup, and a security test checklist.
Best Practices
Token Security
- Use minimum 256-bit secret keys — load from environment variables, never hardcode
- Set short access token lifetimes (15 min); use refresh tokens for longer sessions
- Implement token rotation: revoke old refresh token when issuing a new one
- Use
(JWT ID) claim for blacklisting on logoutjti
Cookie vs Bearer Header
- Prefer HttpOnly cookies for browser clients (XSS-safe)
- Use
header for mobile/API clientsAuthorization: Bearer - Set
,Secure
orSameSite=Lax
on cookies in productionStrict
Spring Security 6.x
- Use
bean — never extendSecurityFilterChainWebSecurityConfigurerAdapter - Disable CSRF only for stateless APIs; keep it enabled for session-based flows
- Use
instead of deprecated@EnableMethodSecurity@EnableGlobalMethodSecurity - Validate
andiss
claims; reject tokens from untrusted issuersaud
Performance
- Cache
withUserDetails
to avoid DB lookup on every request@Cacheable - Cache signing key derivation (avoid re-computing HMAC key per request)
- Use Redis for refresh token storage at scale
What NOT to Do
- Do not store sensitive data (passwords, PII) in JWT claims — claims are only signed, not encrypted
- Do not issue tokens with infinite lifetime
- Do not accept tokens without validating signature and expiration
- Do not share signing keys across environments
Examples
Basic Authentication Flow
@RestController @RequestMapping("/api/auth") @RequiredArgsConstructor public class AuthController { private final AuthService authService; @PostMapping("/authenticate") public ResponseEntity<AuthResponse> authenticate( @RequestBody LoginRequest request) { return ResponseEntity.ok(authService.authenticate(request)); } @PostMapping("/refresh") public ResponseEntity<AuthResponse> refresh(@RequestBody RefreshRequest request) { return ResponseEntity.ok(authService.refreshToken(request.refreshToken())); } @PostMapping("/logout") public ResponseEntity<Void> logout() { authService.logout(); return ResponseEntity.ok().build(); } }
JWT Authorization on Controller Method
@RestController @RequestMapping("/api/admin") @PreAuthorize("hasRole('ADMIN')") public class AdminController { @GetMapping("/users") public ResponseEntity<List<UserResponse>> getAllUsers() { return ResponseEntity.ok(adminService.getAllUsers()); } }
See references/examples.md for complete entity models and service implementations.
References
| File | Content |
|---|---|
| references/jwt-quick-reference.md | Dependencies, minimal service, common patterns |
| references/jwt-complete-configuration.md | Full config: properties, SecurityFilterChain, JwtService, OAuth2 RS |
| references/configuration.md | JWT config beans, CORS, CSRF, error handling, session options |
| references/examples.md | Complete application setup: controllers, services, entities |
| references/authorization-patterns.md | RBAC/ABAC entity model, PermissionEvaluator, SpEL expressions |
| references/token-management.md | Refresh token entity, rotation, blacklisting with Redis |
| references/testing.md | Unit and MockMvc tests, test utilities |
| references/jwt-testing-guide.md | Testcontainers, load testing, security test checklist |
| references/security-hardening.md | Security headers, HSTS, rate limiting, audit logging |
| references/performance-optimization.md | Caffeine cache config, async validation, connection pooling |
| references/oauth2-integration.md | Google/GitHub OAuth2 login, OAuth2UserService |
| references/microservices-security.md | Inter-service JWT propagation, resource server config |
| references/migration-spring-security-6x.md | Migration from Spring Security 5.x |
| references/troubleshooting.md | Common errors, debugging tips |
Constraints and Warnings
Security Constraints
- JWT tokens are signed but not encrypted — do not include sensitive data in claims
- Always validate
,exp
, andiss
claims before trusting the tokenaud - Signing keys must be at least 256 bits; never use weak keys in production
- Load secrets from environment variables or secure vaults, never from config files
- SameSite cookie attribute is essential for CSRF protection in cookie-based flows
Spring Security 6.x Constraints
is removed — useWebSecurityConfigurerAdapter
beans onlySecurityFilterChain
is deprecated — use@EnableGlobalMethodSecurity@EnableMethodSecurity- Lambda DSL is required for
configuration (no method chaining)HttpSecurity
replaced byWebSecurityConfigurerAdapter.order()
on@Order
classes@Configuration
Token Constraints
- Access tokens should expire in 5-15 minutes for security
- Refresh tokens should be stored server-side (DB or Redis), never in localStorage
- Implement token blacklisting for immediate revocation on logout
claim is required for token blacklisting to work correctlyjti
Related Skills
— Constructor injection patterns used throughoutspring-boot-dependency-injection
— REST API security patterns and error handlingspring-boot-rest-api-standards
— Testing Spring Security configurationsunit-test-security-authorization
— User entity and repository patternsspring-data-jpa
— Security monitoring and health endpointsspring-boot-actuator