JWT, OAuth 2.0, and Resource Servers
A Spring Boot resource server protects APIs by accepting OAuth 2.0 access tokens and deciding whether each request may reach a controller. In this lesson, the resource server is not the login screen and not the identity provider. It is the application that receives a bearer token, validates it, turns token claims into Spring Security authorities, and applies authorization rules to endpoints.
The outcome is practical: you should be able to configure JWT validation, explain what happens inside the security filter chain, choose between issuer-based discovery and explicit keys, map provider-specific claims, and diagnose failed requests without guessing. This fits the Spring Boot security section because most real services eventually need to trust an external authorization server while keeping business endpoints small and testable.
OAuth 2.0 Roles and JWT Purpose
OAuth 2.0 defines an authorization flow, not a token format. The main roles are the resource owner, client, authorization server, and resource server. A user or service authorizes a client. The authorization server issues an access token. The client sends that token to the resource server in an HTTP header such as Authorization: Bearer eyJ.... The resource server validates the token and enforces access.
A JWT is a compact signed document with three base64url sections: header, payload, and signature. The header usually says which algorithm and key id were used. The payload contains claims such as issuer, subject, audience, expiration, and scope. The signature lets the resource server detect tampering. With asymmetric signing, the authorization server signs with a private key while resource servers verify with a public key from a JWK set.
A resource server normally validates at least four things: the signature is valid, the token has not expired, the issuer is the expected authorization server, and the token contains claims that satisfy application authorization rules. Some systems also validate audience, tenant, azp, organization, or custom entitlements. Spring Security gives you the validation machinery, but you still own the meaning of those claims for your API.
What Spring Boot Builds Internally
When the OAuth 2.0 resource server starter is on the classpath, Spring Boot can auto-configure JWT support from properties. A request first enters the servlet filter chain. The bearer token authentication filter looks for an Authorization header using the Bearer scheme. If no token is present, the request remains anonymous until authorization rules decide whether that is allowed. If a token is present, the filter asks an authentication manager to authenticate it.
For JWTs, authentication delegates to a decoder. The decoder obtains verification keys, checks the signature, parses claims, and runs validators. Issuer-based configuration commonly uses OpenID Connect discovery: Spring reads metadata from the issuer and locates the JWK set URI. Keys are cached and refreshed as needed. After validation, a JWT authentication converter creates a Spring Authentication object. By default, scopes often become authorities named like SCOPE_read. Endpoint rules then compare those authorities with requirements such as hasAuthority("SCOPE_orders.read").
This separation matters. Signature verification answers, “Was this token issued by the trusted authority and left unchanged?” Claim validation answers, “Is this token acceptable now and for this API?” Authorization answers, “Does this authenticated principal have permission for this operation?” Keeping those questions separate makes failures easier to diagnose.
Configuration Anatomy
The smallest common configuration names the issuer. Spring can discover the signing keys and validate the issuer claim from that value.
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://issuer.example.com/realms/training
That property is concise, but it makes startup and key retrieval dependent on the authorization server metadata endpoint. If your platform does not allow discovery at startup, you can configure a JWK set URI directly. Doing so still validates signatures, but you must also ensure issuer validation is configured appropriately for your security requirements.
The security filter chain states which requests are public, which require authentication, and which require authorities. It should be specific enough that accidental new endpoints do not become public.
@Bean
SecurityFilterChain apiSecurity(HttpSecurity http) throws Exception {
return 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")
.anyRequest().authenticated())
.oauth2ResourceServer(oauth -> oauth.jwt(Customizer.withDefaults()))
.build();
}
Notice the default-deny shape: health is public, two API patterns have explicit scope requirements, and everything else requires authentication. In a larger service, method security can add object-level checks, but request rules are still useful for broad API boundaries.
Example 1: A Read-Only Orders API
Assume an access token contains a scope claim of orders.read. Spring’s default JWT converter represents that as SCOPE_orders.read. A GET request to /api/orders/42 succeeds only if that authority exists. A request without a bearer token receives a 401 response. A token that is valid but lacks the scope receives a 403 response. That distinction is important: 401 means authentication failed or is missing; 403 means authentication succeeded but authorization denied the action.
The deterministic behavior is: public health returns 200 without a token, GET orders returns 200 with orders.read, POST orders returns 403 with only orders.read, and malformed bearer tokens return 401 before the controller runs.
Example 2: Mapping Provider Claims
Providers do not all use the same claim names. One identity platform may put permissions in scope, another in scp, and another in roles. Spring’s defaults are useful, but production APIs often need an explicit converter so the application contract is not hidden inside provider quirks.
@Bean
JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter scopes = new JwtGrantedAuthoritiesConverter();
scopes.setAuthorityPrefix("SCOPE_");
scopes.setAuthoritiesClaimName("scope");
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(scopes);
converter.setPrincipalClaimName("sub");
return converter;
}
This converter says that the principal name comes from sub and authorities come from the space-delimited scope claim. If your tokens contain roles instead, you can write a converter that reads that collection and prefixes values with ROLE_ or another naming convention. The key design choice is to keep endpoint rules and token claim mapping consistent. A rule requiring SCOPE_admin will never match a converter that emits ROLE_admin.
Example 3: Understanding Claim Mapping Output
The following standalone Java example models the same idea without Spring dependencies. It shows why a token with read and write scopes can satisfy separate endpoint checks after the claim string is split and prefixed.
import java.util.Arrays;
import java.util.List;
public class ScopeMappingDemo {
static List<String> authoritiesFromScope(String scope) {
if (scope == null || scope.isBlank()) {
return List.of();
}
return Arrays.stream(scope.split(" "))
.filter(value -> !value.isBlank())
.map(value -> "SCOPE_" + value)
.toList();
}
public static void main(String[] args) {
System.out.println(authoritiesFromScope("orders.read orders.write"));
System.out.println(authoritiesFromScope(""));
}
}
The expected output is [SCOPE_orders.read, SCOPE_orders.write] on the first line and [] on the second line. Spring Security performs richer validation and authentication, but the authorization comparison is still based on concrete authority strings.
Design Choices and Trade-Offs
Issuer discovery is convenient and reduces duplicated configuration, but it couples the resource server to the availability and correctness of provider metadata. A direct JWK set URI can be operationally simpler in locked-down networks, but it is easier to omit issuer or audience checks by mistake. Symmetric signing is simple for small controlled systems, but every verifier must know the signing secret; asymmetric signing is usually better when several resource servers validate tokens.
Short token lifetimes limit damage after leakage, but they require clients to refresh tokens reliably. Long lifetimes reduce authorization-server traffic but keep revoked permissions alive longer unless introspection or revocation checks are added. JWT validation is fast because it is local after keys are cached, but local validation means the resource server may not know that a token was revoked a few seconds ago. Opaque token introspection gives central control at request time, but adds network latency and a dependency for every protected call.
Scopes should describe API capabilities, not UI buttons. For example, orders.read and orders.write are clearer than manager for endpoint authorization. Domain rules still belong in application code: a token may have orders.read, while the service still restricts which customer’s orders the principal may access.
Failure Modes and Troubleshooting
Symptom: every protected request returns 401 with a valid-looking token. Cause: the issuer configured in Spring does not match the token’s iss claim, or the token was issued by a different environment. Diagnose: decode the token payload without trusting it, compare iss with issuer-uri, and enable Spring Security debug logging in a local environment. Correction: use the correct issuer for the deployment and keep development, staging, and production tokens separate.
Symptom: requests started failing after key rotation. Cause: the token header contains a kid that is not available from the configured JWK set, or network rules prevent key refresh. Diagnose: inspect the JWT header, fetch the JWK set from the application network, and check whether the matching key id is present. Correction: fix the JWK URI, allow outbound access to it, or coordinate signing key rotation so old and new keys overlap.
Symptom: authenticated users receive 403 for endpoints they should access. Cause: the token claim does not map to the authority name used by the authorization rule. Diagnose: log authority names in a safe development trace, inspect whether the token uses scope, scp, or roles, and compare with hasAuthority rules. Correction: adjust the converter or the endpoint rule so both use the same naming convention.
Symptom: browser calls fail before authentication seems relevant. Cause: CORS preflight requests are blocked because OPTIONS requests are not handled. Diagnose: check the browser network panel for failed OPTIONS requests and missing CORS headers. Correction: configure CORS deliberately for allowed origins, headers, and methods; do not make every API public to hide the symptom.
Security, Performance, and Reliability Implications
Never log bearer tokens. Log a request id, principal id, issuer, audience, and failure category where appropriate, but avoid secrets and full claims. Validate audience when tokens from the same issuer may be used for multiple APIs. Use HTTPS everywhere because bearer tokens grant access to whoever possesses them. Keep authorization checks close to the endpoint and the domain operation, especially when different tenants or customers share the same service.
JWT verification is usually inexpensive compared with database calls, but key fetching and cache misses can affect cold starts. Readiness checks should not require a protected token unless you intend them to. For reliability, watch 401 and 403 rates separately; a spike in 401 often indicates client, issuer, or key problems, while a spike in 403 often indicates claim mapping or permission rollout problems.
Hands-On Lab
Prerequisites: a Spring Boot web application with Spring Security and OAuth 2.0 resource server support, Java installed, and access to an authorization server that can issue JWT access tokens. A local identity server, a corporate identity provider, or a test realm is sufficient.
- Add resource server support to the application dependencies and configure
spring.security.oauth2.resourceserver.jwt.issuer-urifor the test issuer. - Add the security filter chain from this lesson, adapting the endpoint paths to a real controller in your project.
- Create one public health endpoint or use Actuator health, then create a protected endpoint such as
GET /api/orders. - Obtain one token with
orders.readand one token without that scope. Keep them in a temporary shell variable rather than committing them. - Call the public endpoint without a token and verify HTTP 200. Call the protected endpoint without a token and verify HTTP 401.
- Call the protected endpoint with the read token and verify HTTP 200. Call a write endpoint with only the read token and verify HTTP 403.
- Change the configured issuer to an incorrect value in a local branch, restart, and verify that the same token now fails with 401. Revert the issuer change.
Cleanup: delete temporary token variables, stop any local identity server containers, remove test users or clients that were created only for the lab, and restore the original issuer configuration. Verification is complete when the application again returns 200 for the public endpoint, 401 without a token, 403 with insufficient scope, and 200 with the required scope.
Assessment Exercises
- A token has a valid signature and
orders.read, but your endpoint requiresSCOPE_order.read. Predict the HTTP response and explain the exact mismatch. - Compare JWT validation with opaque token introspection for an API that must honor permission revocation within seconds. Which dependency and latency trade-offs change?
- Design authority names for an invoice API with separate read, submit, approve, and refund actions. Which checks belong in scopes and which belong in domain logic?
- A deployment suddenly emits many 401 responses after an identity-provider maintenance window. List the first three facts you would inspect and why.
- Explain why a resource server should not accept any token from a trusted issuer unless audience or equivalent API targeting is considered.
Summary
A Spring Boot OAuth 2.0 resource server validates bearer JWTs before protected controllers run. The core path is header extraction, JWT decoding, signature and claim validation, authority conversion, and endpoint authorization. Good implementations make issuer, key source, audience, scope naming, and failure behavior explicit. That precision turns token-based security from a black box into a diagnosable part of the service.
