Wiring a resource server in Spring Boot
From dependency to a working authority check.
Open this lesson in the learning hubKey points
- Add
spring-boot-starter-oauth2-resource-serverand setissuer-uri. Boot discovers the JWKS endpoint and configures validation of signature, issuer and expiry. - Audience is not validated by default. It needs a custom validator, and it is the check most often missing in a working setup.
- Scopes arrive as a space-separated
scopeclaim and Spring maps them to authorities prefixed withSCOPE_. That prefix is whyhasRolesilently fails on them. - Roles from your own identity provider usually live in a custom claim, so you need a converter to turn them into
ROLE_authorities. - Set the session policy to stateless. Otherwise Spring creates an HttpSession per request, which quietly defeats the point of using tokens.
- Test with a real token from the provider once, then use
@WithMockJwtor the JWT test helpers for the rest - integration tests that mint real tokens are slow and flaky.
Example
// build.gradle
// implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
class ResourceServerConfig {
@Bean
SecurityFilterChain api(HttpSecurity http) throws Exception {
return http
.securityMatcher("/api/**")
.csrf(csrf -> csrf.disable()) // no cookies, no CSRF surface
.sessionManagement(s -> s.sessionCreationPolicy(
SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(a -> a
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated())
.oauth2ResourceServer(o -> o.jwt(j ->
j.jwtAuthenticationConverter(converter())))
.build();
}
// Map a custom "roles" claim onto ROLE_ authorities.
static JwtAuthenticationConverter converter() {
JwtGrantedAuthoritiesConverter scopes = new JwtGrantedAuthoritiesConverter();
// scope -> SCOPE_read, SCOPE_write (the default)
JwtAuthenticationConverter c = new JwtAuthenticationConverter();
c.setJwtGrantedAuthoritiesConverter(jwt -> {
var authorities = new ArrayList<GrantedAuthority>(scopes.convert(jwt));
List<String> roles = jwt.getClaimAsStringList("roles");
if (roles != null) {
roles.forEach(r -> authorities.add(
new SimpleGrantedAuthority("ROLE_" + r)));
}
return authorities;
});
return c;
}
}
@RestController
class OrderController {
// SCOPE_ prefix for scopes, ROLE_ for roles. Mixing them up is the
// most common cause of an inexplicable 403.
@GetMapping("/api/orders")
@PreAuthorize("hasAuthority('SCOPE_orders.read')")
List<Order> list(@AuthenticationPrincipal Jwt jwt) {
return service.forUser(jwt.getSubject()); // sub, not a request param
}
}
issuer-uri gets you signature and expiry checks; audience, role mapping and STATELESS are the three you must add yourself.
This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the JWT Authentication Course course, and every lesson in it is listed on the JWT Authentication Course contents page.