Source profileQuality 84/100

rrezartprebreza/spring-boot-skills/skills/spring-boot-4/spring-security-jwt/SKILL.md

spring-security-jwt

Use when implementing authentication, authorization, JWT tokens, security filters, password encoding, or any Spring Security configuration. Covers stateless JWT auth, access and refresh token validation, RBAC, and method-level security.

Source repository stars
194
Declared platforms
0
Static risk flags
0
Last source update
2026-07-30
Source checked
2026-08-04

Decision brief

What it does—and where it fits

Spring Boot 4.x ships Spring Security 7: the lambda DSL is the only style — and(), authorizeRequests(), antMatchers(), and WebSecurityConfigurerAdapter no longer exist, and AntPathRequestMatcher/MvcRequestMatcher are replaced by PathPatternRequestMatcher (requestMatchers("/path/…

Best for

  • Use when implementing authentication, authorization, JWT tokens, security filters, password encoding, or any Spring Security configuration.

Not for

  • Tasks that require unconfirmed production actions or broad system permissions.
  • Environments where the pinned source and install steps cannot be inspected.

Compatibility matrix

Platform support, with evidence labels

PlatformStatusEvidenceWhat to check
CodexNot declaredNo explicit evidencePortability before use
Claude CodeNot declaredNo explicit evidencePortability before use
CursorNot declaredNo explicit evidencePortability before use
Gemini CLINot declaredNo explicit evidencePortability before use
Open the compatibility checker

Installation

Inspect first. Install second.

The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.

Source-detected install commandSource
npx skills add https://github.com/rrezartprebreza/spring-boot-skills --skill "skills/spring-boot-4/spring-security-jwt"
Safe inspection promptEditorial

Inspect the Agent Skill "spring-security-jwt" from https://github.com/rrezartprebreza/spring-boot-skills/blob/8cabf531ead42483fba97b47228ea3b8414cf9b7/skills/spring-boot-4/spring-security-jwt/SKILL.md at commit 8cabf531ead42483fba97b47228ea3b8414cf9b7. List every install step, command, network request, credential, file read/write, external action, and rollback step. Explain whether it fits my task. Do not install or execute anything until I approve.

Workflow

What the source asks the agent to do

  1. 01

    Dependencies

    Review the “Dependencies” section in the pinned source before continuing.

    Review and apply the “Dependencies” source section.
  2. 02

    Security Configuration

    Review the “Security Configuration” section in the pinned source before continuing.

    Review and apply the “Security Configuration” source section.
  3. 03

    JWT Service

    Review the “JWT Service” section in the pinned source before continuing.

    Review and apply the “JWT Service” source section.
  4. 04

    JWT Filter

    Review the “JWT Filter” section in the pinned source before continuing.

    Review and apply the “JWT Filter” source section.
  5. 05

    JSON 401/403 — Don't Ship the Defaults

    Out of the box, an unauthenticated API request gets an empty 401 (or worse, a redirect to a login page) and AccessDeniedException becomes an empty 403. REST clients need a body:

    Out of the box, an unauthenticated API request gets an empty 401 (or worse, a redirect to a login page) and AccessDeniedException becomes an empty 403. REST clients need a body:@RestControllerAdvice cannot catch these — security filters run before the dispatcher servlet, so exceptions thrown there never reach your exception handler.

Permission review

Static risk signals and limitations

No configured static risk pattern was detected

This is not proof of safety. Runtime behavior, indirect dependencies, and hidden external systems are outside the static scan.

Evidence record

Why each signal appears

EvidenceSourceComputedTestedEditorial
SignalValueEvidence typeMeaning
Quality score84/100ComputedDocumentation, specificity, maintenance, and trust rules
Repository stars194SourceRepository attention, not individual Skill quality
Compatibility0 platformsSourceDeclared in the catalog source record
Usage guideautomated source guideEditorialGenerated or reviewed according to the visible evidence level

Pinned source

Provenance and original SKILL.md

Repository
rrezartprebreza/spring-boot-skills
Skill path
skills/spring-boot-4/spring-security-jwt/SKILL.md
Commit
8cabf531ead42483fba97b47228ea3b8414cf9b7
License
MIT
Collected
2026-08-04
Default branch
main
View the original SKILL.md

Spring Security — JWT

Spring Boot 4.x ships Spring Security 7: the lambda DSL is the only style — and(), authorizeRequests(), antMatchers(), and WebSecurityConfigurerAdapter no longer exist, and AntPathRequestMatcher/MvcRequestMatcher are replaced by PathPatternRequestMatcher (requestMatchers("/path/**") uses it under the hood).

Dependencies

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.12.6</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.12.6</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.12.6</version>
    <scope>runtime</scope>
</dependency>

Security Configuration

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtAuthFilter;
    private final UserDetailsService userDetailsService;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(AbstractHttpConfigurer::disable)
            .sessionManagement(s -> s.sessionCreationPolicy(STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/v1/auth/**").permitAll()
                .requestMatchers("/actuator/health").permitAll()
                .requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
            .build();
    }

    @Bean
    public AuthenticationProvider authenticationProvider() {
        var provider = new DaoAuthenticationProvider(userDetailsService); // Security 7: constructor, not setter
        provider.setPasswordEncoder(passwordEncoder());
        return provider;
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12);
    }
}

JWT Service

@Service
public class JwtService {

    @Value("${app.jwt.secret}")
    private String secretKey;

    @Value("${app.jwt.access-token-expiry:900000}") // 15 min default
    private long accessTokenExpiry;

    @Value("${app.jwt.refresh-token-expiry:604800000}") // 7 days default
    private long refreshTokenExpiry;

    public String generateAccessToken(UserDetails user) {
        return generateToken(Map.of("type", "access"), user, accessTokenExpiry);
    }

    public String generateRefreshToken(UserDetails user) {
        return generateToken(Map.of("type", "refresh"), user, refreshTokenExpiry);
    }

    private String generateToken(Map<String, Object> claims, UserDetails user, long expiry) {
        return Jwts.builder()
            .claims(claims)
            .subject(user.getUsername())
            .issuedAt(new Date())
            .expiration(new Date(System.currentTimeMillis() + expiry))
            .signWith(getSigningKey())
            .compact();
    }

    public boolean isAccessTokenValid(String token, UserDetails user) {
        Claims claims = extractClaims(token);
        return "access".equals(claims.get("type", String.class))
            && user.getUsername().equals(claims.getSubject())
            && !claims.getExpiration().before(new Date());
    }

    public String extractUsername(String token) {
        return extractClaims(token).getSubject();
    }

    private Claims extractClaims(String token) {
        return Jwts.parser().verifyWith(getSigningKey()).build()
            .parseSignedClaims(token).getPayload();
    }

    private SecretKey getSigningKey() {
        return Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey));
    }
}

JWT Filter

@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtService jwtService;
    private final UserDetailsService userDetailsService;

    @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 token = authHeader.substring(7);
        try {
            String username = jwtService.extractUsername(token);

            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails user = userDetailsService.loadUserByUsername(username);
                if (jwtService.isAccessTokenValid(token, user)) {
                    var auth = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
                    auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(auth);
                }
            }
        } catch (ExpiredJwtException | MalformedJwtException | SignatureException e) {
            // Parsing throws on expired/tampered tokens. Without this catch the exception
            // escapes the filter chain as a 500. Leave the context empty — the entry
            // point below turns it into a clean 401.
            SecurityContextHolder.clearContext();
        }
        chain.doFilter(request, response);
    }
}

JSON 401/403 — Don't Ship the Defaults

Out of the box, an unauthenticated API request gets an empty 401 (or worse, a redirect to a login page) and AccessDeniedException becomes an empty 403. REST clients need a body:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    return http
        // ... as above ...
        .exceptionHandling(ex -> ex
            .authenticationEntryPoint((request, response, e) -> {       // 401 — not authenticated
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                response.getWriter().write("""
                    {"success":false,"error":{"code":"UNAUTHORIZED","message":"Authentication required"}}""");
            })
            .accessDeniedHandler((request, response, e) -> {            // 403 — authenticated, no permission
                response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                response.getWriter().write("""
                    {"success":false,"error":{"code":"FORBIDDEN","message":"Insufficient permissions"}}""");
            })
        )
        .build();
}

@RestControllerAdvice cannot catch these — security filters run before the dispatcher servlet, so exceptions thrown there never reach your exception handler.

Auth Controller

@RestController
@RequestMapping("/api/v1/auth")
@RequiredArgsConstructor
public class AuthController {

    private final AuthService authService;

    @PostMapping("/login")
    public ApiResponse<AuthResponse> login(@Valid @RequestBody LoginRequest request) {
        return ApiResponse.ok(authService.login(request));
    }

    @PostMapping("/refresh")
    public ApiResponse<AuthResponse> refresh(@Valid @RequestBody RefreshRequest request) {
        return ApiResponse.ok(authService.refresh(request.refreshToken()));
    }

    @PostMapping("/register")
    public ResponseEntity<ApiResponse<AuthResponse>> register(@Valid @RequestBody RegisterRequest request) {
        return ResponseEntity.status(201).body(ApiResponse.ok(authService.register(request)));
    }
}

public record AuthResponse(String accessToken, String refreshToken, long expiresIn) {}

Method-Level Security

// On service methods
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(UUID userId) { ... }

@PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.id")
public UserProfile getProfile(UUID userId) { ... }

@PostAuthorize("returnObject.email == authentication.name")
public User findById(UUID id) { ... }

application.yml

app:
  jwt:
    secret: ${JWT_SECRET} # min 256-bit base64 encoded key
    access-token-expiry: 900000   # 15 minutes
    refresh-token-expiry: 604800000 # 7 days

The example creates refresh tokens but does not implement a refresh endpoint. A production refresh flow must accept only type=refresh, rotate the refresh token on every use, and revoke the previous token (for example, with a hashed token-family record in a database or Redis).

Gotchas

  • Agent uses non-lambda chaining (http.csrf().disable(), .and(), authorizeRequests()) — removed in Security 7, won't compile; lambda DSL only: csrf(AbstractHttpConfigurer::disable), authorizeHttpRequests(...)
  • Agent writes antMatchers()/mvcMatchers() or AntPathRequestMatcher/MvcRequestMatcher — removed in Security 7; use requestMatchers("/path/**") (backed by PathPatternRequestMatcher) or PathPatternRequestMatcher.withDefaults().matcher("/path/**")
  • Agent extends WebSecurityConfigurerAdapter — long gone; declare a SecurityFilterChain bean
  • Agent calls provider.setUserDetailsService(...) — gone in Security 7; pass it to the constructor: new DaoAuthenticationProvider(userDetailsService)
  • Agent lets ExpiredJwtException escape the filter — expired token becomes a 500 instead of 401; catch in filter
  • Agent skips exceptionHandling() — clients get empty 401/403 bodies (or a login-page redirect); @RestControllerAdvice can't catch filter-level exceptions
  • Agent stores JWT secret in code — always ${JWT_SECRET} from environment (HS256 needs a ≥256-bit key or Keys.hmacShaKeyFor throws WeakKeyException)
  • Agent uses SessionCreationPolicy.IF_REQUIRED — must be STATELESS for JWT
  • Agent validates only signature and expiry — the bearer filter must accept type=access tokens only; refresh tokens belong to a separate refresh endpoint
  • Agent forgets @EnableMethodSecurity for @PreAuthorize to work
  • Agent uses BCrypt strength < 10 — use 12 for production
  • Agent puts token validation logic in controller — belongs in filter
  • Agent puts refresh tokens in localStorage examples — recommend httpOnly cookies or secure storage; refresh tokens are long-lived credentials
  • Agent tests security with @MockBean/@SpyBean — removed in Boot 4; use @MockitoBean/@MockitoSpyBean, and add @AutoConfigureMockMvc@SpringBootTest no longer provides MockMvc on its own