KeycloakOidcUserService.java
package com.taxonomy.security.keycloak;
import org.springframework.context.annotation.Profile;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* Custom {@link OidcUserService} that maps Keycloak OIDC claims to
* Spring Security authorities for browser-based sessions.
* <p>
* After a successful OIDC login, this service:
* <ol>
* <li>Loads the standard OidcUser from the UserInfo endpoint</li>
* <li>Extracts realm roles from the OIDC user's claims (Keycloak {@code realm_access})</li>
* <li>Returns an OidcUser with the correct ROLE_* authorities</li>
* </ol>
*/
@Component
@Profile("keycloak")
public class KeycloakOidcUserService extends OidcUserService {
/** The recognized application roles. */
private static final Set<String> KNOWN_ROLES = Set.of("ROLE_USER", "ROLE_ARCHITECT", "ROLE_ADMIN");
@Override
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
OidcUser oidcUser = super.loadUser(userRequest);
// Extract realm roles from the ID token claims
Set<GrantedAuthority> authorities = new HashSet<>(oidcUser.getAuthorities());
authorities.addAll(extractRealmRoles(oidcUser));
return new DefaultOidcUser(
authorities,
oidcUser.getIdToken(),
oidcUser.getUserInfo(),
"preferred_username"
);
}
/**
* Extracts realm roles from the OIDC user's claims.
* Keycloak stores roles under {@code realm_access.roles}.
*/
@SuppressWarnings("unchecked")
private Collection<GrantedAuthority> extractRealmRoles(OidcUser oidcUser) {
Set<GrantedAuthority> roles = new HashSet<>();
Map<String, Object> realmAccess = oidcUser.getAttribute("realm_access");
if (realmAccess != null) {
Object rolesObj = realmAccess.get("roles");
if (rolesObj instanceof Collection<?> roleList) {
roleList.stream()
.filter(String.class::isInstance)
.map(String.class::cast)
.filter(KNOWN_ROLES::contains)
.map(SimpleGrantedAuthority::new)
.forEach(roles::add);
}
}
return roles;
}
}