Add spring-boot-starter-oauth2-resource-server and configure spring.security.oauth2.resourceserver.jwt.issuer-uri
Use NimbusJwtDecoder with public key or JWKS URI for JWT validation
Annotate with @EnableWebSecurity and call .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
Protect endpoints with hasAuthority("SCOPE_read") or hasRole using converted claims
Use opaque token introspection when tokens are not self-contained JWTs
✦ Definition~90s read
What is OAuth2 with Spring Security?
An OAuth2 Resource Server is any service that accepts access tokens and serves protected resources based on the claims those tokens carry. In Spring Boot, the spring-boot-starter-oauth2-resource-server auto-configures a security filter chain that intercepts incoming HTTP requests, extracts the Authorization: Bearer <token> header, validates the token cryptographically (for JWTs) or via network call (for opaque tokens), and populates the SecurityContext with an Authentication object derived from the token claims.
★
Think of OAuth2 like a nightclub with a wristband system.
NimbusJwtDecoder is the default JWT decoder implementation. It fetches the authorization server's JSON Web Key Set (JWKS) endpoint on startup and caches public keys. When a JWT arrives, it verifies the signature using the matching key (identified by kid), checks exp, nbf, iss, and aud claims, and converts the payload into a Jwt object.
Spring Security's JwtAuthenticationConverter then maps scope or scp claims to GrantedAuthority objects prefixed with SCOPE_.
Opaque token introspection follows RFC 7662. Instead of verifying a signature locally, Spring POST to the introspection endpoint with the token value and client credentials, parses the JSON response, and builds the Authentication from the active token's claims. This approach trades latency for flexibility — tokens can be revoked instantly without waiting for expiry.
Plain-English First
Think of OAuth2 like a nightclub with a wristband system. The authorization server (bouncer at the door) issues wristbands (tokens) after checking your ID. Your app is the bar inside — it only checks the wristband, not your ID again. JWT wristbands have the info printed right on them; opaque tokens need to be verified with the main bouncer every time.
You've set up Spring Security before — form login, in-memory users, the works. Then your team adopts microservices and suddenly every service needs to validate tokens issued by a central authorization server. You add a Bearer token check by hand, store user info in a custom header, and pray nothing changes. It breaks in staging because you forgot to update the JWKS URI. This is the production pain that spring-boot-starter-oauth2-resource-server was built to eliminate.
The OAuth2 Resource Server starter ships with everything needed to validate JWTs cryptographically, decode claims into a JwtAuthenticationToken, and expose those claims to your @PreAuthorizeannotations. You get automatic JWKS rotation, clock skew handling, and a security filter chain that rejects unsigned or expired tokens before your business logic ever runs.
In microservice architectures the story gets more nuanced. A gateway receives a user token and needs to relay it downstream. Downstream services need to validate it independently without coupling to the gateway. Spring's token relay support and SecurityContext propagation handle this cleanly — but only if you configure the resource server correctly on every service.
Opaque tokens add another dimension. When the authorization server cannot embed claims in the token (for example, when the token is a random UUID stored server-side), each resource server must call the introspection endpoint. Spring Security 6 supports both flows with nearly identical configuration.
This guide walks through production-ready Resource Server configuration: NimbusJwtDecoder setup, JWKS rotation, scope extraction, custom claims conversion, OAuth2 Login integration with GitHub and Google, and token relay patterns for microservices. Every code sample runs on Spring Boot 3.x with Java 17+.
Setting Up the OAuth2 Resource Server
Adding spring-boot-starter-oauth2-resource-server to your pom.xml pulls in Spring Security's full OAuth2 stack including NimbusJwtDecoder, JWT validation filters, and auto-configuration. With a single property — spring.security.oauth2.resourceserver.jwt.issuer-uri — Spring Boot fetches the OpenID Connect discovery document, extracts the JWKS URI, and configures the decoder automatically.
For production, always declare a SecurityFilterChain bean explicitly rather than relying on the auto-configured one. This gives you control over which endpoints require authentication, what scopes are needed, and how exceptions are handled. The auto-configured chain requires every endpoint to be authenticated — a common surprise when you add an /actuator/health endpoint.
The oauth2ResourceServer DSL in Spring Security 6 uses lambdas. Pass a customizer to jwt() to override the decoder, set audience validation, or add custom claim validators. The JwtDecoder bean is separate from the filter chain — you can override just the decoder while keeping the rest of the auto-configuration.
Always set audiences validation in production. Without it, a token issued for a different resource server (but signed by the same IdP) is accepted by your service. Use JwtValidators.createDefaultWithIssuer() combined with new AudienceValidator(Set.of("my-api")) and wrap both in DelegatingOAuth2TokenValidator.
Always validate the audience claim
Without audience validation, a token issued for service-A is valid at service-B if both share the same IdP. Always add AudienceValidator in production to prevent token misuse across services.
Production Insight
Set spring.security.oauth2.resourceserver.jwt.jwk-set-uri directly in staging to bypass discovery and reduce startup latency by ~200ms.
Key Takeaway
Explicitly configure JwtDecoder with audience validation and a custom JwtAuthenticationConverter — never rely on defaults in production.
thecodeforge.io
Spring Security Oauth2
Scope-Based Authorization with @PreAuthorize
Spring Security's method security integrates cleanly with OAuth2 scopes. Once you enable @EnableMethodSecurity, you can place @PreAuthorize("hasAuthority('SCOPE_read')") on service methods, controllers, or even repository methods. This pushes authorization logic close to the domain rather than scattering it across the security filter chain configuration.
For complex authorization rules combining scopes with business data (e.g., a user can only read their own orders), use @PostAuthorize or Spring Security's @PreAuthorize with SpEL expressions accessing the Authentication object. Extract the subject (sub) claim via authentication.name or directly from the JwtAuthenticationToken.
A common pattern is to create a custom @CurrentUser annotation backed by @AuthenticationPrincipal. This gives controller methods clean access to the JWT claims without manually extracting from the SecurityContext. Pair this with a custom principal class that wraps the JWT and exposes typed claim accessors.
Scope hierarchies are not natively supported by Spring Security's OAuth2 layer, but you can implement them by expanding scopes in a custom JwtGrantedAuthoritiesConverter. For example, admin scope implicitly grants read and write — your converter can synthesize all implied authorities when it detects the admin scope.
Use @EnableMethodSecurity not @EnableGlobalMethodSecurity
In Spring Boot 3.x, @EnableGlobalMethodSecurity is deprecated. Use @EnableMethodSecurity(prePostEnabled = true) which also enables @PostAuthorize and @Secured by default.
Production Insight
Avoid putting authorization logic purely in the filter chain for microservices — method security survives refactoring and is easier to test in isolation.
Key Takeaway
@PreAuthorize with scope-based authorities gives fine-grained, testable authorization that survives refactoring better than URL-pattern matching.
Opaque Token Introspection
When the authorization server issues random reference tokens instead of JWTs, the resource server cannot validate them locally. Spring Security supports RFC 7662 introspection out of the box. Configure spring.security.oauth2.resourceserver.opaquetoken.introspection-uri, client-id, and client-secret, then switch the DSL to .oauth2ResourceServer(oauth2 -> oauth2.opaqueToken(...)). Spring posts the token to the introspection endpoint and maps the response to OAuth2AuthenticatedPrincipal.
The cost is a network round-trip per request. In high-throughput services this is significant. Mitigate with a short-lived local cache: wrap the default OpaqueTokenIntrospector with a Caffeine-backed decorator that caches introspection responses keyed by token hash for up to the token's remaining TTL.
Custom claims in the introspection response are automatically available on OAuth2AuthenticatedPrincipal.getAttribute("custom_claim"). If you need to transform introspection response claims into GrantedAuthority objects, implement a custom OpaqueTokenIntrospector that delegates to NimbusOpaqueTokenIntrospector and then post-processes the result.
Security note: always hash the token before using it as a cache key. Storing raw tokens in memory or logs is a security anti-pattern — if your cache is dumped, attackers gain valid tokens.
Never log or store raw tokens
Always hash tokens (SHA-256) before using as cache keys, log keys, or storing in Redis. A raw token is equivalent to a plaintext password.
Production Insight
Add a circuit breaker around the introspection endpoint — if the IdP is down and you have no cache, all requests fail. Fail-open only for internal services with network-level security.
Key Takeaway
Cache introspection responses keyed by token hash to reduce latency; wrap the cache in a circuit breaker to survive IdP downtime gracefully.
thecodeforge.io
Spring Security Oauth2
OAuth2 Login with GitHub and Google
Spring Boot's spring-boot-starter-oauth2-client enables OAuth2 Login — delegating user authentication to external providers like GitHub or Google. Unlike the Resource Server (which validates tokens), OAuth2 Login initiates the authorization code flow, exchanges the code for an access token, fetches user info, and creates a Spring Security session backed by OAuth2User or OidcUser.
Register each provider under spring.security.oauth2.client.registration. Spring Boot pre-configures commonOAuth2Provider entries for Google, GitHub, Facebook, and Okta — you only need to supply client-id and client-secret. For custom IdPs, define a provider block with authorization URI, token URI, and user-info URI.
Extract the authenticated user's information in controllers using @AuthenticationPrincipal OAuth2User for standard OAuth2 providers or @AuthenticationPrincipal OidcUser for OIDC providers like Google. The OidcUser exposes standard claims directly (user.getEmail(), user.getName()) without manual attribute mapping.
For microservices combining OAuth2 Login at the gateway with JWT-based downstream services, implement token exchange: after the user authenticates via OAuth2 Login, issue an internal JWT containing the user's id, email, and roles, then propagate that JWT in service-to-service calls. This decouples downstream services from the external OAuth2 provider.
GitHub may return null email
If a GitHub user has a private email, the email attribute will be null. Call the GitHub /user/emails API separately using the access token to retrieve the verified primary email.
Production Insight
Store the OAuth2 registrationId alongside the user record in your database to support multiple providers per user account (social account linking).
Key Takeaway
Use OAuth2UserService to bridge external OAuth2 identities to your local user model, assigning roles from your own database rather than trusting provider claims blindly.
Token Relay in Microservices
In a microservice architecture, the gateway authenticates the user and receives a JWT. Downstream services need that token to authorize requests independently. Token relay means forwarding the original access token from the incoming request to outgoing service calls.
Spring Security's OAuth2AuthorizedClientManager combined with ServerOAuth2AuthorizedClientExchangeFilterFunction (for reactive stacks) or a ClientHttpRequestInterceptor (for servlet stacks) handles token relay automatically for client credentials flow. For user-context relay, extract the token from the SecurityContext and inject it into WebClient or RestTemplate headers.
The spring-cloud-starter-gateway with TokenRelayGatewayFilterFactory provides turnkey relay: add - TokenRelay= to your route filters and the gateway appends Authorization: Bearer <token> to all proxied requests. Downstream services configured as Resource Servers validate the token independently.
For service-to-service calls without user context (batch jobs, background tasks), use the Client Credentials grant. Configure a separate OAuth2 client registration with authorization-grant-type: client_credentials and inject OAuth2AuthorizedClientManager to obtain and auto-refresh tokens. The manager handles token expiry transparently.
Avoid token relay to external services
Only relay tokens to services within your trust boundary. Never forward a user's access token to third-party APIs — it grants those APIs the ability to act on behalf of your user.
Production Insight
Use short-lived client credentials tokens (< 5 min TTL) for service-to-service calls and rely on OAuth2AuthorizedClientManager for automatic refresh — never hardcode or cache tokens manually.
Key Takeaway
Token relay keeps the user's security context intact through the entire service call chain; use client credentials for background service-to-service calls without user context.
Testing OAuth2 Resource Server Endpoints
Testing secured endpoints requires injecting mock tokens or using Spring Security's test support. @WithMockUser does not work for OAuth2 Resource Servers because it creates a UsernamePasswordAuthenticationToken, not a JwtAuthenticationToken. Use SecurityMockMvcRequestPostProcessors.jwt() instead, which builds a real JwtAuthenticationToken with configurable claims and authorities.
For integration tests, @SpringBootTest with @AutoConfigureMockMvc combined with jwt() post-processor gives full coverage of the security filter chain without a real authorization server. You can set specific JWT claims, authorities, and subject to simulate different user roles.
For actual token validation (testing the decoder configuration), use Testcontainers with a real Keycloak instance or WireMock to stub the JWKS endpoint and issue real JWTs signed with a test key. This validates your audience and issuer configuration end-to-end.
Spring Security's @WithSecurityContext allows creating custom annotations that combine jwt() setup with role assignments, making tests readable: @WithJwtUser(subject = "user-123", scopes = {"orders:read"}) is self-documenting and avoids repetitive jwt().jwt(...) builder chains in every test.
Test 401 and 403 separately
401 means unauthenticated (no token); 403 means authenticated but unauthorized (wrong scope). Test both cases explicitly — they have different root causes and fixes.
Production Insight
Add a test that verifies your endpoint returns WWW-Authenticate: Bearer header on 401 — some API clients require this header to trigger token refresh logic.
Key Takeaway
Use SecurityMockMvcRequestPostProcessors.jwt() for unit tests and Testcontainers + Keycloak for full integration testing of your OAuth2 Resource Server configuration.
Why You Need a Custom Token Converter (and When You Don't)
The default Spring Security token converter works fine for standard claims. But real auth servers send custom claims—tenant IDs, custom roles, or department codes. You cannot rely on the default mapper when your authorization server returns something like { "sub": "user", "x-org": "acme", "x-roles": ["admin"] }. The default converter will silently drop those fields. You must write a custom OAuth2TokenConverter that maps those claims into your Authentication object. This is not optional. If you don't do it, your @PreAuthorize annotations will see an empty authority list, and every request will be denied. The fix is a simple OAuth2TokenValidator and OAuth2TokenConverter implementation. Start with a claim set adapter that reads your custom fields and populates GrantedAuthority objects. Then register it as a bean. Nothing else changes.
2025-03-19T10:00:00.000Z INFO 12345 --- [ restartedMain] c.t.c.CustomTokenConverter: Registered custom converter for org=acme
Production Trap:
If you use @PreAuthorize with custom claims and skip the converter, your endpoints will silently return 403. Add a log line in the converter to verify claims are parsed every time.
Key Takeaway
Never trust the default token converter. If your auth server sends custom claims, write a converter.
OAuth2 Client Credentials Flow: The Server-to-Server Pattern
Most tutorials show the authorization code flow for user login. But microservices talk to each other using the client credentials flow—no user involved. Your service needs a client ID and client secret, not a browser redirect. Configure a ClientRegistration with authorizationGrantType=CLIENT_CREDENTIALS and a tokenUri. Then use OAuth2AuthorizedClientManager to get a token before calling another service. This flow is stateless and does not require user consent. The token expires. Your code must handle token refresh automatically. Spring's WebClient with the ServletOAuth2AuthorizedClientExchangeFilterFunction does this for you. You just need one bean: OAuth2AuthorizedClientManager. Wire it into your WebClient builder. Then every outbound request gets a fresh token. No manual token fetching.
2025-03-19T10:00:01.000Z INFO 12346 --- [ main] c.t.c.ClientCredentialsConfig: Client credentials provider registered for client registration 'my-service'
Production Optimization:
Set token expiry buffer to 30 seconds. Spring's default refresh is lazy—if your token expires mid-request, you get a 401. Configure a ScheduledExecutorService to refresh tokens proactively.
Key Takeaway
Client credentials flow is for machine-to-machine communication. Configure it once, and your services will never ask for user credentials.
● Production incidentPOST-MORTEMseverity: high
JWKS Cache Miss After Key Rotation Caused 401 Storms
Symptom
Spike of 401 Unauthorized responses starting at 02:03 UTC. All requests with tokens issued after 02:00 failed; tokens issued before succeeded. No deployment had occurred.
Assumption
The team assumed JWKS rotation was seamless because Spring Security fetches keys automatically.
Root cause
The default NimbusJwtDecoder caches keys for up to 5 minutes and only refreshes when a kid is unknown. The authorization server rotated to a new key but kept the old kid value (a misconfiguration on the IdP side). Spring never triggered a cache refresh and kept failing validation with the stale key.
Fix
Configured a custom NimbusJwtDecoder with jwsAlgorithm set explicitly and added a RestOperationsResourceRetriever with a 30-second cache TTL. Also fixed the IdP to use unique kid values per key. Added a health check that pre-validates a test JWT on startup.
Key lesson
Never assume JWKS rotation is transparent.
Test key rotation in staging with realistic TTLs.
Always ensure kid values are unique across key versions, and configure a maximum cache age explicitly.
Production debug guideSymptom → root cause → fix5 entries
Symptom · 01
401 Unauthorized on all requests
→
Fix
Check spring.security.oauth2.resourceserver.jwt.issuer-uri is reachable from the service. Run curl <issuer-uri>/.well-known/openid-configuration inside the container. If the issuer URI is wrong or unreachable, the decoder fails to initialize and rejects all tokens. Fix the URI and ensure network policies allow the JWKS fetch.
Symptom · 02
403 Forbidden despite valid token
→
Fix
Print the token at jwt.io and check the scope or scp claim. Spring prefixes scopes with SCOPE_, so a scope of read becomes SCOPE_read. Verify your hasAuthority call uses the prefixed form. If you use hasRole, ensure your JwtAuthenticationConverter maps claims to roles with the ROLE_ prefix.
Symptom · 03
Token validates locally but fails in production
→
Fix
Compare the iss claim in the token against spring.security.oauth2.resourceserver.jwt.issuer-uri. A trailing slash mismatch (https://auth.example.com vs https://auth.example.com/) will cause issuer validation to fail. Also verify clock synchronization — a clock skew greater than 5 seconds causes nbf/exp failures.
Symptom · 04
NullPointerException in custom JwtAuthenticationConverter
→
Fix
The scope claim may be absent or formatted differently (space-separated string vs array). Guard with jwt.hasClaim("scope") ? ... : Collections.emptyList(). Check whether your IdP uses scope or scp as the claim name. Add a unit test that exercises the converter with a minimal JWT containing only the claims your code touches.
Symptom · 05
Opaque token introspection returns 401 from the introspection endpoint
→
Fix
Verify spring.security.oauth2.resourceserver.opaquetoken.client-id and client-secret are correct. Use curl -u client:secret -d 'token=<value>' <introspection-uri> to test independently. Ensure the introspection endpoint returns {"active":true} for valid tokens; some IdPs return 200 with active:false instead of 401.
★ Debug Cheat SheetFast commands for diagnosing OAuth2 Resource Server issues in production.
401 on every request−
Immediate action
Verify JWKS endpoint reachability and issuer URI config
Ensure kid in JWT header matches a key in the JWKS endpoint; configure unique kid per key rotation
JWT vs Opaque Token Introspection
Aspect
JWT (Self-Contained)
Opaque Token (Introspection)
Validation
Local crypto verify
Network call to IdP
Latency
< 1ms
5-50ms per request
Revocation
Wait for expiry
Instant via IdP
Offline validation
Yes
No
Claims
Embedded in token
Returned by introspection response
Token size
200-500 bytes
16-32 bytes
IdP dependency
Only on key rotation
Every request
Recommended for
High-throughput APIs
Security-critical, low-latency-tolerant APIs
⚙ Quick Reference
2 commands from this guide
File
Command / Code
Purpose
CustomTokenConverter.java
@Component
Why You Need a Custom Token Converter (and When You Don't)
ClientCredentialsConfig.java
@Configuration
OAuth2 Client Credentials Flow
Key takeaways
1
Always validate aud (audience) claim to prevent token misuse across services sharing the same IdP.
2
NimbusJwtDecoder relies on unique kid values for JWKS cache invalidation
ensure your IdP uses distinct key IDs per rotation.
3
Scope-based authorities use the SCOPE_ prefix
hasAuthority('SCOPE_read') not hasRole('read').
4
Opaque token introspection trades latency for instant revocability
always cache responses keyed by token hash.
5
Use SessionCreationPolicy.STATELESS and disable CSRF for all REST APIs secured with JWT.
Common mistakes to avoid
6 patterns
×
Using `hasRole('ADMIN')` instead of `hasAuthority('SCOPE_admin')`
Symptom
403 Forbidden even though the token contains the correct scope
Fix
OAuth2 scopes are mapped to authorities with SCOPE_ prefix. Use hasAuthority('SCOPE_admin') or configure a custom converter that maps scopes to ROLE_ prefix.
×
Forgetting to validate the `aud` (audience) claim
Symptom
Tokens issued for other services are accepted by your API
Fix
Add AudienceValidator to DelegatingOAuth2TokenValidator in your JwtDecoder bean.
×
Not setting `SessionCreationPolicy.STATELESS`
Symptom
Resource server creates HTTP sessions, causing memory leak and horizontal scaling issues
Fix
Always set .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) in Resource Server configurations.
×
Enabling CSRF with stateless JWT authentication
Symptom
POST/PUT/DELETE requests fail with 403 CSRF token missing
Fix
Disable CSRF for stateless APIs: .csrf(csrf -> csrf.disable()). CSRF protection is only needed for browser-based session authentication.
×
Hardcoding the JWKS URI instead of using issuer-uri discovery
Symptom
Service breaks when IdP rotates or changes JWKS URI
Fix
Use spring.security.oauth2.resourceserver.jwt.issuer-uri to enable OpenID Connect discovery, which fetches the JWKS URI dynamically.
×
Not handling token relay in async/reactive contexts
Symptom
Downstream service calls fail with 401 because the SecurityContext is not propagated across threads
Fix
Use ReactiveSecurityContextHolder in reactive stacks or DelegatingSecurityContextExecutor in servlet stacks to propagate context across async boundaries.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01JUNIOR
What is the difference between an OAuth2 Resource Server and an Authoriz...
Q02SENIOR
How does NimbusJwtDecoder handle JWKS key rotation?
Q03JUNIOR
Explain how scope-based authorization works in Spring Security OAuth2.
Q04SENIOR
How would you prevent token misuse across different microservices sharin...
Q05JUNIOR
Describe the OAuth2 Authorization Code flow for a web app using Spring B...
Q06SENIOR
How do you implement token relay in a Spring Cloud Gateway setup?
Q07SENIOR
What is the security risk of using opaque token introspection without ca...
Q08SENIOR
How would you implement token exchange — converting a user JWT from an e...
Q09SENIOR
How do you test a Spring Security OAuth2 Resource Server endpoint withou...
Q01 of 09JUNIOR
What is the difference between an OAuth2 Resource Server and an Authorization Server?
ANSWER
The Authorization Server issues tokens (handles login, consent, token issuance). The Resource Server accepts tokens and serves protected resources. In Spring Boot, spring-boot-starter-oauth2-resource-server configures a Resource Server; Spring Authorization Server is a separate project for building Authorization Servers.
Q02 of 09SENIOR
How does NimbusJwtDecoder handle JWKS key rotation?
ANSWER
NimbusJwtDecoder fetches the JWKS endpoint on startup and caches keys. When a JWT arrives with an unknown kid, it triggers a JWKS refresh. Key rotation is seamless only if the new key uses a different kid. If the IdP reuses kid values, the cache is never invalidated and new tokens fail — requiring explicit cache TTL configuration.
Q03 of 09JUNIOR
Explain how scope-based authorization works in Spring Security OAuth2.
ANSWER
The JwtGrantedAuthoritiesConverter extracts the scope claim (space-separated or array) and maps each value to a GrantedAuthority with the SCOPE_ prefix. hasAuthority('SCOPE_read') in security rules then matches. You can customize the claim name (e.g., scp) and prefix in the converter.
Q04 of 09SENIOR
How would you prevent token misuse across different microservices sharing the same IdP?
ANSWER
Validate the aud (audience) claim. Each service should have a unique audience value, and the JWT must include that service's audience. Implement AudienceValidator in a DelegatingOAuth2TokenValidator to reject tokens not intended for the current service.
Q05 of 09JUNIOR
Describe the OAuth2 Authorization Code flow for a web app using Spring Boot.
ANSWER
1) User visits protected resource, is redirected to IdP login. 2) User authenticates, IdP redirects back with an authorization code. 3) Spring exchanges the code for access+id tokens at the token endpoint (server-to-server, never exposed to browser). 4) Spring fetches user info, creates OAuth2User, and establishes a session. The code is single-use and the token exchange happens server-side.
Q06 of 09SENIOR
How do you implement token relay in a Spring Cloud Gateway setup?
ANSWER
Add - TokenRelay= to the route's filters list in the gateway configuration. The TokenRelayGatewayFilterFactory extracts the OAuth2AuthorizedClient from the ReactiveSecurityContext and appends the Authorization: Bearer header to all proxied requests. Downstream services validate the token as a standard Resource Server.
Q07 of 09SENIOR
What is the security risk of using opaque token introspection without caching, and how do you mitigate it?
ANSWER
Without caching, every request triggers a network call to the IdP. A slow or down IdP causes latency spikes or total outage. Mitigation: cache introspection responses for a short TTL (e.g., 60 seconds), keyed by the SHA-256 hash of the token (never the raw token). Add a circuit breaker to fail gracefully when the IdP is unavailable.
Q08 of 09SENIOR
How would you implement token exchange — converting a user JWT from an external IdP to an internal service token?
ANSWER
Implement RFC 8693 Token Exchange. The gateway validates the external JWT, then requests a new token from the internal Authorization Server using grant_type=urn:ietf:params:oauth:grant-type:token-exchange with the external token as subject_token. The internal token carries the user identity plus internal roles. Downstream services only trust the internal IdP, never the external one directly.
Q09 of 09SENIOR
How do you test a Spring Security OAuth2 Resource Server endpoint without a real IdP?
ANSWER
Use SecurityMockMvcRequestPostProcessors.jwt() to inject a JwtAuthenticationToken directly into the security context. Configure claims and authorities on the builder. For full integration testing of decoder configuration, use WireMock to stub the JWKS endpoint and sign test JWTs with a generated RSA key pair.
01
What is the difference between an OAuth2 Resource Server and an Authorization Server?
JUNIOR
02
How does NimbusJwtDecoder handle JWKS key rotation?
SENIOR
03
Explain how scope-based authorization works in Spring Security OAuth2.
JUNIOR
04
How would you prevent token misuse across different microservices sharing the same IdP?
SENIOR
05
Describe the OAuth2 Authorization Code flow for a web app using Spring Boot.
JUNIOR
06
How do you implement token relay in a Spring Cloud Gateway setup?
SENIOR
07
What is the security risk of using opaque token introspection without caching, and how do you mitigate it?
SENIOR
08
How would you implement token exchange — converting a user JWT from an external IdP to an internal service token?
SENIOR
09
How do you test a Spring Security OAuth2 Resource Server endpoint without a real IdP?
SENIOR
FAQ · 6 QUESTIONS
Frequently Asked Questions
01
Can I use both JWT and opaque tokens in the same application?
Yes. Configure two security filter chains with different securityMatchers. One chain handles JWT tokens (e.g., for API endpoints), another handles opaque tokens (e.g., for admin endpoints). Each chain gets its own oauth2ResourceServer configuration.
Was this helpful?
02
How do I add custom claims to the JWT and access them in Spring Security?
Configure your authorization server to include custom claims in the JWT payload. In Spring Security, access them via jwt.getClaim('custom_claim') in controllers, or map them to authorities in a custom JwtGrantedAuthoritiesConverter. Custom claims are available on the Jwt object injected via @AuthenticationPrincipal.
Was this helpful?
03
What happens if the JWKS endpoint is down when the application starts?
The JwtDecoder bean fails to initialize, causing the application context to fail startup. Use spring.security.oauth2.resourceserver.jwt.jwk-set-uri with a retry mechanism, or configure a NimbusJwtDecoder with lazy initialization. In Kubernetes, add an initContainer that waits for the IdP to be healthy before starting the app.
Was this helpful?
04
How do I support multiple issuers (multi-tenant)?
Implement a JwtDecoder that inspects the iss claim and delegates to the appropriate tenant-specific decoder. Use a ConcurrentHashMap<String, JwtDecoder> keyed by issuer URI, creating decoders lazily. Set this custom decoder on the oauth2ResourceServer JWT DSL.
Was this helpful?
05
Should I use OAuth2 Login or Resource Server for a REST API?
Use Resource Server for REST APIs consumed by other services or SPAs — it validates Bearer tokens. Use OAuth2 Login for browser-based apps that redirect users to an IdP for authentication — it manages sessions and user info. Many apps use both: Resource Server for the API layer, OAuth2 Login for the frontend.
Was this helpful?
06
How do I handle token expiry gracefully on the client side?
Implement silent token refresh: detect 401 responses in your HTTP interceptor, use the refresh token to obtain a new access token, replay the failed request. For SPAs, use short-lived access tokens (15 min) and trigger refresh 1 minute before expiry. On the server side, always return WWW-Authenticate: Bearer error=invalid_token in 401 responses to signal token expiry vs. other errors.