Most Spring Boot apps validate JWTs incorrectly — wrong algorithm, missing claim validation, no token revocation. Learn the complete OAuth2 Resource Server configuration with Spring Security 6.
JOptimize Team
Spring Security's OAuth2 Resource Server support does a lot of the heavy lifting — but only if you configure it correctly. A misconfigured resource server might accept tokens signed with none algorithm, skip audience validation (accepting tokens meant for other services), or fail to enforce scopes properly. These aren't theoretical vulnerabilities.
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> </dependency>
The simplest correct configuration uses your Authorization Server's JWKS endpoint:
# application.properties spring.security.oauth2.resourceserver.jwt.issuer-uri=https://auth.myapp.com # Spring fetches keys from: https://auth.myapp.com/.well-known/openid-configuration # then: https://auth.myapp.com/.well-known/jwks.json
This approach:
iss (issuer) claim against the configured URIJOptimize detects this automatically
Scan your Java project in 30 seconds — free, no credit card.
@Configuration @EnableWebSecurity @EnableMethodSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) // Stateless API — no CSRF needed .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/v1/public/**").permitAll() .requestMatchers("/actuator/health").permitAll() .requestMatchers("/api/v1/admin/**").hasRole("ADMIN") .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 .jwt(jwt -> jwt .decoder(jwtDecoder()) .jwtAuthenticationConverter(jwtAuthenticationConverter()) )); return http.build(); } @Bean public JwtDecoder jwtDecoder() { NimbusJwtDecoder decoder = NimbusJwtDecoder .withJwkSetUri("https://auth.myapp.com/.well-known/jwks.json") .jwsAlgorithm(SignatureAlgorithm.RS256) // Enforce algorithm — reject 'none' .build(); // Validate audience claim — reject tokens meant for other services OAuth2TokenValidator<Jwt> audienceValidator = new JwtClaimValidator<List<String>>( JwtClaimNames.AUD, aud -> aud != null && aud.contains("order-service") ); OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer("https://auth.myapp.com"); decoder.setJwtValidator( new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator) ); return decoder; } @Bean public JwtAuthenticationConverter jwtAuthenticationConverter() { JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter(); converter.setAuthoritiesClaimName("roles"); // Custom claim name converter.setAuthorityPrefix("ROLE_"); // Maps 'ADMIN' → 'ROLE_ADMIN' JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter(); jwtConverter.setJwtGrantedAuthoritiesConverter(converter); return jwtConverter; } }
@RestController @RequestMapping("/api/v1/orders") public class OrderController { // Requires scope 'orders:read' OR role 'ADMIN' @GetMapping @PreAuthorize("hasAuthority('SCOPE_orders:read') or hasRole('ADMIN')") public List<OrderDto> listOrders() { ... } // Requires scope 'orders:write' @PostMapping @PreAuthorize("hasAuthority('SCOPE_orders:write')") public OrderDto createOrder(@RequestBody CreateOrderRequest req) { ... } // User can only read their own orders unless ADMIN @GetMapping("/{id}") @PreAuthorize("hasRole('ADMIN') or @orderSecurityService.canAccess(authentication, #id)") public OrderDto getOrder(@PathVariable Long id) { ... } }
@Component public class OrderSecurityService { private final OrderRepository orderRepo; public boolean canAccess(Authentication auth, Long orderId) { String userId = ((Jwt) auth.getPrincipal()).getSubject(); return orderRepo.findById(orderId) .map(order -> order.getUserId().equals(userId)) .orElse(false); } }
@RestController public class UserController { @GetMapping("/api/v1/me") public UserProfileDto getMyProfile( @AuthenticationPrincipal Jwt jwt) { String userId = jwt.getSubject(); // 'sub' claim String email = jwt.getClaimAsString("email"); List<String> roles = jwt.getClaimAsStringList("roles"); Instant issuedAt = jwt.getIssuedAt(); return new UserProfileDto(userId, email, roles); } }
For opaque tokens (reference tokens that can be revoked):
# application.properties spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://auth.myapp.com/oauth2/introspect spring.security.oauth2.resourceserver.opaquetoken.client-id=resource-server-client spring.security.oauth2.resourceserver.opaquetoken.client-secret=${INTROSPECTION_SECRET}
Each request calls the introspection endpoint — slower than JWT (one HTTP call per request) but supports real-time revocation.
aud claim — a JWT issued for auth-service can be presented to order-service; always validate audienceRS256 and HS256 allows algorithm confusion attacks; enforce a single algorithm401 vs 403 correctly — unauthenticated requests should return 401 Unauthorized; authenticated users without permission should return 403 Forbidden; Spring Security handles this correctly by defaultHttpOnly cookies are immune to XSS token theft; localStorage is notSpring Security OAuth2 Resource Server with JWT requires four things to be correct: algorithm enforcement (RS256 only, never none), issuer validation, audience validation (your service's identifier), and scope/role authorization on endpoints. Use JWKS endpoint for automatic key rotation. Add @PreAuthorize with SCOPE_ prefixed authorities for fine-grained access control. The configuration is verbose but each piece exists for a reason.
JOptimize flags missing audience validation, permissive authorization rules, hardcoded JWT secrets, and CSRF disabled without stateless session in Spring Security configurations.
Fix security misconfigurations before they become incidents.
JOptimize finds N+1 queries, EAGER collections, and 70+ other issues in your Java codebase — in under 30 seconds.