@adatechnology/keycloak-admin
v1.0.0
Published
Agnostic Keycloak Admin API client authenticated as a service account (client_credentials)
Maintainers
Readme
@adatechnology/keycloak-admin
Cliente do Keycloak Admin API autenticado como service account (client_credentials).
Agnóstico de framework e de runtime: só depende de fetch e de zod.
Diferença deliberada para o @adatechnology/nestjs-keycloak-admin: aquele obtém token com
grant_type=password usando o usuário administrador do realm master. Este nunca envia senha
nem usuário — a identidade é do client confidencial, com manage-users do realm-management.
Uso
import { createKeycloakAdminClient } from '@adatechnology/keycloak-admin'
const keycloak = createKeycloakAdminClient({
config: {
baseUrl: process.env.KEYCLOAK_URL,
clientId: process.env.KEYCLOAK_ADMIN_CLIENT_ID,
clientSecret: process.env.KEYCLOAK_ADMIN_CLIENT_SECRET,
realm: process.env.KEYCLOAK_REALM,
},
})
const { id } = await keycloak.createUser({
attributes: { company_id: companyId },
email: '[email protected]',
enabled: true,
firstName: 'Ada',
lastName: 'Lovelace',
password: { temporary: false, value: chosenPassword },
username: '[email protected]',
})Operações de usuário: createUser, findUserByEmail, listUsers, updateUser, setEnabled,
updateAttributes, deleteUser, setPassword, setTemporaryPassword.
Foto de perfil: setProfilePicture({ userId, pictureUrl }). O atributo é picture, o nome que o
OIDC reserva — com um mapeador no realm ele chega ao token, e a tela desenha o avatar sem uma
consulta por pessoa.
⚠️ O Keycloak não hospeda imagem: o valor é uma URL e o arquivo é do produto. Base64 no atributo
cresce o token até ele parar de caber no cabeçalho, e o sintoma aparece longe — login que funciona
no navegador e falha no curl. A operação lê os atributos antes de gravar, porque o Admin API
substitui o conjunto: mandar só a foto apagaria company_id e o resto.
Operações de grupo: createGroup, updateGroup, deleteGroup, listGroups, addUserToGroup,
removeUserFromGroup. Só o primeiro nível — grupo aninhado muda o significado de "pertencer",
porque quem está no filho herda o pai, e um produto que não modela hierarquia não deve criá-la por
acidente. A filiação é endereçada pelo usuário (/users/{id}/groups/{id}), não pelo grupo.
listUsers({ first, limit, search }) devolve { users, hasMore }. O realm não informa total, então
a página pede um registro a mais que o limite e descarta-o: é assim que hasMore sai sem uma
segunda chamada.
Token
Obtido sob demanda, guardado em memória e renovado 30s antes de expirar
(KEYCLOAK_ADMIN_TOKEN_RENEWAL_SKEW_MS). Chamadas concorrentes compartilham a mesma requisição em
voo — o Keycloak recebe uma, não N.
Injeção
fetch e now são injetáveis, o que torna rede e relógio observáveis em teste:
createKeycloakAdminClient({ config, fetch: stubFetch, now: clock.now })Erros
Toda falha vira KeycloakAdminError com code estável (KEYCLOAK_ADMIN_ERROR_CODE), status e
context. O contexto é montado por allowlist e passa por um redator que substitui clientSecret,
access token e senha por [REDACTED] — nem a mensagem nem o erro serializado carregam segredo.
Configuração inválida falha na construção do cliente, com o caminho do campo no contexto e nunca o valor.
