OAuth2 with Spring Boot and Keycloak: OpenID Connect and SSO Implementation
Learn how to implement OAuth2 and OpenID Connect in Spring Boot 3.2 with Keycloak 22 for SSO.
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
- ✓Java 17+ and Spring Boot 3.2+ project with spring-boot-starter-web and spring-boot-starter-oauth2-resource-server
- ✓Running Keycloak 22+ instance (Docker image quay.io/keycloak/keycloak:22.0.0)
- ✓Basic understanding of OAuth2 flows (authorization code, client credentials) and JWT structure
• OAuth2 with Spring Boot and Keycloak enables secure SSO using OpenID Connect (OIDC) for authentication and OAuth2 for authorization. • Keycloak acts as the identity provider (IdP) managing user credentials, roles, and tokens. • Spring Boot 3.2 with spring-boot-starter-oauth2-client handles token validation and session management. • You need to configure Keycloak realms, clients, and role mappings, then secure your REST APIs with @PreAuthorize and SecurityFilterChain. • Common production issues include token expiration, CORS misconfiguration, and role prefix mismatches.
Think of Keycloak as the bouncer at a VIP club. You show your ID (login credentials) once, and the bouncer gives you a wristband (access token). That wristband lets you enter any room (microservice) in the club without showing your ID again. OAuth2 is the rulebook for how the wristband works, and OpenID Connect is the bouncer's way of verifying who you are.
OAuth2 and OpenID Connect (OIDC) have become the de facto standard for securing microservices and enabling Single Sign-On (SSO) in modern Java applications. Spring Boot 3.2 provides first-class support for OAuth2 through the spring-boot-starter-oauth2-client and spring-boot-starter-oauth2-resource-server starters, but the official documentation often glosses over real-world production nuances. In this tutorial, we will integrate Spring Boot with Keycloak 22 (the latest stable version as of 2024) to implement a complete OAuth2 + OIDC flow. You will learn how to set up a Keycloak realm, configure an OAuth2 client in Spring Boot, secure REST endpoints with role-based access, and validate JWT tokens. We will also cover common production incidents like token refresh failures, CORS errors in SPA integrations, and role prefix mismatches between Keycloak and Spring Security. By the end, you will have a working SSO setup that can serve as the foundation for a multi-service SaaS billing platform or a real-time analytics dashboard. I assume you have a running Keycloak instance (Docker recommended) and basic familiarity with Spring Boot 3.2. Let's skip the Hello World and get to the hard stuff.
Setting Up Keycloak Realm and Client
First, you need a Keycloak realm and a client. Let's create a realm called 'saas-billing' and a client named 'payment-service'. In Keycloak Admin Console (http://localhost:8080), go to Realms > Create Realm. Set the realm name to 'saas-billing'. Then create a client: Client ID = 'payment-service', Client authentication = ON (for confidential client), Service accounts roles = ON, Standard flow = ON. Save and note the client secret under the Credentials tab. Also create a test user under Users: username = 'john_doe', email = john@example.com, set a password. Assign the role 'admin' under Role Mappings (we'll create a realm role named 'admin' first). This setup mimics a real SaaS billing system where the payment service needs to authenticate users and validate their roles. The client secret will be used in Spring Boot for the authorization code flow. For resource server mode (API-only), you don't need the secret — just the issuer URI. But for SSO with a UI, you'll need the client secret for token exchange. I recommend using Docker: docker run -p 8080:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:22.0.0 start-dev. This gives you a local Keycloak with the admin console at http://localhost:8080.
What the Official Docs Won't Tell You
The Spring Security reference documentation for OAuth2 resource server is decent, but it hides three landmines. First, the default JwtDecoder uses the JWK Set URI from the issuer-uri property, but it caches the keys forever. If you rotate keys in Keycloak (which you should do regularly), your service will reject tokens until you restart. Solution: configure a Caffeine cache with a TTL for the JWK set. Second, role mapping is not automatic. Keycloak sends roles as a 'realm_access.roles' claim, but Spring Security expects 'roles' or 'authorities'. You must write a custom GrantedAuthoritiesMapper to extract roles from the correct claim. Third, CORS is a nightmare when integrating with SPAs. Keycloak's default CORS policy blocks all origins except localhost. You must explicitly configure allowed origins in the Keycloak client settings under 'Web Origins'. I've seen teams waste days debugging 403 errors from their React frontend because they forgot to add 'http://localhost:3000' to Web Origins. Another gotcha: the authorization code flow requires a redirect URI that exactly matches what's configured in Keycloak. If you use HTTPS locally with a self-signed certificate, Keycloak will reject the redirect. Use http://localhost for local development. Finally, token introspection is slow in Keycloak — prefer local JWT validation with the JWK set for performance.
Configuring Spring Boot Application Properties
The application.yml file is where you wire Keycloak to Spring Boot. For a resource server (API-only), you only need the issuer-uri. For a client (SSO with login page), you need the registration details. I'll show both. The critical property is spring.security.oauth2.resourceserver.jwt.issuer-uri. This must point to your Keycloak realm's issuer URL, e.g., http://keycloak:8080/realms/saas-billing. Spring Boot will automatically fetch the JWK set URI from the OpenID Connect discovery endpoint (/.well-known/openid-configuration). If you're running Keycloak behind a reverse proxy (like Nginx), make sure the issuer-uri matches the external URL, not the internal one. Otherwise, token validation will fail because the 'iss' claim in the JWT will not match. For the OAuth2 client configuration (if you want Spring Boot to handle the login flow), add spring.security.oauth2.client.registration.keycloak.client-id and client-secret. The provider details can be auto-configured from the issuer-uri as well. I prefer using environment variables for the client secret — never hardcode it in application.yml. Use ${KEYCLOAK_CLIENT_SECRET} and pass it via Docker or Kubernetes secrets.
Securing REST Endpoints with Role-Based Access
Now we protect our payment-processing endpoints. Use @PreAuthorize annotations on controller methods for fine-grained access control. First, enable method security with @EnableMethodSecurity in your configuration. Then define roles as realm roles in Keycloak (e.g., 'admin', 'user'). In the JwtAuthenticationConverter we configured earlier, roles from the 'realm_access.roles' claim are mapped to Spring Security authorities with the 'ROLE_' prefix. This means @PreAuthorize("hasRole('admin')") will match a user with the 'admin' realm role. For endpoint-level security, you can also use requestMatchers in the SecurityFilterChain. I prefer a hybrid approach: use requestMatchers for broad categories (e.g., /api/public/**) and @PreAuthorize for fine-grained control. One common mistake is forgetting that hasRole() expects the role name without the 'ROLE_' prefix (Spring adds it automatically). Another is mixing up hasAuthority() and hasRole(). Use hasRole() for realm roles and hasAuthority() for client roles or custom claims. In a SaaS billing system, you might have endpoints like /api/invoices that require 'admin' role, and /api/invoices/{id} that require 'user' role and ownership validation.
Handling Token Refresh and Session Management
Access tokens have a short lifetime (default 5 minutes in Keycloak). Refresh tokens live longer (default 30 minutes). For a seamless user experience, you need to handle token refresh. In a Spring Boot resource server (API-only), you don't manage refresh tokens — the client (e.g., a React SPA) handles that. But if you're using Spring Boot as an OAuth2 client (with a login page), Spring Security handles refresh automatically via the OAuth2AuthorizedClientManager. The tricky part is when the refresh token expires. Keycloak returns a new refresh token with each refresh, but if the user is inactive for 30 minutes, the refresh token expires and the user must re-login. To extend sessions, you can configure Keycloak's SSO Session Idle timeout. I set mine to 8 hours for a billing system where users work all day. Another production issue: if you have multiple microservices, each service needs to validate the access token independently. But token refresh should happen only at the edge (API gateway or SPA). Never let microservices refresh tokens — that creates tight coupling and security holes. For real-time analytics, consider using opaque tokens with introspection if you need to revoke tokens immediately. JWT tokens cannot be revoked until they expire, unless you maintain a blacklist in Redis.
Testing the OAuth2 Flow with Spring Boot
Testing OAuth2 flows is notoriously difficult because you need a running Keycloak instance and valid tokens. I use Testcontainers to spin up a Keycloak container in integration tests. This gives you a real Keycloak that you can configure programmatically. For unit tests, you can mock the JwtDecoder and return a fake Jwt token. But integration tests with Testcontainers catch real issues like role mapping mismatches or issuer URI errors. I'll show you a test that creates a Keycloak realm, a client, a user with roles, then calls your secured endpoint. You need the Keycloak admin client (org.keycloak:keycloak-admin-client:22.0.0) to create the realm programmatically. The test gets an access token for the test user, then calls the API. This is the only reliable way to test OAuth2 security. Mocking the security context is fine for unit tests, but never ship without an integration test that validates the full OAuth2 flow. I've seen teams deploy broken security because they only tested with mocked tokens that didn't match the real Keycloak claims structure.
Production Debugging: CORS and Token Validation Issues
CORS is the number one issue when integrating a React SPA with Spring Boot and Keycloak. The browser sends a preflight OPTIONS request, and if the server doesn't respond with the correct CORS headers, the actual request fails with a misleading error. In Keycloak, you must configure 'Web Origins' in the client settings to include your SPA's origin (e.g., http://localhost:3000). For production, use the actual domain. Additionally, Spring Boot's resource server must handle CORS itself because the browser sends the preflight before the Authorization header. Configure a CorsConfigurationSource bean that allows your SPA origin, methods, and headers. Another common issue is token validation failing due to clock skew. If your Keycloak server and Spring Boot service have different system times (more than a few seconds), JWT validation will fail because the 'nbf' (not before) or 'exp' (expiration) claims are in the future or past. Use NTP to synchronize clocks across all servers. For debugging token issues, log the JWT claims during development. Add a filter that prints the decoded token. I use a custom OncePerRequestFilter that logs the principal and authorities. This helps identify role mapping issues quickly. Finally, never log the full JWT in production — it contains sensitive user information. Log only the claims you need.
Advanced: Client Credentials Flow for Machine-to-Machine
Not all clients are users. In a SaaS billing system, you have background jobs, batch processors, and other microservices that need to call APIs without user interaction. For these, use the client credentials flow. Configure a separate Keycloak client (e.g., 'batch-processor') with 'Service accounts roles' enabled. Assign roles to the service account (not a user). In Spring Boot, use the OAuth2ClientCredentialsGrantRequestEntityConverter to get a token for the client. Then use a RestTemplate or WebClient with the OAuth2AuthorizedClientManager to automatically obtain and refresh tokens. The key difference: the token contains no user information — only client roles. This flow is ideal for batch jobs that process invoices at midnight. The token is long-lived (you can set it to 1 hour), but you still need to handle refresh. One gotcha: Keycloak's service account roles are mapped to the 'resource_access.{client-id}.roles' claim, not 'realm_access.roles'. So you need a separate JwtAuthenticationConverter for client credentials tokens that extracts roles from the correct claim. I use a custom JwtAuthenticationConverter that checks both claims and merges them. This way, the same SecurityConfig works for both user tokens and service account tokens.
The Silent 401 After Token Refresh
- Always verify the issuer URI matches the Keycloak realm exactly, including the /realms/ path.
- Use a custom JwtDecoder bean to log token claims during development for easier debugging.
- Never assume token lifetime is the root cause of 401 errors — always check token claims first.
curl -k https://keycloak:8080/realms/saas-billing/.well-known/openid-configuration | jq .issuercurl -k https://keycloak:8080/realms/saas-billing/protocol/openid-connect/certs | jq .keys[0].kid| File | Command / Code | Purpose |
|---|---|---|
| docker-compose.yml | version: '3.8' | Setting Up Keycloak Realm and Client |
| SecurityConfig.java | @Configuration | What the Official Docs Won't Tell You |
| application.yml | spring: | Configuring Spring Boot Application Properties |
| PaymentController.java | @RestController | Securing REST Endpoints with Role-Based Access |
| TokenRefreshConfig.java | @Configuration | Handling Token Refresh and Session Management |
| OAuth2IntegrationTest.java | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) | Testing the OAuth2 Flow with Spring Boot |
| CorsConfig.java | @Configuration | Production Debugging |
| ClientCredentialsConfig.java | @Configuration | Advanced |
Key takeaways
Interview Questions on This Topic
Explain the OAuth2 authorization code flow with PKCE and how Spring Boot handles it.
Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
That's Spring Security. Mark it forged?
6 min read · try the examples if you haven't