Implementing security in a distributed system can feel like a nightmare if you’re staring at the documentation for the first time. In my experience building microservices, getting the handshake between the client, the identity provider, and the API just right is where most developers get stuck. That is why I’ve put together this spring security oauth2 tutorial step by step to take you from a blank project to a fully secured Resource Server.
Whether you are using Keycloak, Okta, or Auth0, the underlying principles of Spring Security remain the same. We aren’t just adding a login page; we are implementing a standardized protocol that allows your services to trust tokens without needing to know the user’s password.
Prerequisites
Before we dive into the code, make sure you have the following installed in your environment:
- Java 17 or higher (I recommend Amazon Corretto or Temurin)
- Maven 3.8+ or Gradle
- An IDE (IntelliJ IDEA is my go-to for Spring development)
- An account with an Identity Provider (IdP) like Okta or a local Keycloak Docker container
Step 1: Initialize Your Spring Boot Project
I always start with Spring Initializr. Generate a project with the following dependencies:
- Spring Web
- Spring Security
- OAuth2 Resource Server
- Lombok (to keep the boilerplate low)
// pom.xml dependency snippet
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Step 2: Configure the Identity Provider (IdP)
You need a place to issue tokens. If you’re using an IdP, you’ll need to create an ‘Application’ and gather three key pieces of information: the Issuer URI, the Client ID, and the Client Secret. As shown in the diagram in the introduction, the Resource Server (your API) needs the Issuer URI to validate the JWT signatures automatically.
Add these to your application.yml:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://your-identity-provider.com/auth/realms/myrealm
Step 3: Create the Security Filter Chain
In Spring Security 6+, we no longer extend WebSecurityConfigurerAdapter. Instead, we define a SecurityFilterChain bean. I prefer this approach because it’s more modular and avoids the pitfalls of inheritance.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.requestMatchers("/api/admin/**").hasAuthority("SCOPE_admin")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
}
At this stage, your API is protected. Any request without a valid Bearer token will return a 401 Unauthorized. However, for production systems, you should also consider spring boot actuator security best practices to ensure your management endpoints aren’t exposed to the public internet.
Step 4: Implementing Role-Based Access Control (RBAC)
By default, Spring Security maps OAuth2 scopes to authorities with the prefix SCOPE_. But in real-world apps, you usually have complex roles. To handle this, I use a custom JwtAuthenticationConverter.
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter()
{
JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
grantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
This ensures that if your token has a roles: ["ADMIN"] claim, Spring recognizes it as ROLE_ADMIN.
Step 5: Testing the Implementation
I recommend using Postman or Insomnia for testing. First, perform a Client Credentials flow to get your token, then add it to the Header:
Authorization: Bearer <your_token_here>
If you are building a larger ecosystem with multiple services, you might want to move this security logic to a gateway. I’ve written a detailed best java api gateway comparison 2026 to help you decide between Spring Cloud Gateway and others for handling OAuth2 centrally.
Pro Tips for Production
- Token Introspection: For high-security apps, use Opaque Tokens and Introspection instead of JWTs to allow immediate token revocation.
- Caching: Cache the JWK (JSON Web Key) set to avoid hitting the IdP on every single request.
- HTTPS: Never, ever run OAuth2 over HTTP in production. The tokens are easily intercepted.
Troubleshooting Common Issues
1. 401 Unauthorized despite valid token: Check if your system clock is synchronized. If your server’s time is off by a few seconds, the exp (expiration) or nbf (not before) claims in the JWT will cause validation to fail.
2. ‘Invalid Issuer’ Error: Ensure the issuer-uri in your config exactly matches the iss claim inside the JWT. Even a trailing slash can cause a mismatch.
What’s Next?
Now that you’ve secured your API, you should look into implementing Method Level Security using @PreAuthorize. This allows you to secure specific service methods rather than just URL patterns. Additionally, consider implementing a custom AuthenticationEntryPoint to return a structured JSON error instead of a generic Spring white-label error page.