Skip to content
Ashish.
All posts
Diagram illustrating the Spring Security OAuth2 Resource Server architecture with JWT decoder flow.

Spring Security OAuth2 Resource Server: Securing APIs

Learn how to configure a Spring Security OAuth2 resource server with JWT decoder for API security and token validation.

By Ashish SrivastavaPart 5 of Spring Security Deep Dive Series

The Mechanism of Trust: From Legacy Services to JWT Decoders

Part 5 of the Spring Security Deep Dive Series.

In legacy Spring Security architectures, securing an API relied on a ResourceServerTokenServices bean acting as a remote proxy. When a request arrived with a Bearer token, the application would make an HTTP call to the authorization server's introspection endpoint to verify validity. This introduced latency and created a single point of failure: if the introspection service was down, the API became inaccessible.

Modern Spring Security (5.7+) flips this model. It treats the token itself as a self-contained credential. The core mechanism is the JwtDecoder interface. Instead of asking "Is this token valid?" via a network call, the resource server asks "Can I cryptographically verify this token using a public key I already possess?"

This shift moves the validation logic into the application context. The NimbusJwtDecoder is the standard implementation. It takes the JWT string, parses the JWS (JSON Web Signature) header, retrieves the public key (either from a local cache or the JWKS URI), and verifies the signature. If the signature matches, the token is authentic. The decoder then extracts the claims and passes them to the security filter chain. This is a stateless, high-performance approach where the trust boundary is established locally at the time of receipt.

Configuration: The Gateway and the Issuer

Consider a concrete scenario involving two named actors: AuthServer (the issuer) and ApiGateway (the resource server). The AuthServer issues a JWT containing an iss (issuer) claim and a aud (audience) claim. Before the client can access protected resources, the OAuth2 client obtains the token from the AuthServer and includes it in the request headers sent to the ApiGateway.

To configure the ApiGateway, you do not define a custom service. You rely on Spring Boot auto-configuration driven by properties. The critical property is spring.security.oauth2.resourceserver.jwt.issuer-uri. This tells the NimbusJwtDecoder where to fetch the public keys for verification.

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://auth.example.com

When the application starts, it performs a silent GET request to https://auth.example.com/.well-known/jwks.json. It retrieves the JSON Web Key Set (JWKS), which contains the public keys (jwk-set) used to sign the tokens. The decoder caches these keys.

If the AuthServer rotates its signing keys, the ApiGateway automatically updates its cache based on the kid (key ID) in the JWT header. This ensures that even if the AuthServer rotates keys, the ApiGateway remains available without a restart, provided the rotation happens within the cache TTL. This configuration pattern is widely regarded as a best practice in backend development for building scalable, secure microservices.

Validation Logic: Audiences and Scopes

The decoder does not just check the signature. It performs strict validation of the claims. If the iss claim in the token does not match the configured issuer-uri, validation fails immediately. Similarly, if the aud claim is missing or does not include the ApiGateway's identifier, the token is rejected.

This mechanism prevents a token issued for a different service (e.g., a MobileApp) from being accepted by the ApiGateway, even if the signature is valid.

However, sometimes you need more granular control. For example, you might want to ensure the token has a specific scope. Spring Security allows you to customize the JwtDecoder or, more commonly, add an AuthenticationConverter to the filter chain.

Suppose the AuthServer issues a token with a scope claim: read:users write:users. The ApiGateway needs to ensure the user has read:users. You can configure a JwtAuthenticationConverter to map this claim to a Spring Security GrantedAuthority.

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter();
    jwtConverter.setJwtGrantedAuthoritiesConverter(new JwtGrantedAuthoritiesConverter());
 
    http
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/api/public").permitAll()
            .anyRequest().hasAuthority("SCOPE_read") // Custom authority check
        )
        .oauth2ResourceServer(oauth2 -> oauth2
            .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtConverter))
        );
    
    return http.build();
}

In this configuration, the JwtDecoder handles the cryptographic verification. The JwtAuthenticationConverter handles the semantic authorization (checking scopes). This separation of concerns is vital: the decoder ensures the token is real; the converter ensures the token grants permission for the specific action.

Failure Modes and Edge Cases

Confusion often arises when a valid token is rejected. In the mechanism-level view, this is almost always a claim mismatch.

  1. Issuer Mismatch: If the AuthServer changes its domain but the ApiGateway still points to the old issuer-uri, the iss claim check fails. The error is InvalidTokenException.
  2. Audience Mismatch: If the token is intended for MobileApp but arrives at ApiGateway, and ApiGateway expects ApiGateway in the aud list, validation fails.
  3. Key Rotation: If the AuthServer rotates keys and the ApiGateway hasn't fetched the new JWKS yet, the signature verification fails. The NimbusJwtDecoder handles this by retrying the JWKS fetch if a key ID is unknown, but prolonged outages can cause transient failures.

It is critical to understand that spring-security-oauth2-resource-server does not support the legacy opaque token introspection flow by default in the same way. If you are migrating from a system that relies on introspection, you must switch to the JWT flow or implement a custom OpaqueTokenIntrospection mechanism, which reintroduces the network call latency you sought to avoid.

The JwtDecoder mechanism is reliable because it relies on the mathematical certainty of the signature, not the availability of a remote database. As long as the public key is present and the claims match, the API is secured. This is the standard for modern microservices architectures where low latency and high availability are non-negotiable.

Opinion on Strategy

While the JWT approach is the default and recommended path for most RESTful APIs, there is a tradeoff. Once a JWT is issued, the AuthServer cannot revoke it until it expires (unless you implement a complex token blacklist or use a shorter lifetime). If you require immediate revocation capabilities (e.g., for a compromised account), the opaque token introspection flow is technically superior, despite the performance cost.

For most "stateless" API designs, however, the JWT decoder strategy is the correct choice. It aligns with the HTTP stateless nature of REST and removes the dependency on the authorization server's availability for every single request. The mechanism is simple, but the configuration details—specifically the issuer-uri and audience matching—are where the security boundary is actually drawn. Misconfiguration here renders the cryptographic verification useless.

Conclusion

Configuring a Spring Security OAuth2 resource server with a JwtDecoder shifts the security boundary from network availability to cryptographic verification. By leveraging NimbusJwtDecoder and Spring Boot auto-configuration, developers can achieve high-performance, stateless API security. Understanding the interplay between the issuer-uri, audience validation, and custom JwtAuthenticationConverter logic is essential for building robust microservices. While the JWT model introduces specific challenges regarding token revocation, its benefits in latency and availability make it the standard for modern API architectures.

Common Pitfalls

Even with auto-configuration, developers frequently stumble over specific configuration details that lead to authentication failures.

  1. Issuer URI Mismatch: A common error occurs when the issuer-uri in the resource server configuration does not exactly match the iss claim in the token. Even a trailing slash difference or a protocol mismatch (http vs https) will cause InvalidTokenException. Always ensure the URI is canonicalized.
  2. Audience Validation Errors: If the aud claim is not explicitly set in the token or does not include the resource server's client ID, validation will fail. Developers often forget to configure the audience claim in the authorization server's token response, assuming the signature is enough.
  3. Key Rotation Issues: During key rotation, if the resource server's JWKS cache is not refreshed quickly enough, valid tokens signed with the new key may be rejected. Ensure your infrastructure allows the NimbusJwtDecoder to fetch the latest JWKS set without excessive latency.

Practical Takeaways

  • Leverage Auto-Configuration: Rely on spring.security.oauth2.resourceserver.jwt.issuer-uri to simplify setup; it handles JWKS fetching and caching automatically.
  • Separate Concerns: Use JwtDecoder for cryptographic verification and JwtAuthenticationConverter for mapping claims to authorities; do not mix these responsibilities.
  • Validate Claims Strictly: Never rely solely on the signature; always enforce strict iss and aud matching to prevent cross-service token abuse.

FAQ

Q: Can I use opaque tokens with Spring Security 5.7+? A: Yes, but you must explicitly configure OpaqueTokenIntrospection with a custom introspection URI. The default behavior assumes JWTs and will fail if you attempt to validate opaque tokens without this configuration.

Q: How do I handle token revocation in a JWT-based system? A: Since JWTs are stateless, you cannot revoke them server-side immediately. Common strategies include using short-lived access tokens with refresh tokens, implementing a token blacklist database, or reducing the access token lifetime significantly.

Q: What happens if the JWKS endpoint is unreachable? A: The NimbusJwtDecoder caches the keys. If the endpoint is down, existing cached keys remain valid. However, if a new key is needed (e.g., during rotation) and the endpoint is unreachable, new tokens signed with that key will fail validation until connectivity is restored.

Related posts