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.

Xiaolin Talks Programming
Xiaolin Talks Programming
Xiaolin Talks Programming
Spring Boot Keycloak Integration: OIDC SSO, Role Mapping & Method Security

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-dev

Access 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_username

The 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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Spring BootMulti-tenancyRBACSSOSpring SecurityLDAPKeycloakOIDC
Xiaolin Talks Programming
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.