Keycloak Integration

How the admin-service integrates with Keycloak — the KeyCloakManager factory, client creation flow, and realm role management.

Overview

The admin-service uses the Keycloak Admin Client (keycloak-admin-client:26.0.4) to programmatically manage the event-based-banking-service realm. It is the only service in the platform that holds admin-level Keycloak credentials.

The integration covers:

  • Creating confidential OAuth2 clients for new microservices (service accounts flow)
  • Listing and creating realm-level roles
  • Assigning roles to the service account of a newly provisioned client

KeyCloakManager

KeyCloakManager is a @Component that acts as a factory and accessor for the Keycloak Admin API. It initialises the client on startup via @PostConstruct using the client_credentials grant type.

Java code-highlight
@PostConstruct
public void init() {
    keycloak = KeycloakBuilder.builder()
        .serverUrl(serverUrl)
        .realm(keyCloakRealm)
        .clientId(clientId)
        .clientSecret(clientSecret)
        .grantType(CLIENT_CREDENTIALS)
        .build();
}

The clientSecret is resolved from Vault at startup (${ADMIN_SERVICE_CLIENT_SECRET}), so KeyCloakManager only initialises correctly after Vault bootstrap completes.

Accessor Methods

MethodReturnsUse
getKeyCloakInstanceWithRealm()RealmResourceTop-level realm handle
getRealmClient()ClientsResourceClient management operations
getRealmRoles()RolesResourceRealm role management
getRealmUsers()UsersResourceUser management (future use)

Client Creation Flow

When POST /api/admin/inter-service-clients?clientName=<name> is called, KeyCloakServiceImpl.createClient(String clientName) executes the following steps:

The INTERNAL_SERVICE realm role is what enables the new client to call /internal/** paths on other services. The admin-service's SecurityConfig grants access to /internal/** only to principals holding ROLE_INTERNAL_SERVICE.


Realm Role Management

createRealmRole

Java code-highlight
public KeyCloakResponse createRealmRole(KeycloakRole keycloakRole) {
    try {
        // Attempt to read the role first — if found, it already exists
        keyCloakManager.getRealmRoles().get(keycloakRole.name()).toRepresentation();
        throw new KeyCloakRealmRoleAlreadyExists("Role already exists: " + keycloakRole.name());
    } catch (NotFoundException e) {
        // NotFoundException means it doesn't exist — safe to create
        RoleRepresentation role = keycloakRoleMapper.toEntity(keycloakRole);
        keyCloakManager.getKeyCloakInstanceWithRealm().roles().create(role);
        return new KeyCloakResponse(KEYCLOAK_ROLE_CREATED_200, "Role created successfully");
    }
}
The Keycloak client throws a NotFoundException when a role does not exist. KeyCloakServiceImpl uses this as a signal to proceed with creation — an idiomatic pattern when using the Keycloak Admin Client.

MapStruct Mapper

The KeycloakRoleMapper converts between Keycloak's RoleRepresentation (Admin Client model) and the service's local KeycloakRole record:

Java code-highlight
@Mapper(componentModel = "spring")
public interface KeycloakRoleMapper extends BaseMapper<RoleRepresentation, KeycloakRole> {
    KeycloakRole toDto(RoleRepresentation roleRepresentation);
    List<KeycloakRole> toDtoList(List<RoleRepresentation> roleRepresentations);
}

The BaseMapper<E, D> contract from arya-banking-common also requires toEntity and toEntityList, which MapStruct generates automatically.


Configuration

Yaml code-highlight
app:
  config:
    keycloak:
      url: http://localhost:5433
      realm: event-based-banking-application
      client-id: admin-service-client
      client-secret: ${ADMIN_SERVICE_CLIENT_SECRET}
Keycloak runs on port 5433 in the local Docker environment — non-standard. Ensure this port is not blocked by a firewall and is exposed correctly in the keycloak.yml Docker Compose file in the infra repo.
keycloakoauth2clientroles