Spring Boot Keycloak Integration: OIDC SSO, Role Mapping & Method Security
This article provides a practical guide to integrating Spring Boot with Keycloak for unified authentication and single sign-on using OIDC authorization code flow, covering Keycloak deployment, realm and client configuration, Spring Security setup with role mapping for both browser login and resource server tokens, method-level authorization with @PreAuthorize, and extensions for multi-tenancy, LDAP federation, and Admin API.
Scenario
When a company has many internal systems (OA, ERP, CRM, project management, ops platforms), each managing its own user accounts, employees must log in repeatedly and IT faces provisioning/deprovisioning overhead and audit gaps. The solution is unified Identity and Access Management (IAM) plus Single Sign-On (SSO). Keycloak is a widely used open-source IAM in the Java ecosystem, supporting OIDC, SAML 2.0, LDAP, with built-in user management, roles, event auditing, and tenant isolation. Integration with Spring Boot is straightforward.
Keycloak Core Concepts
Realm
A Realm is Keycloak's isolation unit. It manages a set of users, roles, clients, and configurations, fully isolated from other realms. Typical practice: separate realms per environment (dev/test/prod) or per tenant in SaaS.
Client
A Client represents an application integrating with SSO. It has a unique client-id, authenticates via client-secret, and registers a redirect URI for callbacks. Spring Boot apps usually act as web clients; pure backend APIs can also act as resource servers.
User, Role, Group
User: an identity that can hold multiple roles and attributes.
Role: realm roles (global) and client roles (scoped to a specific client).
Group: a collection of users; assigning roles to a group grants those roles to all members.
OIDC Authorization Code Flow
OIDC extends OAuth 2.0 with an ID Token (JWT) carrying user identity. The flow:
User accesses the app; if unauthenticated, redirected to Keycloak login page.
User enters credentials (optionally MFA) at Keycloak.
On success, Keycloak redirects back to the app with an authorization code.
The app exchanges the code for an ID Token and Access Token via a backend call.
Subsequent requests carry the Access Token to access protected APIs.
Spring Security's oauth2-client module handles this flow; only configuration is required.
Deploying Keycloak
Development environment via Docker:
docker run -d --name keycloak
-p 8080:8080
-e KEYCLOAK_ADMIN=admin
-e KEYCLOAK_ADMIN_PASSWORD=admin
-e KC_HOSTNAME=localhost
keycloak/keycloak:25.0.0 start-devAccess http://localhost:8080 and log in with admin/admin.
Creating Realm and Client
Create a realm named my-realm.
Add a Client:
Client type: OpenID Connect Client ID: spring-boot-app Client authentication: ON (confidential)
Valid redirect URIs: http://localhost:8081/login/oauth2/code/keycloak Web origins: http://localhost:8081 Retrieve the client secret from the Credentials tab.
Define realm roles ADMIN and USER, create test users and assign roles.
For repeatability, import a realm JSON file via the admin console's Add/Import realm feature. Example realm JSON includes realm metadata, roles, client configuration, and a test user alice with password alice123 and both roles.
Spring Boot Integration
Dependencies
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Optional: if the service also acts as a resource server validating API tokens -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
</dependencies>application.yml
server:
port: 8081
spring:
application:
name: sso-demo
security:
oauth2:
client:
registration:
keycloak:
provider: keycloak
client-id: spring-boot-app
client-secret: your-secret
scope: openid, profile, email
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
provider:
keycloak:
issuer-uri: http://localhost:8080/realms/my-realm
user-name-attribute: preferred_usernameThe issuer-uri is critical; Spring Boot automatically fetches authorization, token, and JWK endpoints from /.well-known/openid-configuration.
Security Configuration
Two key points: oauth2Login() handles browser login redirects.
If the same service also exposes APIs for other systems, add oauth2ResourceServer().jwt() to validate Access Tokens.
However, roles from the realm_access claim in the JWT are not automatically mapped to Spring Security authorities. A custom JwtAuthenticationConverter is required. The following configuration handles both OIDC login (via GrantedAuthoritiesMapper) and resource server tokens (via JwtAuthenticationConverter), mapping realm roles to authorities prefixed with ROLE_ so that @PreAuthorize("hasRole('ADMIN')") works. The offline_access role is filtered out.
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**", "/error").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(Customizer.withDefaults())
.oauth2ResourceServer(resourceServer -> resourceServer
.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))
);
// For pure API with separate frontend, CSRF can be disabled:
// http.csrf(csrf -> csrf.disable());
return http.build();
}
@Bean
public GrantedAuthoritiesMapper userAuthoritiesMapper() {
return authorities -> {
Collection<GrantedAuthority> result = new HashSet<>(authorities);
for (GrantedAuthority authority : authorities) {
if (authority instanceof OidcUserAuthority oidcUserAuthority) {
Map<String, Object> claims = oidcUserAuthority.getIdToken().getClaims();
List<String> realmRoles = extractRealmRoles(claims);
for (String role : realmRoles) {
result.add(new SimpleGrantedAuthority("ROLE_" + role));
}
}
}
return result;
};
}
private Converter<Jwt, AbstractAuthenticationToken> jwtAuthenticationConverter() {
return jwt -> {
List<String> realmRoles = extractRealmRoles(jwt.getClaims());
Collection<GrantedAuthority> authorities = realmRoles.stream()
.map(role -> (GrantedAuthority) new SimpleGrantedAuthority("ROLE_" + role))
.collect(Collectors.toList());
var principal = JwtAuthenticationToken.withJwt(jwt)
.authorities(authorities)
.build();
return principal;
};
}
@SuppressWarnings("unchecked")
private List<String> extractRealmRoles(Map<String, Object> claims) {
if (claims.get("realm_access") instanceof Map) {
Object roles = ((Map<String, Object>) claims.get("realm_access")).get("roles");
if (roles instanceof List) {
return ((List<String>) roles).stream()
.filter(role -> !role.equals("offline_access"))
.collect(Collectors.toList());
}
}
return List.of();
}
}Accessing User Information
After login, controllers can inject OidcUser to retrieve claims:
@RestController
@RequestMapping("/api")
public class UserController {
@GetMapping("/me")
public Map<String, Object> userInfo(@AuthenticationPrincipal OidcUser oidcUser) {
return oidcUser.getClaims();
}
}Typical ID Token claims:
{
"sub": "78f2e9f5-...",
"preferred_username": "alice",
"email": "[email protected]",
"realm_access": {
"roles": ["USER", "ADMIN"]
}
}Method-Level Authorization
With role mapping in place, method security is concise:
@RestController
@RequestMapping("/api")
public class AdminController {
@GetMapping("/admin")
@PreAuthorize("hasRole('ADMIN')")
public String adminEndpoint() {
return "Only admins can see this";
}
@GetMapping("/user")
@PreAuthorize("hasAnyRole('USER', 'ADMIN')")
public String userEndpoint() {
return "Any logged-in user can see this";
}
}Object-level authorization is also possible using SpEL expressions, e.g., @PreAuthorize("#username == authentication.name") to ensure users only access their own data.
End-to-End Flow Test
Start Keycloak and Spring Boot.
Visit http://localhost:8081/api/me; redirected to Keycloak login.
Log in as alice/alice123, consent, and return to the app.
Claims are returned.
Access /api/admin; alice (with ADMIN role) gets 200; a user without ADMIN gets 403.
Extensions
Multi-Tenancy
Keycloak uses realms for isolation. Each tenant gets a realm. Spring Boot can define multiple registration entries (e.g., tenant-a, tenant-b). For dynamic routing, a custom JwtIssuerAuthenticationManagerResolver can be implemented, though fixed configuration suffices for most initial use cases.
User Federation (LDAP/AD)
Keycloak can connect to existing LDAP/Active Directory via User Federation in the admin console. Authentication is proxied to Keycloak. Configuration is straightforward but production sync policies need careful tuning.
Admin REST API
The official keycloak-admin-client library enables programmatic user management (registration, offboarding, sync from other systems). Dependency:
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-admin-client</artifactId>
<version>25.0.0</version>
</dependency>Use service-account (client credentials) flow with a dedicated client (e.g., admin-service) granted realm-management roles. Example code creates a user, sets a password, and assigns the USER role. In production, wrap the Admin API in a dedicated service with audit logging.
Security Recommendations
Enforce HTTPS in production; store client-secret in environment variables or KMS, not in config files.
Set Valid Redirect URIs explicitly; avoid wildcards to prevent open redirect vulnerabilities.
Keep Access Token lifetime short (e.g., 5 minutes) and use Refresh Tokens for rolling renewal.
Filter out internal roles like offline_access from realm_access to reduce noise in the security context. @PreAuthorize only applies to the current thread's security context; for async methods, explicitly propagate authentication.
Conclusion
The Keycloak + Spring Boot stack centralizes identity, SSO, and permission management, eliminating duplicate authentication logic. Once the configuration is verified, adapt role mapping and token parsing to your user model to meet enterprise requirements. All code in this article is production-ready and can be used directly.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Xiaolin Talks Programming
Focuses on sharing original technical insights. Senior architect at a top tech company with years of experience in technical architecture and management, and extensive interview experience. Offers one-on-one technical coaching, guiding you from beginner to architecture design to technical management. Follow for free learning resources. Free one-on-one interview coaching to help you land offers quickly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
