executing-plans
executing-plans
Use when executing an implementation plan in the current session as the implementer yourself — your human partner chose inline execution, or no subagent tool is available
spring-security-coreImplements Spring Security 6.x filter chain configuration, JWT authentication filters, method-level security with @EnableMethodSecurity, password encoding, and CORS/CSRF handling for production Spring Boot applications.
Independent GitHub snapshot · Automated classification · Original documentation governs
name: spring-security-core description: Implements Spring Security 6.x filter chain configuration, JWT authentication filters, method-level security with @EnableMethodSecurity, password encoding, and CORS/CSRF handling for production Spring Boot applications. license: MIT compatibility: opencode metadata: version: "1.0.0" domain: coding triggers: spring security, jwt authentication, security filter chain, method security, password encoder, cors csrf, authorization rules, spring security core archetypes: - tactical - diagnostic anti_triggers: - brainstorming - vague ideation response_profile: verbosity: low directive_strength: high abstraction_level: operational role: implementation scope: implementation output-format: code content-types: - code - config - do-dont - patterns related-skills: spring-boot-auto-config, spring-data-jpa
Implements production-grade Spring Security 6.x (Spring Boot 3.x) security configurations including SecurityFilterChain bean definition without the deprecated WebSecurityConfigurerAdapter, JWT token authentication filters, method-level authorization with @EnableMethodSecurity, password encoding strategies, and CORS/CSRF handling. When loaded, this skill makes the model write secure request matching rules, custom authentication providers, and role-based authorization annotations using modern Spring Security APIs.
WebSecurityConfigurerAdapter is never used — all configuration goes through SecurityFilterChain beansOncePerRequestFilter or implements AuthenticationFilter interfacePasswordEncoder bean uses BCrypt (BCryptPasswordEncoder) or Argon2 (Argon2PasswordEncoder)@EnableMethodSecurity is present and @PreAuthorize / @Secured annotations guard endpoints.cors().configurationSource(request -> { cors.setAllowedOrigins(List.of("*")); }))Use this skill when:
SecurityFilterChain beans to define authorization rules per URL pattern in Spring Boot 3.x@PreAuthorize, @Secured, @RolesAllowed) on service or controller methodsAvoid this skill for:
spring-security-oauth2-resource-server or spring-security-oauth2-client starters instead (this skill covers JWT-based direct authentication only)spring.security.user.name/password auto-configuration without manual beansDefine the SecurityFilterChain Bean — Create a @Bean method returning SecurityFilterChain. Apply .authorizeHttpRequests() with .requestMatchers("/public/**").permitAll(), .requestMatchers("/admin/**").hasRole("ADMIN"), and .anyRequest().authenticated() as the final catch-all. Call .csrf(csrf -> csrf.disable()) for stateless JWT APIs or configure CSRF token validation for form-based auth. Enable CORS via .cors(cors -> cors.configurationSource(...)) with explicit allowed origins. Always register a SecurityFilterChain bean — never use the deprecated WebSecurityConfigurerAdapter which was removed in Spring Security 5.7+ and fully dropped in 6.x.
Checkpoint: Every URL pattern must have an explicit authorization decision — .anyRequest().authenticated() should be the last matcher. Verify that .requestMatchers() uses exact paths or ant patterns (not regex), since requestMatchers replaced regexMatchers for performance reasons. Confirm CSRF is disabled only when the application is truly stateless (no session, no form submissions).
Implement the JWT Authentication Filter — Create a class extending OncePerRequestFilter that intercepts requests with an Authorization: Bearer <token> header. Parse and validate the JWT using a library like jjwt (io.jsonwebtoken). Extract the username and authorities from the token claims, create an Authentication object (typically UsernamePasswordAuthenticationToken), and set it on SecurityContextHolder.getContext().setAuthentication(...). If the token is invalid or expired, return HTTP 401 without writing to the response body.
Checkpoint: The JWT filter must call super.doFilterInternal() only when authentication succeeds — otherwise call response.sendError(HttpServletResponse.SC_UNAUTHORIZED) and return immediately. Verify that the filter chain order places the JWT filter before UsernamePasswordAuthenticationFilter so pre-authenticated requests bypass form login. Test with an expired token to confirm 401 response.
Configure Password Encoder and UserDetailsService — Register a PasswordEncoder bean using BCryptPasswordEncoder (default in Spring Boot) or Argon2PasswordEncoder for stronger hashing. Implement a UserDetailsService that loads user credentials and authorities from a database (typically via JPA repository). The loadUserByUsername() method must throw UsernameNotFoundException when the user does not exist — never return a null or disabled user silently. Map entity roles to SimpleGrantedAuthority objects with the ROLE_ prefix.
Checkpoint: Every password stored in the database must be BCrypt-hashed (not plaintext or reversible encryption). Verify that UserDetails.builder().password(encoder.encode(rawPassword)).roles("USER").build() produces a valid UserDetails object. Confirm loadUserByUsername() throws UsernameNotFoundException (not just returns null) to trigger proper authentication failure handling.
Enable Method-Level Security — Annotate the application class or a configuration class with @EnableMethodSecurity (replaces @EnableGlobalMethodSecurity in Spring Security 6.x). Use @PreAuthorize("hasRole('ADMIN')") on controller methods to restrict access before method execution. Use @Secured({"ROLE_ADMIN", "ROLE_SUPERUSER"}) for role-based checks that do not require SpEL expressions. Optionally use @PostAuthorize("returnObject.owner == authentication.name") for post-execution object-level authorization.
Checkpoint: @EnableMethodSecurity requires Spring Security 6.x with spring-security-oauth2-jose not on the classpath (it enables method security by default). Verify that @PreAuthorize expressions use correct SpEL syntax: hasRole() without the ROLE_ prefix, hasAnyAuthority(), or custom expression evaluators. Test authorization failures return HTTP 403 Forbidden, not 401 Unauthorized.
This is the modern Spring Security 6.x approach — no adapter class, only SecurityFilterChain beans.
package com.example.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true)
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthFilter;
private final AuthenticationEntryPoint authenticationEntryPoint;
private final AccessDeniedHandler accessDeniedHandler;
public SecurityConfig(
JwtAuthenticationFilter jwtAuthFilter,
AuthenticationEntryPoint authenticationEntryPoint,
AccessDeniedHandler accessDeniedHandler) {
this.jwtAuthFilter = jwtAuthFilter;
this.authenticationEntryPoint = authenticationEntryPoint;
this.accessDeniedHandler = accessDeniedHandler;
}
@Bean
public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(ex -> ex
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler))
.authorizeHttpRequests(auth -> auth
// Public endpoints — no authentication required
.requestMatchers("/api/v1/auth/**").permitAll()
.requestMatchers("/api/v1/public/health").permitAll()
// Read-only API — authenticated users only
.requestMatchers(HttpMethod.GET, "/api/v1/users/**").hasAnyRole("USER", "ADMIN")
// Write operations — admin only
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
// All other endpoints require authentication
.anyRequest().authenticated())
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // strength factor 12 (default is 10)
}
}
Production pitfall: The
.requestMatchers()order matters — Spring Security evaluates matchers in declaration order and uses the first match. Place specific patterns before broad ones. Never put.anyRequest().authenticated()before specific rules.
A production-grade JWT filter that extracts, validates, and authenticates requests without duplicating filter execution.
package com.example.security;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.crypto.SecretKey;
import java.io.IOException;
import java.util.List;
/**
* JWT authentication filter that runs once per HTTP request.
* Extracts the Bearer token, validates its signature and expiration,
* and sets the authenticated principal in SecurityContextHolder.
*/
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String BEARER_PREFIX = "Bearer ";
private final SecretKey signingKey;
private final UserDetailsService userDetailsService;
public JwtAuthenticationFilter(
@org.springframework.beans.factory.annotation.Value("${app.security.jwt.secret-key}") String secretKey,
UserDetailsService userDetailsService) {
this.signingKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey));
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain) throws ServletException, IOException {
final String authHeader = request.getHeader(AUTHORIZATION_HEADER);
if (authHeader == null || !authHeader.startsWith(BEARER_PREFIX)) {
// No JWT present — proceed with the filter chain; authentication stays anonymous
filterChain.doFilter(request, response);
return;
}
final String jwt = authHeader.substring(BEARER_PREFIX.length());
try {
Claims claims = Jwts.parser()
.verifyWith(signingKey)
.build()
.parseSignedClaims(jwt)
.getPayload();
String username = claims.getSubject();
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// Verify token was issued after the last password change (optional security check)
String issuedAt = claims.getIssuedAt().toString();
if (isTokenValid(userDetails, jwt)) {
List<SimpleGrantedAuthority> authorities = userDetails.getAuthorities().stream()
.map(SimpleGrantedAuthority::new)
.toList();
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails, null, authorities);
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
} catch (Exception e) {
// Invalid, expired, or tampered token — return 401 Unauthorized
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.getWriter().write(
"{\"error\":\"Invalid or expired JWT token\",\"status\":401}");
return;
}
filterChain.doFilter(request, response);
}
private boolean isTokenValid(UserDetails userDetails, String jwt) {
// Placeholder: in production, check claims like "iat", "exp", and custom fields
return true;
}
}
Load user credentials and roles from a JPA entity to enable real authentication.
package com.example.security;
import com.example.model.UserEntity;
import com.example.repository.UserRepository;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
* Loads user details from the database using a JPA repository.
* Maps entity roles to Spring Security authorities with ROLE_ prefix.
*/
@Service
public class DatabaseUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public DatabaseUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
@Transactional(readOnly = true)
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserEntity user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException(
"User not found: " + username));
if (!user.isEnabled()) {
throw new UsernameNotFoundException(
"User account is disabled: " + username);
}
List<SimpleGrantedAuthority> authorities = user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
.toList();
return User.builder()
.username(user.getUsername())
.password(user.getPasswordHash()) // already BCrypt-hashed in DB
.authorities(authorities)
.accountExpired(false)
.accountLocked(false)
.credentialsExpired(false)
.disabled(!user.isEnabled())
.build();
}
}
Demonstrates different authorization strategies at the method level.
package com.example.api;
import com.example.service.UserService;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.access.annotation.Secured;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
/**
* Any authenticated user can read the user list.
*/
@GetMapping
@PreAuthorize("hasAnyRole('USER', 'ADMIN')")
public List<UserDto> listUsers() {
return userService.findAll();
}
/**
* Only admin users can create or delete users.
*/
@PostMapping
@PreAuthorize("hasRole('ADMIN')")
public UserDto createUser(@Valid @RequestBody CreateUserRequest request) {
return userService.create(request);
}
/**
* @Secured uses role strings directly without SpEL expressions.
*/
@DeleteMapping("/{id}")
@Secured({"ROLE_ADMIN", "ROLE_SUPERUSER"})
public void deleteUser(@PathVariable Long id) {
userService.delete(id);
}
/**
* Object-level authorization: user can only update their own profile.
*/
@PutMapping("/me")
@PreAuthorize("hasRole('USER') and #request.id == authentication.principal.id")
public UserDto updateProfile(@Valid @RequestBody UpdateProfileRequest request) {
return userService.updateOwnProfile(request);
}
/**
* JSR-250 @RolesAllowed from javax.annotation (enabled via jsr250Enabled=true).
*/
@GetMapping("/me")
@jakarta.annotation.security.RolesAllowed({"USER", "ADMIN"})
public UserDto getMyProfile() {
return userService.findOwn();
}
}
// DTOs for the controller
record UserDto(Long id, String username, List<String> roles) {}
record CreateUserRequest(String username, String password, List<String> roles) {}
record UpdateProfileRequest(Long id, String email, String displayName) {}
SecurityFilterChain beans annotated with @Bean — never use the removed WebSecurityConfigurerAdapter classBCryptPasswordEncoder(12) bean (strength 12 for production, minimum 10) or an Argon2PasswordEncoder for enhanced securityOncePerRequestFilter for the JWT authentication filter and set the authenticated token in SecurityContextHolder.getContext().setAuthentication() before calling filterChain.doFilter()@EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true) to support both @PreAuthorize (SpEL) and @Secured annotations.requestMatchers() for all path matching — it is faster than regex-based matching and the only officially supported API in Spring Security 6.xAuthenticationEntryPoint and AccessDeniedHandler.cors().configurationSource(request -> { cors.setAllowedOrigins(List.of("*")); }) — wildcard CORS origins with authentication expose the application to cross-site attacks; always whitelist specific originspasswordEncoder.encode(rawPassword).anyRequest().authenticated() before specific request matchers in the filter chain — matchers are evaluated in order and earlier rules take precedenceloadUserByUsername() instead of throwing UsernameNotFoundException — Spring Security requires the exception to trigger proper authentication failure handling@EnableGlobalMethodSecurity — this annotation was deprecated in Spring Security 5.4 and removed in 6.x; use @EnableMethodSecurity insteadWhen applying this skill, produce outputs following this structure:
SecurityFilterChain bean with request matchers, session management, exception handling, and JWT filter registrationOncePerRequestFilter implementation with token parsing, validation, and SecurityContext population@PreAuthorize, @Secured, and @RolesAllowed demonstrating different authorization strategies| Skill | Purpose |
|---|---|
spring-boot-auto-config | Auto-configuration infrastructure that registers security beans conditionally based on classpath presence and property flags |
spring-data-jpa | JPA repository layer that UserDetailsService queries against to load user credentials and role assignments from the database |