Receita — Setup multi-conta (organizações)

Cenário: seu SaaS permite que usuários criem organizações e convidem membros. Cada organização é uma Account no Auth Platform. O mesmo usuário pode ser admin de "Empresa X" e membro de "Empresa Y".


Diferença entre 1:1 e multi-conta

1:1 (padrão)Multi-conta
Contas por usuário1 (auto-criada no cadastro)N (usuário cria)
Quem adiciona membrosSó o Platform AdminAdmin da conta (via sua API)
account_id no tokenSempre a conta pessoalA conta selecionada no login
Seletor de conta no loginNão apareceAparece quando tem > 1 conta

Passo 1 — Criar o sistema com multiAccount=true

SYSTEM=$(curl -s -X POST http://localhost:4000/admin/systems \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Financeiro SaaS",
    "redirectUris": ["http://localhost:3000/callback"],
    "allowedScopes": ["openid","email","profile","offline_access"],
    "multiAccount":           true,   # ← habilita múltiplas contas
    "allowSelfRegister":      true,
    "requireAdminActivation": false,
    "allowClientCredentials": true    # ← para chamadas M2M da sua API
  }')

SYSTEM_ID=$(echo $SYSTEM | jq -r '.id')
CLIENT_ID=$(echo $SYSTEM | jq -r '.clientId')
CLIENT_SECRET=$(echo $SYSTEM | jq -r '.clientSecret')  # guarde — aparece uma vez

Passo 2 — Criar as roles da organização

# Admin da conta — pode convidar membros
ADMIN_ROLE=$(curl -s -X POST http://localhost:4000/admin/systems/$SYSTEM_ID/roles \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"admin","isAdmin":true,"scopes":["fin:admin","fin:read","fin:write"]}')
ADMIN_ROLE_ID=$(echo $ADMIN_ROLE | jq -r '.id')

# Membro comum
MEMBER_ROLE=$(curl -s -X POST http://localhost:4000/admin/systems/$SYSTEM_ID/roles \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"membro","isAdmin":false,"scopes":["fin:read","fin:write"]}')
MEMBER_ROLE_ID=$(echo $MEMBER_ROLE | jq -r '.id')

Passo 3 — Usuário cria uma organização (via sua API)

Quando o usuário quer criar uma nova organização, sua API faz a chamada com um service token (client_credentials):

// Sua API: POST /orgs   { name: "Empresa X Ltda" }
// req.user.sub = userId do usuário autenticado (do JWT)

async function createOrganization(req: any, res: any) {
  const userId = req.user.sub;       // do JWT do usuário
  const { name } = req.body;

  // Sua API chama o Auth Platform com um service token
  const serviceToken = await getServiceToken();  // client_credentials

  const mbRes = await fetch('http://localhost:4000/admin/memberships', {
    method:  'POST',
    headers: {
      Authorization:  `Bearer ${serviceToken}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      userId,
      systemId:    process.env.SYSTEM_ID,
      accountName: name,              // ← nome da organização
      roles:       [process.env.ADMIN_ROLE_ID],  // ← o criador vira admin
    }),
  });

  const { account } = await mbRes.json() as any;
  
  // Opcional: inicializar dados da org na sua base
  await db.organization.create({
    data: { authAccountId: account.id, name, ownerId: userId },
  });

  res.status(201).json({ orgId: account.id, name });
}

Passo 4 — Seletor de conta no login

Quando o usuário tem > 1 conta, o Auth Platform mostra automaticamente o seletor de conta na tela de login.

Para pré-selecionar uma conta (trocar de organização sem logout):

// Iniciar login já selecionando uma conta específica
function switchOrganization(accountId: string) {
  userManager.signinRedirect({
    extraQueryParams: { account_id: accountId },
  });
}

// Após o login, o token terá:
// payload.account_id = accountId escolhida
// payload.roles = roles nesta conta específica
// payload.is_admin = true/false para esta conta

Passo 5 — Convidar membro para a organização

// Sua API: POST /orgs/:accountId/members   { email, roleId }
// Requer: req.user.is_admin === true  E  req.user.account_id === accountId

async function inviteMember(req: any, res: any) {
  const { accountId } = req.params;
  const { email, roleId } = req.body;

  // Validar que o solicitante é admin DESTA conta
  if (!req.user.is_admin || req.user.account_id !== accountId) {
    return res.status(403).json({
      error: 'Apenas o admin desta organização pode convidar membros',
    });
  }

  const serviceToken = await getServiceToken();
  const headers = { Authorization: `Bearer ${serviceToken}`, 'Content-Type': 'application/json' };

  // Buscar userId pelo e-mail
  const search = await fetch(
    `http://localhost:4000/admin/users?search=${encodeURIComponent(email)}`,
    { headers }
  ).then(r => r.json()) as any;

  if (!search.data?.length) {
    return res.status(404).json({ error: 'Usuário não encontrado na plataforma' });
  }

  const userId = search.data[0].id;

  // Conceder acesso à conta compartilhada do admin
  await fetch('http://localhost:4000/admin/memberships', {
    method: 'POST',
    headers,
    body: JSON.stringify({
      userId,
      accountId,   // ← conta existente do admin
      roles: [roleId ?? process.env.MEMBER_ROLE_ID],
    }),
  });

  res.status(201).json({ message: 'Membro adicionado com sucesso' });
}

Passo 6 — Isolamento de dados por organização

Sempre filtre pelo account_id do token — nunca pelo sub:

// Middleware para extrair a conta ativa
function getActiveAccount(req: any): string {
  return req.user.account_id;  // do JWT — imutável por request
}

// ✅ Correto — dados isolados por organização
app.get('/transacoes', authMiddleware, async (req, res) => {
  const accountId = getActiveAccount(req);
  const data = await db.transaction.findMany({
    where: { accountId },  // ← filtra pela conta do token
  });
  res.json(data);
});

// ❌ Errado — mistura dados de todas as organizações do usuário
app.get('/transacoes', authMiddleware, async (req, res) => {
  const data = await db.transaction.findMany({
    where: { userId: req.user.sub },  // ← ERRADO
  });
});

Passo 7 — Remover membro

// Sua API: DELETE /orgs/:accountId/members/:membershipId
async function removeMember(req: any, res: any) {
  const { accountId, membershipId } = req.params;

  // Validar que é admin desta conta
  if (!req.user.is_admin || req.user.account_id !== accountId) {
    return res.status(403).json({ error: 'Sem permissão' });
  }

  const serviceToken = await getServiceToken();
  await fetch(`http://localhost:4000/admin/memberships/${membershipId}`, {
    method: 'DELETE',
    headers: { Authorization: `Bearer ${serviceToken}` },
  });
  // → Auth Platform emite webhook user.access_revoked

  res.status(204).send();
}

Checklist

  • Sistema com multiAccount: true e allowClientCredentials: true
  • Service token configurado para chamadas da sua API ao Auth Platform
  • Sua API cria a organização via /admin/memberships (não o frontend)
  • Dados filtrados por account_id em todas as queries
  • Validação req.user.account_id === params.accountId antes de operações sensíveis
  • Webhook configurado para provisionar/desprovisionar dados

Exemplo funcional: examples/03-multi-conta com servidor Express completo