Spring Security as a Resource Server
Spring Security as a Resource Server
In a modern OAuth2 architecture your application rarely issues tokens itself. Instead, a dedicated Authorization Server (Keycloak, Auth0, Okta, your own Spring Authorization Server) hands out JWTs, and every downstream Resource Server — the APIs that actually hold the data — must independently verify each incoming bearer token on every request. This lesson covers exactly that role: configuring Spring Security 6 to act as a stateless OAuth2 Resource Server that validates bearer JWTs without calling back to the Authorization Server on each request.
Why a Resource Server Does Not Trust the Token Blindly
A bearer token is just a string. Anyone who intercepts it can reuse it. The Resource Server's job is to answer three questions before honoring a request:
- Is the signature valid? The token was signed with the Authorization Server's private key. Verifying against the public key proves it was not tampered with.
- Has the token expired? The
expclaim is checked against the server clock. - Is this token meant for me? The
aud(audience) claim names the Resource Server(s) that may accept it.
Adding the Dependency
Spring Security's resource-server support lives in spring-security-oauth2-resource-server, which is bundled with the spring-boot-starter-oauth2-resource-server starter. Add it to your pom.xml:
That single starter pulls in the JWT decoder, the bearer-token filter, and all required Spring Security converters.
Configuring the JWT Decoder via OIDC Discovery
The simplest — and recommended — approach is to point Spring at the Authorization Server's OIDC well-known endpoint. Spring downloads the public-key set (JWKS) automatically, caches it, and rotates it when the Authorization Server rolls its keys.
Spring derives the JWKS URI from issuer-uri + /.well-known/openid-configuration. It also validates that every incoming JWT's iss claim matches this URI — a free forgery check you would otherwise have to write yourself.
jwk-set-uri skips issuer validation, so make sure you add it manually in your security config.
The Security Configuration Class
With the starter on the classpath and issuer-uri set, Spring Boot auto-configures a resource server that requires a valid JWT on every request. In practice you will almost always override the defaults to add fine-grained authorization rules:
Authorization: Bearer <token> header. If your API is also consumed by a browser-based front-end that stores the JWT in a cookie, you need CSRF protection back on.
Understanding JwtAuthenticationConverter
After signature and expiry validation, Spring must turn the JWT's claims into a Spring Security Authentication object — specifically a JwtAuthenticationToken. The JwtAuthenticationConverter controls that mapping. Its key responsibility is extracting granted authorities (roles/scopes) from the token claims.
Different Authorization Servers embed roles differently:
- Keycloak:
realm_access.roles(nested JSON) - Auth0: custom claim, e.g.
https://example.com/roles - Spring Authorization Server:
scope(space-separated string, prefixed withSCOPE_)
For complex claim extraction you can implement Converter<Jwt, Collection<GrantedAuthority>> directly and wire it into the converter. Here is an example that reads Keycloak's nested structure:
Audience Validation — Restricting Which Tokens Are Accepted
By default, Spring's JWT decoder does not validate the aud claim. In a microservices environment where one Authorization Server issues tokens for many Resource Servers, skipping audience validation means Service A's token could be replayed against Service B. Always configure an audience validator:
Accessing the Token in a Controller
Once the filter chain validates the token, the full Jwt object is available in the security context. You can inject it into controller methods using @AuthenticationPrincipal:
@PreAuthorize: Because @EnableMethodSecurity is on the config class, you can lock individual endpoints to specific roles without repeating requestMatchers rules everywhere:
Distributed-Systems Trade-offs
Stateless JWT validation is fast and scalable, but it comes with real trade-offs you must understand:
- No immediate revocation: A stolen JWT remains valid until it expires. Mitigate with short expiry times (5–15 minutes) and refresh-token rotation (covered in Lesson 7).
- Clock skew: If the Resource Server's clock drifts ahead of the Authorization Server's, tokens will appear expired. NimbusJwtDecoder allows a configurable
clockSkewtolerance (default: 60 seconds). - JWKS caching: Spring caches the public key set and refreshes it when it encounters a signature that matches no cached key. If your Authorization Server rotates keys frequently, tune the cache TTL. Do not disable caching — fetching the JWKS on every request defeats the purpose of stateless validation.
- Key rollover: The Authorization Server should publish new keys with a new
kid(Key ID) alongside the old ones during the rotation window. Spring matches each JWT'skidheader to the cached JWKS entry, so most rollover scenarios are invisible to the Resource Server.
Summary
Configuring Spring Security as an OAuth2 Resource Server takes three concrete steps: add the starter, point issuer-uri at your Authorization Server, and write a SecurityFilterChain that calls oauth2ResourceServer(...).jwt(...). The framework handles JWKS retrieval, signature verification, expiry and issuer checks, and populating the security context. Your responsibilities are audience validation, role extraction via a JwtAuthenticationConverter, and choosing sensibly short token lifetimes to limit the blast radius of a stolen token. In the next lesson you put everything together in a fully secured project.