Securing Your APIs: A Guide to OAuth2 Resource Server Configuration in Java

In a modern microservices architecture, the separation between the entity that authenticates users (the Authorization Server) and the entity that serves protected resources (the Resource Server) is a fundamental pattern. OAuth 2.0 formalizes this with the Resource Server role. Its job is to validate access tokens presented by clients and grant or deny access to API endpoints.

This article provides a practical guide to configuring an OAuth2 Resource Server in Java using the de facto standard: Spring Security.


Core Concepts

  • Resource Server: An application that hosts protected resources (e.g., a REST API serving user data). It must validate OAuth 2.0 access tokens to service requests.
  • Access Token: A credential, typically a JWT (JSON Web Token), issued by an Authorization Server (e.g., Keycloak, Auth0, Okta) that represents a grant of access.
  • JWT (JSON Web Token): A compact, URL-safe token format that contains a set of claims (key-value pairs) which are encoded in a JSON object. It is digitally signed, allowing the Resource Server to verify its integrity and authenticity without contacting the Authorization Server.

Configuration with Spring Security

Spring Security provides a dedicated DSL (Domain Specific Language) for configuring a resource server, making the setup declarative and straightforward.

1. Maven/Gradle Dependencies

First, you need to include the Spring Security OAuth2 Resource Server dependency.

Maven:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

Gradle:

implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'

2. Application Configuration (application.yml or application.properties)

You must tell your application where to find the Authorization Server's metadata, specifically the JWK Set URI (used for JWT validation) or the issuer URI.

Using application.yml:

spring:
security:
oauth2:
resourceserver:
jwt:
# Option 1: Specify the Issuer URI (Recommended)
# Spring Security will auto-configure by fetching the .well-known/openid-configuration
issuer-uri: https://idp.example.com/auth/realms/my-realm
# Option 2: Specify the JWK Set URI directly (if issuer-uri is not available)
# jwk-set-uri: https://idp.example.com/auth/realms/my-realm/protocol/openid-connect/certs

3. Java Security Configuration

This is where you define the security rules for your application. The @EnableWebSecurity and @EnableMethodSecurity annotations are key.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // Enables @PreAuthorize, @PostAuthorize, etc.
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// Disable CSRF for stateless API
.csrf(csrf -> csrf.disable())
// Configure the application as a Resource Server
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))
)
// Session management is stateless
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
// Define authorization rules
.authorizeHttpRequests(authz -> authz
// Public endpoints
.requestMatchers("/api/public/**").permitAll()
// Endpoints requiring specific scopes
.requestMatchers("/api/admin/**").hasAuthority("SCOPE_admin")
.requestMatchers("/api/users/**").hasAuthority("SCOPE_profile")
// Authenticated endpoints (any valid token)
.requestMatchers("/api/**").authenticated()
// All other requests are denied by default
.anyRequest().denyAll()
);
return http.build();
}
}

JWT Claim to Authority Mapping

By default, Spring Security will extract scopes from the scope or scp claim in the JWT and prefix them with SCOPE_ to create authorities (e.g., "read" becomes "SCOPE_read"). However, many identity providers use custom claims (like roles or groups). You need a custom converter to handle this.

Custom JWT Authentication Converter

This example converts a roles claim from a Keycloak-like token into Spring Security GrantedAuthority objects.

import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Bean
public Converter<Jwt, AbstractAuthenticationToken> jwtAuthenticationConverter() {
return jwt -> {
// 1. Extract authorities from the JWT claims
Collection<GrantedAuthority> authorities = extractAuthorities(jwt);
// 2. You can also extract the principal (e.g., username) from a claim
String principalName = jwt.getClaimAsString("preferred_username");
// 3. Return a JwtAuthenticationToken with the extracted authorities
return new JwtAuthenticationToken(jwt, authorities, principalName);
};
}
private Collection<GrantedAuthority> extractAuthorities(Jwt jwt) {
// Check for a 'roles' claim within a 'realm_access' claim (Keycloak format)
Map<String, Object> realmAccess = jwt.getClaim("realm_access");
if (realmAccess != null) {
@SuppressWarnings("unchecked")
List<String> roles = (List<String>) realmAccess.get("roles");
if (roles != null) {
return roles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role)) // Prefix with ROLE_
.collect(Collectors.toList());
}
}
// Fallback: extract from the standard 'scope' claim
String scopeClaim = jwt.getClaimAsString("scope");
if (scopeClaim != null) {
return List.of(scopeClaim.split(" ")).stream()
.map(scope -> new SimpleGrantedAuthority("SCOPE_" + scope))
.collect(Collectors.toList());
}
return Collections.emptyList();
}

Method-Level Security

With @EnableMethodSecurity, you can secure individual methods in your service layer using annotations.

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
@Service
public class UserService {
// Requires the 'admin' scope
@PreAuthorize("hasAuthority('SCOPE_admin')")
public void deleteUser(String userId) {
// ... implementation
}
// Requires the 'ROLE_MODERATOR' authority we mapped from the JWT
@PreAuthorize("hasRole('MODERATOR')") // Note: hasRole automatically prefixes with 'ROLE_'
public void moderateContent(String contentId) {
// ... implementation
}
// Access control based on the authentication object itself
@PreAuthorize("isAuthenticated()")
public String getProfile() {
// ... implementation
}
}

Accessing JWT Claims in Your Controller

You can inject the Jwt object or the Authentication object into your controller methods to access token claims directly.

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
public class UserController {
@GetMapping("/api/whoami")
public Map<String, Object> whoAmI(@AuthenticationPrincipal Jwt jwt) {
// Access claims from the JWT
String username = jwt.getClaimAsString("preferred_username");
String email = jwt.getClaimAsString("email");
List<String> roles = jwt.getClaimAsStringList("roles"); // If you have a custom 'roles' claim
return Map.of(
"username", username,
"email", email,
"subject", jwt.getSubject(),
"issuer", jwt.getIssuer().toString(),
"all_claims", jwt.getClaims() // Be careful not to expose sensitive claims!
);
}
}

Testing Your Resource Server

You can write unit tests using Spring's @WebMvcTest and @AutoConfigureTestDatabase and integration tests with @SpringBootTest.

Example Unit Test with Mocked JWT:

import org.springframework.security.test.context.support.WithMockJwt;
import org.springframework.test.web.servlet.MockMvc;
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
@WithMockJwt(authorities = "SCOPE_profile") // Mock a JWT with the 'profile' scope
void whenUserHasScopeProfile_thenEndpointIsAccessible() throws Exception {
mockMvc.perform(get("/api/users/me"))
.andExpect(status().isOk());
}
@Test
@WithMockJwt(authorities = "SCOPE_read") // Mock a JWT with the wrong scope
void whenUserLacksScopeProfile_thenEndpointIsForbidden() throws Exception {
mockMvc.perform(get("/api/users/me"))
.andExpect(status().isForbidden());
}
}

Best Practices and Pitfalls

  1. Validate the Issuer (iss claim): Always configure the issuer-uri. This prevents tokens from a malicious Authorization Server from being accepted.
  2. Use Method-Level Security: Don't rely solely on URL-based security. Use @PreAuthorize for fine-grained, business-level security.
  3. Proper Authority Mapping: Ensure your custom JwtAuthenticationConverter correctly maps the claims from your specific identity provider.
  4. Token Introspection vs. JWT: This article focuses on JWT validation. For opaque tokens (non-JWT), you would configure the Resource Server for token introspection using .opaqueToken() and an introspection URI, which requires a call to the Authorization Server for each request.
  5. Keep Your Dependencies Updated: Spring Security and its OAuth2 support are actively developed. Keep your dependencies up-to-date to benefit from security patches and new features.

Conclusion

Configuring an OAuth2 Resource Server with Spring Security is a powerful and streamlined process. By leveraging the oauth2ResourceServer DSL, correctly mapping JWT claims to Spring Security authorities, and applying method-level security, you can build robust, secure, and scalable APIs that seamlessly integrate with modern identity providers. The key to success lies in understanding the token format provided by your Authorization Server and configuring the claim-to-authority mapping accordingly.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper