Fluxo 3 - Ativação de Conta: Integração Keycloak + Circle
Vincular automaticamente um usuário à comunidade Circle quando ele é adicionado ao grupo "Assinantes Circle" no Keycloak, garantindo acesso imediato e sincronização de dados de perfil.
Contexto e Arquitetura de Segurança
SSO (Single Sign-On) com Controle de Acesso via Keycloak
Utilizamos Keycloak como provedor SSO para autenticação na plataforma Circle. Como o Circle não implementa controle de acesso nativo (roles/permissions), implementamos uma camada de segurança diretamente no Keycloak:
- Executor
Restrict Client Auth: Apenas usuários que são membros do grupo "Assinantes Circle" conseguem acessar o cliente OIDC do Circle - Autorização exclusiva por grupo: Independente de outros papéis ou permissões
- Sincronização em tempo real: Quando adicionado/removido do grupo, o acesso é ativado/desativado imediatamente
Macro Arquitetura
| Componente | Responsabilidade | Ferramenta |
|---|---|---|
| Gatilho | Monitorar novo membro em grupo | Postgres Trigger (user_group_membership) |
| Validação | Confirmar que é o grupo certo | N8N (condicional) |
| Busca de dados | Recuperar dados do usuário | Keycloak (Query Postgres) |
| Integração Circle | Verificar/criar membro na comunidade | Circle API |
| Atualização de perfil | Sincronizar IDs entre sistemas | Keycloak API |
Fluxo Técnico Detalhado
1. Gatilho: Monitoramento de Grupo no Keycloak
O que dispara:
- Inserção de novo registro na tabela user_group_membership do banco de datos do Keycloak
- Representa: um usuário foi adicionado a um grupo
Dados capturados:
{
"group_id": "f8a91e89-f8dc-459d-9c59-3b1aaf945b28", // ID do grupo
"user_id": "17896b50-8e7a-4715-b217-3caf12f090de", // ID do usuário
"membership_type": "UNMANAGED" // Tipo de associação
}
2. Validação: É o Grupo Assinantes Circle?
Condicional:
IF group_id == "f8a91e89-f8dc-459d-9c59-3b1aaf945b28" THEN
→ Continuar com ativação
ELSE
→ Parar (No Operation)
Por quê: Vários grupos existem no Keycloak (admin, moderators, etc). Essa validação garante que o fluxo de ativação só dispara para o grupo certo.
3. Busca de Dados do Usuário
Query executada:
SELECT
ue.id AS user_id,
ue.email AS email,
ue.first_name AS first_name,
ue.last_name AS last_name,
COALESCE(
jsonb_object_agg(ua.name, ua.value) FILTER (WHERE ua.name IS NOT NULL),
'{}'::jsonb
) AS attributes
FROM user_entity ue
LEFT JOIN user_attribute ua ON ua.user_id = ue.id
INNER JOIN realm r ON r.id = ue.realm_id
WHERE ue.id = '{{ user_id }}'
AND r.name = 'bonde'
GROUP BY ue.id, ue.email;
Dados retornados:
| Campo | Tipo | Uso |
|----|----|----|
| user_id | UUID | Identificador único no Keycloak |
| email | string | Identificador no Circle |
| first_name, last_name | string | Dados de perfil |
| attributes | JSON | Metadados customizados (ex: circle_community_id) |
4. Verificar Existência do Atributo circle_community_id
Condicional:
IF attributes.circle_community_id EXISTS THEN
→ Usuário já foi vinculado ao Circle
→ Buscar dados do membro no Circle
ELSE
→ Primeira ativação
→ Criar novo membro no Circle
Importância: Evita criar duplicatas no Circle.
5A. Caminho: Membro Já Existe no Circle
Passo 1 - Buscar membro na Circle via email:
GET https://app.circle.so/api/admin/v2/community_members/search?email={{user.email}}
Passo 2 - Validar se encontrou:
IF response.error == null THEN
→ Membro encontrado, continuar
ELSE
→ Erro na busca, parar
Passo 3 - Autenticar no Keycloak:
POST https://auth.bonde.org/realms/bonde/protocol/openid-connect/token
grant_type: "client_credentials"
client_id: "n8n"
client_secret: "***"
Passo 4 - Montar payload com novo ID:
{
"email": "{{ user.email }}",
"firstName": "{{ user.first_name }}",
"lastName": "{{ user.last_name }}",
"attributes": {
"...": "{{ other_attributes }}",
"circle_community_member_id": "{{ circle_member.id }}"
}
}
Passo 5 - Atualizar usuário no Keycloak:
PUT https://auth.bonde.org/admin/realms/bonde/users/{{ user.id }}
Authorization: Bearer {{ token }}
Body: {{ payload }}
5B. Caminho: Criar Novo Membro no Circle
Passo 1 - Criar membro na comunidade Circle:
POST https://app.circle.so/api/admin/v2/community_members
{
"email": "{{ user.email }}",
"skip_invitation": true // Não enviar convite por email
}
Resposta esperada:
{
"community_member": {
"id": "abc123...",
"email": "{{ user.email }}"
}
}
Passo 2 - Autenticar no Keycloak:
POST https://auth.bonde.org/realms/bonde/protocol/openid-connect/token
(mesmo que caminho anterior)
Passo 3 - Montar payload com novo ID:
{
"email": "{{ user.email }}",
"firstName": "{{ user.first_name }}",
"lastName": "{{ user.last_name }}",
"attributes": {
"...": "{{ other_attributes }}",
"circle_community_member_id": "{{ community_member.id }}"
}
}
Passo 4 - Atualizar usuário no Keycloak:
PUT https://auth.bonde.org/admin/realms/bonde/users/{{ user.id }}
Authorization: Bearer {{ token }}
Body: {{ payload }}
Resultado Esperado
Estado Antes do Fluxo
- ✗ Usuário adicionado ao grupo "Assinantes Circle" no Keycloak
- ✗ Sem vinculação no Circle
- ✗ Sem permissão para fazer login no Circle
Estado Depois do Fluxo
- ✅ Usuário vinculado como membro da comunidade Circle
- ✅ Atributo
circle_community_member_idregistrado no Keycloak - ✅ SSO preparado: próximo login no Circle será bem-sucedido
- ✅ Dados de perfil sincronizados em ambos os sistemas
Possíveis Cenários de Erro
| Erro | Causa | Resolução |
|---|---|---|
| Group ID inválido | Usuário adicionado a outro grupo | O fluxo ignora, nenhuma ação é tomada |
| Usuário não encontrado no Keycloak | Falha na query | Verifique conexão com banco de dados Keycloak |
| Email duplicado no Circle | Mesmo email já existe como membro | Fluxo recupera o ID existente e sincroniza |
| Falha ao atualizar Keycloak | Token expirado ou permissões insuficientes | Verifique credenciais do cliente N8N no Keycloak |
Conceitos-Chave
Circle Community Member ID
Identificador único que vincula um usuário no Circle ao seu registro no Keycloak. Armazenado em:
Keycloak → user_attribute[circle_community_member_id]
Por quê importante: Garante sincronização bidirecional (atualizações de perfil Circle → Keycloak funcionam via esse ID).
Skip Invitation
Flag skip_invitation: true ao criar membro no Circle:
- Não envia email de convite ao novo membro
- Usuário terá acesso via SSO do Keycloak (não precisa confirmar por email)
Autorização SSO vs Autorização Circle
Camada 1 (Keycloak): Camada 2 (Circle):
├─ Está no grupo? ← Autorização ├─ Existe membro? ← Validação
└─ SIM → gera JWT └─ SIM → sessão de usuário
Se removido do grupo Keycloak → JWT revogado → Sem acesso ao Circle (mesmo que membro exista lá).