skip to content

Resource Server: JWT

A resource server validates the bearer JWT's signature against a JWK set and converts its claims into authorities. Interviewers ask how a scope becomes a GrantedAuthority, and whether you verified the issuer and audience or only the signature.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

questions

5

How do you configure a Spring Security application to act as an OAuth2 Resource Server that accepts JWT bearer tokens?

level: juniorimportance: must knowfreq 78%

answer

  1. Bearer header -> validate, don't log in
  2. starter-oauth2-resource-server
  3. oauth2ResourceServer().jwt()
  4. issuer-uri or jwk-set-uri
  5. NimbusJwtDecoder + JwtAuthenticationToken

basics

~10 s

In 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 s

A 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
java
@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/katajob

go deeper

for a junior

Know the header is Authorization: Bearer, the two config properties, and that .jwt() enables validation.

for a middle

Explain the stateless request lifecycle and the decoder/provider/filter split.

for a senior

Contrast issuer-uri vs jwk-set-uri, discuss revocation limits and pairing with authorization rules.

for a principal

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

context

open as a page

How does NimbusJwtDecoder verify a JWT's signature, and what is the difference between configuring it with jwk-set-uri versus issuer-uri?

level: middleimportance: must knowfreq 70%

basics

~20 s

NimbusJwtDecoder downloads the authorization server's public keys (the JWK set), picks the key matching the token header's kid, and checks the signature. jwk-set-uri points straight at the keys; issuer-uri discovers the jwks URL and also validates the iss claim.

open as a page

How does a JWT's scopes/claims become Spring Security GrantedAuthorities, and how do you customize that mapping with JwtAuthenticationConverter?

level: seniorimportance: must knowfreq 66%

basics

~20 s

By default Spring reads the scope (or scp) claim and turns each value into a SCOPE_<value> authority. To use different claims — like Keycloak realm roles — supply a JwtAuthenticationConverter with a custom JwtGrantedAuthoritiesConverter (prefix + claim name) via oauth2ResourceServer().jwt().jwtAuthenticationConverter(...).

open as a page

Beyond signature verification, what claims should a JWT resource server validate, and how do you add a custom validator such as an audience check?

level: seniorimportance: should knowfreq 52%

basics

~10 s

Validate exp/nbf (timestamps) and iss (issuer) — the defaults cover these when you use issuer-uri. Add an audience (aud) check with a custom OAuth2TokenValidator<Jwt> combined via DelegatingOAuth2TokenValidator and set it on the NimbusJwtDecoder.

open as a page

As a platform architect, how would you handle multi-tenant JWT issuers and decide between local JWT validation and opaque-token introspection?

level: principalimportance: nice to knowfreq 34%

basics

~20 s

For many issuers, use a JwtIssuerAuthenticationManagerResolver so each tenant's iss routes to its own decoder. Choose local JWT validation for speed and offline verification; choose opaque-token introspection when you need instant revocation and central control, at the cost of a network call per request.

open as a page