Configuring a Resource Server

Dependency and Properties

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

application.yml

Point Spring at the issuer. On startup it reads the discovery document and the JWKS keys:

order-service/src/main/resources/application.yml

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: ${OAUTH_ISSUER:http://localhost:8180/realms/shopease}
          audiences: shopease-api

Tip: If the API must start even when the authorization server is down, set jwk-set-uri as well; Spring then fetches keys lazily on the first request instead of calling discovery at startup.

Security Configuration

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class ResourceServerConfig {

    @Bean
    SecurityFilterChain api(HttpSecurity http, JwtAuthenticationConverter jwtAuthConverter) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/actuator/health").permitAll()
                .requestMatchers(HttpMethod.GET, "/api/orders/**").hasAuthority("SCOPE_orders.read")
                .requestMatchers(HttpMethod.POST, "/api/orders/**").hasAuthority("SCOPE_orders.write")
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated())
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthConverter)))
            .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .csrf(csrf -> csrf.disable());     // no cookies: bearer tokens are not sent automatically
        return http.build();
    }
}
@RestController
@RequestMapping("/api/orders")
public class OrderController {

    private final OrderService orders;

    public OrderController(OrderService orders) { this.orders = orders; }

    @GetMapping
    public List<OrderDto> myOrders(@AuthenticationPrincipal Jwt jwt) {
        return orders.findByCustomer(jwt.getSubject());   // "sub" claim: the user's stable id
    }
}

What the Client Sees

Requests

No token, an expired token, a token without the needed scope, and a good token:

curl -i http://localhost:8081/api/orders
curl -i http://localhost:8081/api/orders -H "Authorization: Bearer $EXPIRED"
curl -i -X POST http://localhost:8081/api/orders -H "Authorization: Bearer $READ_ONLY" -H "Content-Type: application/json" -d '{"items":[{"productId":7,"quantity":1}]}'
curl -i http://localhost:8081/api/orders -H "Authorization: Bearer $TOKEN"

Example output

HTTP/1.1 401
WWW-Authenticate: Bearer

HTTP/1.1 401
WWW-Authenticate: Bearer error="invalid_token", error_description="An error occurred while attempting to decode the Jwt: Jwt expired at 2026-03-01T04:45:00Z", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1"

HTTP/1.1 403
WWW-Authenticate: Bearer error="insufficient_scope", error_description="The request requires higher privileges than provided by the access token.", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1"

HTTP/1.1 200
Content-Type: application/json

[{"id":1042,"status":"SHIPPED","total":2499.00,"placedAt":"2026-02-27T18:22:05"}]