How do you configure a Spring Security application to act as an OAuth2 Resource Server that accepts JWT bearer tokens?
answer
- Bearer header -> validate, don't log in
- starter-oauth2-resource-server
- oauth2ResourceServer().jwt()
- issuer-uri or jwk-set-uri
- NimbusJwtDecoder + JwtAuthenticationToken
basics
~10 sIn the SecurityFilterChain, call http.oauth2ResourceServer(oauth2 -> oauth2.jwt(...)). Add a jwk-set-uri or issuer-uri in application.yml so Spring can fetch the keys and verify each incoming Bearer token's signature.
solid answer
~40 sA resource server protects APIs by validating a JWT sent in the Authorization: Bearer header. You add spring-boot-starter-oauth2-resource-server, then in your SecurityFilterChain call http.oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults())). You point Spring at the authorization server's keys via spring.security.oauth2.resourceserver.jwt.issuer-uri (or jwk-set-uri). Spring auto-configures a NimbusJwtDecoder that fetches the JWK set, verifies the token signature, and validates standard claims (expiry, not-before, issuer). A BearerTokenAuthenticationFilter extracts the token; on success the request gets a JwtAuthenticationToken with a Jwt principal. No sessions are created — every request re-validates the token. Failed validation returns 401 with a WWW-Authenticate header. You still add authorizeHttpRequests rules to gate endpoints by authority/scope.
code
java · 19 lines@Configuration
@EnableWebSecurity
public class ResourceServerConfig {
@Bean
SecurityFilterChain api(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // pure token API, no browser session
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health").permitAll()
.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
}
// application.yml
// spring.security.oauth2.resourceserver.jwt.issuer-uri: https://auth.example.com/realms/katajobgo deeper
Know the header is Authorization: Bearer, the two config properties, and that .jwt() enables validation.
Explain the stateless request lifecycle and the decoder/provider/filter split.
Contrast issuer-uri vs jwk-set-uri, discuss revocation limits and pairing with authorization rules.
Weigh JWT-local validation vs opaque-token introspection for the platform, revocation strategy, and startup-time coupling to the auth server.
## What a Resource Server is In OAuth2, the **Resource Server** is the service that owns the protected APIs (your data/endpoints). It does NOT log users in — that's the **Authorization Server** (e.g. Keycloak, Auth0, Okta, Spring Authorization Server). Clients obtain an **access token** from the authorization server and send it to your resource server on each call in the HTTP header `Authorization: Bearer <token>`. Your job is only to *validate* that token and derive an authenticated principal + authorities from it. A **JWT** (JSON Web Token) is a self-contained, signed token with three base64url parts separated by dots: `header.payload.signature`. The header names the signing algorithm (e.g. `RS256`) and a key id (`kid`). The payload holds **claims** (`sub`, `iss`, `exp`, `nbf`, `scope`, etc.). The signature lets the resource server verify authenticity without calling the authorization server for each request. ## Dependency + config Add `org.springframework.boot:spring-boot-starter-oauth2-resource-server`. Then either: ```yaml spring: security: oauth2: resourceserver: jwt: issuer-uri: https://auth.example.com/realms/katajob ``` `issuer-uri` is the most robust: at startup Spring hits the well-known OpenID discovery document (`{issuer}/.well-known/openid-configuration`) to learn the `jwks_uri`, and it also adds an **issuer validator** that checks the `iss` claim. Alternatively `jwk-set-uri: https://.../jwks` points directly at the JSON Web Key Set (the public keys) — use it when there's no discovery endpoint. ## The security config ```java @Bean SecurityFilterChain api(HttpSecurity http) throws Exception { http .authorizeHttpRequests(a -> a.anyRequest().authenticated()) .oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults())); return http.build(); } ``` `oauth2ResourceServer().jwt()` registers a `BearerTokenAuthenticationFilter` and, on the `jwt()` path, a `JwtAuthenticationProvider` backed by a `JwtDecoder`. When Boot sees the `jwt.*` properties it auto-creates a `NimbusJwtDecoder`; if you have neither property, `.jwt()` fails to start because there is no decoder. ## Request lifecycle 1. `BearerTokenAuthenticationFilter` pulls the token from the `Authorization` header (or a form/query param if configured). 2. `JwtAuthenticationProvider` calls `JwtDecoder.decode(token)`. Nimbus verifies the signature against the JWK matching the header `kid`, then runs claim validators (timestamp, issuer). 3. On success a `JwtAuthenticationToken` (an `AbstractOAuth2TokenAuthenticationToken`) is placed in the `SecurityContext`; its `getName()` is the `sub`, principal is the decoded `Jwt`, and authorities come from a `JwtAuthenticationConverter` (default: `SCOPE_` prefix over the `scope` claim). 4. On failure it returns 401 with a `WWW-Authenticate: Bearer error=...` header via `BearerTokenAuthenticationEntryPoint`. ## Stateless Resource-server JWT auth is inherently **stateless**: no `HttpSession`, no CSRF token needed for pure token APIs, every request is validated fresh. Revocation is a known limitation — a valid, unexpired JWT stays valid until `exp` unless you add token introspection or short lifetimes. ## Gotchas - Forgetting `authorizeHttpRequests` — `.jwt()` authenticates but does not authorize; without rules everything may be permitted or (with `anyRequest().authenticated()`) require a token. - Confusing `oauth2Login()` (a *login/client* flow that establishes a session) with `oauth2ResourceServer()` (validates bearer tokens). They are different features. - Missing the starter dependency → `oauth2ResourceServer` method not available. - `issuer-uri` requires the app to reach the auth server at startup; a locked-down/offline environment may need `jwk-set-uri` plus a public-key or a restart-tolerant setup.
- What is the difference between oauth2Login() and oauth2ResourceServer()?oauth2Login() makes the app an OAuth2/OIDC client: it runs the authorization-code login flow, establishes a session, and is for browser sign-in. oauth2ResourceServer() makes the app validate incoming bearer tokens on API calls; it is stateless and does not perform any login.
- If you set neither jwk-set-uri nor issuer-uri, what happens?Boot cannot auto-configure a JwtDecoder, so the .jwt() configuration has no decoder bean and the context fails to start (or you must supply a JwtDecoder @Bean manually).
saying these in an interview costs you the question
- Thinking oauth2ResourceServer().jwt() logs the user in or creates a session
- Believing the resource server calls the authorization server on every request (JWT is validated locally via cached JWKs)
- Saying a JWT can be revoked instantly without introspection or short expiry
- Confusing oauth2Login with oauth2ResourceServer