@tjpa/n8n-nodes-keycloak-guard
@tjpa/n8n-nodes-keycloak-guard
Community node para n8n com propósito único: proteger webhooks do n8n com
Keycloak. O login do usuário acontece inteiramente fora do n8n (keycloak-js +
Authorization Code Flow com PKCE, no browser); este pacote apenas injeta esse login no HTML
servido e valida o token nas chamadas de volta.
É uma reescrita enxuta e endurecida do n8n-nodes-keycloak
(MIT), do qual este projeto herda a licença e créditos.
Nodes
- Keycloak Guard — injeta o login Keycloak + um interceptor de
fetch/XMLHttpRequestnuma página HTML. - Keycloak Verify — valida o token enviado pelo interceptor e libera o fluxo do workflow.
Credencial: Keycloak Guard API
Uma única credencial, tipo keycloakGuardApi, com 5 campos:
| Campo | Uso |
|---|---|
serverUrl |
URL base do Keycloak |
realm |
Realm |
frontendClientId |
Client público (Standard Flow + PKCE), vai para o browser |
backendClientId |
Client confidential, usado só no server (Keycloak Verify) |
backendClientSecret |
Secret do client confidential. Nunca é injetado no HTML. |
O credential test bate em .well-known/openid-configuration do realm configurado.
serverUrl é sempre normalizado antes de montar qualquer endpoint — tanto faz digitar com
barra final, com /realms ou /realms/ no fim: tudo resolve para a mesma URL.
Fluxo completo
[Webhook GET] -> [Node HTML gera a página] -> [Keycloak Guard injeta login] -> [Respond to Webhook]
no browser: o usuário loga no Keycloak (fora do n8n)
chamadas JS da página passam pelo interceptor injetado, que anexa o Bearer token
[Webhook API] -> [Keycloak Verify] -> authorized -> resto do workflow usa item.json.user
-> unauthorized
-> error
Configuração dos 2 clients no Keycloak
- Client público (ex.:
n8n-frontend): Standard Flow habilitado, PKCES256,Valid Redirect URIs= URL da página servida pelo webhook,Web Origins= origin do n8n. - Client confidential (ex.:
n8n): usado só pelo Keycloak Verify para introspection.
Keycloak Guard
Parâmetros:
- HTML Field (
htmlField, defaulthtml) — nome do campo emitem.jsonque contém a string HTML a modificar (nem sempre se chamahtml). - Keycloak JS URL (
keycloakJsUrl) — default aponta pro build ESM (lib/keycloak.js) dokeycloak-js@26no CDN jsdelivr. A partir da v26 o pacote só é distribuído como módulo ES (sem bundle UMD/global emdist/), por isso o script injetado usa<script type="module">+import. Se sua rede não tiver saída pra CDN externo, hospede esse arquivo internamente e aponte esse campo pra lá. - Allowed API Origins (
apiOrigins, CSV) — origens além do mesmo-origin que também podem receber o headerAuthorization. Qualquer outro destino nunca recebe o token do usuário.
O node injeta, logo antes de </body> (ou no final da string, se não houver </body>):
<script type="module">comimport Keycloak from "<keycloakJsUrl>".- Inicialização:
new Keycloak({ url, realm, clientId: frontendClientId })comonLoad: 'login-required',pkceMethod: 'S256',checkLoginIframe: false. Sem sessão, redireciona para o login oficial do Keycloak; só segue com o usuário autenticado. - Um interceptor global de
window.fetcheXMLHttpRequest.prototype.sendque, antes de cada requisição, chamakeycloak.updateToken(30)(refresh se o token estiver perto de expirar) e só anexaAuthorization: Bearer <token>se o destino for o mesmo origin da página ou estiver na allowlist deapiOrigins.
O node só lê serverUrl, realm e frontendClientId da credencial — nunca referencia
backendClientId/backendClientSecret, garantindo que o secret jamais vá parar no HTML.
Keycloak Verify
Parâmetros:
- Token Key (
tokenKey, defaultheaders.authorization) — caminho (dot path) dentro deitem.jsononde está o token. O prefixoBeareré removido automaticamente, case-insensitive. A extração é feita por item, dentro do loop — diferente do node original, que tinha um bug de ler o token só do primeiro item e validar todos os outros com ele. - Verify Mode (
verifyMode):introspection(default):POSTem${base}/realms/${realm}/protocol/openid-connect/token/introspect, comContent-Type: application/x-www-form-urlencodedeAuthorization: Basic base64(backendClientId:backendClientSecret). Seactive=false, o item vai paraunauthorized. Pega tokens revogados.offline: valida a assinatura localmente via JWKS (${base}/realms/${realm}/protocol/openid-connect/certs), com cache (TTL configurável, default 1h) e checagem deexp/iss. Mais rápido, porém não detecta revogação — se isso importa para o seu caso de uso, prefiraintrospection.
- JWKS Cache TTL (Seconds) — só aparece com
verifyMode = offline. - Required Realm Roles, Required Client Roles (
client:role), Required Groups — gates opcionais, checados contrarealm_access.roles,resource_access[client].rolesegroupsdo token. Faltando algum, o item vai paraunauthorized.
Independentemente do modo escolhido, o JWT é sempre decodificado para extrair as claims —
introspection decide se o token está active, o decode do JWT popula os dados do usuário
(a introspection nem sempre retorna todas as claims).
3 saídas: authorized / unauthorized / error (erro de rede, JWKS inacessível, etc.).
No item que sai por authorized, é anexado item.json.user enxuto:
{
"sub": "...",
"preferred_username": "...",
"name": "...",
"given_name": "...",
"family_name": "...",
"email": "...",
"cpf": "...",
"groups": ["..."],
"roles": ["..."],
"membership_unit_ref": "..."
}
(membership_unit_ref só aparece se presente no token.) O token completo nunca é anexado
ao item, e nem o token nem o secret são logados em nenhum momento.
Configuração via CREDENTIALS_OVERWRITE_DATA
A credencial é preenchida globalmente pelo ambiente do n8n — no editor, o usuário só cria
a credencial vazia (os campos ficam ocultos) e seleciona. Não usa prefixo N8N_:
{
"keycloakGuardApi": {
"serverUrl": "https://sso.tjpa.jus.br",
"realm": "tjpa",
"frontendClientId": "n8n-frontend",
"backendClientId": "n8n",
"backendClientSecret": "<secret>"
}
}
Para o secret, prefira CREDENTIALS_OVERWRITE_DATA_FILE (arquivo montado read-only) em vez
de CREDENTIALS_OVERWRITE_DATA inline — a documentação do n8n avisa que variáveis de ambiente
podem vazar para os usuários (ex.: em nodes que exibem env vars).
Testando localmente
npm run build
Copie as pastas resultantes de dist/ para ~/.n8n/custom/ do container e reinicie o n8n.
Distribuição via GitLab Package Registry
- Publicar com o
publishConfigdopackage.jsonjá apontando para o Project Package Registry deste projeto (id85627349):O pipeline em"publishConfig": { "@tjpa:registry": "https://gitlab.com/api/v4/projects/85627349/packages/npm/" }.gitlab-ci.ymlpublica automaticamente ao criar uma tagvX.Y.Z, usando${CI_JOB_TOKEN}(não precisa cadastrar token manualmente). Há também um job manualpublish_npmque publica a mesma tag no npmjs.com público, usando a variável de CINPM_TOKEN. - Instalar via imagem Docker customizada, com
.npmrcmapeando o scope@tjpapara o registry do grupo,ENV N8N_CUSTOM_EXTENSIONS=/home/node/custom-nodesepnpm install(o n8n usapnpm;npm install -gnão funciona mais nas imagens atuais). Vejadocker/Dockerfileedocker/.npmrc.example.
Kubernetes em queue mode
A execução dos nodes roda nos workers, não no main — a imagem com este pacote precisa
estar presente no main e em todos os workers. Ao atualizar a versão, é preciso "rolar"
(rolling restart) todos os pods. O CREDENTIALS_OVERWRITE_DATA/_FILE também precisa estar
presente em todos os pods (main + workers), senão a credencial fica vazia em parte deles.
Testes
npm test
Cobre: normalização de serverUrl em todas as variações aceitas; injeção antes de </body>
(incluindo case-insensitive) e fallback de append quando não há </body>; ausência de
backendClientId/backendClientSecret no HTML gerado; extração de token com/sem Bearer
case-insensitive; extração independente por item (regressão do bug do node original);
active=false sempre resultando em unauthorized; gates de role/grupo autorizando e negando;
e o formato enxuto do user na saída authorized.
Licença
MIT. Ver LICENSE. Este projeto é uma reescrita do
n8n-nodes-keycloak, também MIT.