Auth Hub v1

Auth Hub

O Identity Provider (IdP) central da Innoscience. Uma única base de usuários e um único login para todos os produtos — cada app valida os tokens de forma distribuída e mantém seu próprio banco.

O Auth Hub resolve o problema de "vários logins espalhados": ele é a fonte única de verdade de quem é o usuário e a que produtos ele tem acesso. Os apps (Sonar, Innocase, Innoup, etc.) não têm login próprio — recebem o usuário já autenticado, via um handoff por código, e apenas espelham a identidade localmente para aplicar suas próprias regras (RLS, papéis, permissões).

Componente Repositório / pasta Stack Papel
Hub (backend) auth-hub/ FastAPI + asyncpg + Postgres Emite e valida JWT, cadastro, SSO, admin
Front de login auth-hub-front/ React + Vite + TypeScript Tela de login, seletor de produtos, convite/reset
Skill de integração auth-hub/skills/… Docs + código pronto Guia devs a plugar um app novo no hub
App consumidor cada produto (ex.: sonar-api) FastAPI / Node / etc. Consome o hub; tem seu próprio Postgres
💡
Princípio central

Senha e identidade vivem só no hub. Cada app tem a sua própria base de usuários, mas o usuário lá é um espelho sem senha — existe apenas para FKs, RLS e permissões locais.

Arquitetura & Fluxo

O usuário loga uma vez no front central. Para entrar num produto, o hub emite um código de uso único (SSO), o app troca esse código por tokens no backend e cria a própria sessão. O token nunca trafega na URL.

Fluxo de SSO (login → app de destino)
1. Usuário abre o app sem sessão
2. App  →  redireciona para  {HUB_FRONT_URL}/?projeto=<slug>
3. Front central pede e-mail/senha e autentica no hub (POST /login)
4. Front  →  POST /sso/authorize {project_slug}   →  { code, redirect_url }
5. Front  →  redireciona para  {redirect_url}?code=XXXX
6. App (backend)  →  POST /sso/exchange {code}   →  { access_token, refresh_token }
7. App valida o token (offline, via JWKS), CADASTRA o usuário local, cria sessão

Validação offline: o access token é um JWT RS256. Os apps buscam a chave pública uma vez em /.well-known/jwks.json, cacheiam e validam todos os tokens localmente — o hub não é chamado a cada request.

Sem parâmetro ?projeto=

Se o usuário abrir o front central sem o parâmetro, ele vê o seletor de produtos (os apps a que tem acesso, via GET /me) e escolhe para onde ir.

Estrutura do Código

Backend — auth-hub/

árvore
auth-hub/
├── app/
│   ├── main.py        # todos os endpoints (login, sso, admin, me, jwks…)
│   ├── config.py      # Settings (pydantic) — lê o .env
│   ├── crypto.py      # Argon2id, geração de chave RSA, assinar/validar JWT
│   ├── db.py          # pool asyncpg
│   ├── email.py       # envio via Resend (não-fatal)
│   └── jwks.py        # converte a chave pública PEM em JWK
├── schema.sql         # schema completo (banco novo)
├── migrates/          # migrações incrementais 002 → 003 → 004
├── skills/auth-hub-integration/   # a skill de integração + assets
├── requirements.txt
├── railpack.json      # start command + pin do Python p/ Railway
└── .env.example

Front — auth-hub-front/

árvore
auth-hub-front/
├── src/
│   ├── App.tsx        # orquestra fases e o roteamento simples (por pathname)
│   ├── api.ts         # cliente do hub (login, refresh, me, ssoAuthorize…)
│   ├── auth.ts        # sessão (refresh no localStorage), leitura de ?projeto= e rota
│   ├── i18n.ts        # textos em pt-BR / es / en
│   ├── types.ts       # Tokens, Me, Project…
│   ├── styles.css     # tema claro, split-screen
│   └── components/    # LoginForm, AppGrid, AcceptInvite, ResetPassword,# ForgotPassword, BrandPanel, Wordmark, Field, LangSwitch
├── public/logo.svg    # logo oficial (fallback p/ texto se ausente)
└── .env               # VITE_HUB_URL
🧭
Rotas do front

/ (login + seletor), /accept-invite?token=…, /reset-password?token=… e /forgot-password. Roteamento por pathname, sem router externo.

Env — Backend (Hub)

Copie .env.example para .env. Só DATABASE_URL é obrigatória; o resto tem default (mostrado na tabela). O .env é gitignorado — nunca comite credenciais.

Variável Default Significado
DATABASE_URL obrig. Postgres do hub. Sem ela o app não sobe. Na Railway use ${{Postgres.DATABASE_URL}}.
JWT_ISSUER opc. https://auth.innoscience.internal Claim iss. Se mudar, todos os apps precisam usar o mesmo valor.
JWT_AUDIENCE opc. innoscience Claim aud. Idem — precisa bater nos apps.
ACCESS_TOKEN_TTL_SECONDS opc. 900 Validade do access token (15 min).
REFRESH_TOKEN_TTL_SECONDS opc. 2592000 Validade do refresh token (30 dias).
INVITE_TTL_SECONDS opc. 172800 Prazo para aceitar o convite (48h).
RESET_TTL_SECONDS opc. 3600 Prazo do link de reset de senha (1h).
SSO_CODE_TTL_SECONDS opc. 120 Prazo entre emitir o code SSO e o app trocá-lo (2 min).
APP_BASE_URL opc. http://localhost:3000 Base dos links de convite/reset nos e-mails. Aponte para o front (ex.: a URL publicada, ou :5173 em dev).
FRONTEND_ORIGINS opc. http://localhost:5173,http://localhost:4173 Origens liberadas no CORS (CSV). Sem a URL do front aqui, o navegador bloqueia o login.
RESEND_API_KEY opc. vazio Chave da Resend. Vazio = modo dev (não envia; devolve dev_invite_link/dev_reset_link na resposta).
EMAIL_FROM opc. Innoscience Auth <onboarding@resend.dev> Remetente. O domínio precisa estar verificado na Resend — o onboarding@resend.dev só envia para o e-mail da própria conta.

Env — Front

O front tem uma única variável. No Vite, variáveis expostas ao browser precisam do prefixo VITE_.

Variável Exemplo Significado
VITE_HUB_URL http://localhost:8000 URL base da API do hub. Em produção, a URL pública do backend.
⚠️
Deploy estático

O host do front precisa de SPA fallback (qualquer rota → index.html), senão abrir /accept-invite direto dá 404. O vite dev/preview já faz isso; um host estático puro, não.

Env — App consumidor

Variáveis que cada produto novo configura para consumir o hub (entregues prontas pela skill). Nenhum segredo compartilhado na validação — ela usa a chave pública.

Variável Significado
HUB_URL API do hub (ex.: https://auth.innoscience.app).
HUB_FRONT_URL Front central de login — para onde mandar quem não está logado.
HUB_JWKS_URL Opcional; derivado de {HUB_URL}/.well-known/jwks.json.
HUB_ISSUER / HUB_AUDIENCE Precisam bater com o JWT_ISSUER/JWT_AUDIENCE do hub.
PROJECT_SLUG O slug deste app no hub (ex.: innocase). Define o acesso exigido.
HUB_CLIENT_SECRET Segredo do projeto, exigido no /sso/exchange (se configurado no hub).
DATABASE_URL Postgres local do app. Use um role restrito (não superuser) para a RLS valer.

Banco de Dados

O hub usa Postgres. Abaixo, todas as tabelas e colunas. PK = chave primária.

organizations — tenants (empresa + cada cliente)

Coluna Tipo Descrição
id PK uuid Identificador da org.
slug text · unique Vai no JWT (org). Ex.: innoscience, cliente-acme.
name text Nome de exibição.
is_internal bool true só para a org da própria empresa.
created_at timestamptz Criação.

users — identidade central (login por e-mail)

Coluna Tipo Descrição
id PK uuid Vai no JWT como sub.
org_id uuid → organizations Org (tenant) do usuário.
email text · unique Único globalmente (login é por e-mail).
name text · null Nome de exibição (vai no JWT e no /me).
password_hash text · null Argon2id. null até aceitar o convite.
is_active bool Ativa ao definir a senha.
is_super_admin bool Super-admin da plataforma.
mfa_secret, mfa_enabled text/bool Hooks para MFA (não usados no v1).
created_at, updated_at timestamptz Timestamps.

projects — catálogo global de apps

Coluna Tipo Descrição
id PK uuid Identificador do projeto.
slug text · unique Vai no JWT em projects. Ex.: sonar, innocase.
name text Nome de exibição.
redirect_url text · null Para onde o SSO redireciona (o /auth/callback do app).
client_secret text · null Exigido no /sso/exchange, se definido.

org_projects — quais projetos cada org pode usar

Coluna Tipo Descrição
org_id PK uuid → organizations A org.
project_id PK uuid → projects O projeto habilitado. Trava que impede cliente receber projeto interno.

user_access — quem acessa o quê, com qual papel

Coluna Tipo Descrição
user_id PK uuid → users O usuário.
project_id PK uuid → projects O projeto.
role text Papel do usuário nesse projeto (ex.: admin, member).
granted_at timestamptz Concessão.

one_time_tokens — convite e reset de senha

Coluna Tipo Descrição
id PK uuid
user_id uuid → users Dono do token.
type text invite ou reset.
token_hash text · unique SHA-256 do token (o cru vai só no link).
expires_at, used_at timestamptz Validade e consumo.
created_at timestamptz Criação.

sso_codes — códigos de uso único do handoff

Coluna Tipo Descrição
id PK uuid
user_id uuid → users Usuário logado.
project_id uuid → projects App de destino.
code_hash text · unique SHA-256 do code.
expires_at, used_at timestamptz Curto (~2min) e uso único.
created_at timestamptz Criação.

refresh_tokens — rotation com detecção de reuso

Coluna Tipo Descrição
id PK uuid
user_id uuid → users Dono.
family_id uuid Mesma família = mesma sessão. Reuso revoga a família toda.
token_hash text · unique SHA-256 do token.
expires_at timestamptz Validade.
revoked_at, used_at timestamptz · null Revogação e rotação.
created_at timestamptz Criação.

signing_keys — chaves de assinatura do JWT

Coluna Tipo Descrição
kid PK text Key id (vai no header do JWT). Permite rotação.
private_pem text Chave privada — fica só aqui.
public_pem text Chave pública — publicada em /jwks.json.
is_active bool Só uma ativa para assinar.
created_at timestamptz Criação.
🗄️
Banco do app consumidor (separado)

Cada app tem seu próprio Postgres com a tabela app_users (id, org, email, name, role, last_seen_at) — um espelho sem senha, preenchido via upsert a partir do token. As tabelas de dados usam RLS por org + sub.

Endpoints da API

Método Rota Body / uso
POST /login {email, password} → access + refresh
POST /refresh {refresh_token} → novo par (rotation)
POST /logout {refresh_token} → revoga a família
GET /me Bearer → perfil + projetos do usuário
GET /.well-known/jwks.json Chave pública para validação offline
POST /accept-invite {token, password, name?} → define senha + auto-login
POST /forgot-password {email} → sempre ok (anti-enumeração)
POST /reset-password {token, password} → troca senha + revoga sessões
POST /sso/authorize Bearer + {project_slug}{code, redirect_url}
POST /sso/exchange {code, client_secret?} → access + refresh
POST /admin/orgs {slug, name, is_internal}
POST /admin/projects {slug, name, redirect_url?, client_secret?}
POST /admin/org-projects {org_slug, project_slug} → habilita projeto p/ org
POST /admin/users {email, org_slug, name?, projects[], is_super_admin} → convida
POST /admin/access {user_id, project_slug, role}
DELETE /admin/access {user_id, project_slug}
GET /health {status: "ok"}

Claims do JWT

O access token é um JWT RS256. Os apps validam offline (assinatura + aud + iss) e usam org + sub na RLS.

payload do access token
{
  "sub":   "user-uuid",              // id do usuário (chave da RLS/provisioning)
  "email": "ana@empresa.com",
  "name":  "Ana Silva",              // pode ser null
  "org":   "cliente-acme",           // tenant — isola dados
  "projects": { "sonar": "admin" }, // { slug-do-app: papel }
  "iss": "https://auth.innoscience.internal",
  "aud": "innoscience",
  "exp": 1234567890
}

O acesso a um app é ter o PROJECT_SLUG presente em projects; o valor é o papel do usuário naquele app.

Usando a Skill

A skill auth-hub-integration guia (e entrega o código pronto para) plugar um app novo no hub: receber o handoff, trocar o code por tokens, cadastrar o usuário na base local (sem senha) e aplicar RLS por tenant. Ela cobre FastAPI e Node/Express.

Instalar

Copie a pasta da skill para onde o Claude Code procura skills — por projeto ou pessoal:

Terminal
# por projeto (vai junto no git do app):
<app>/.claude/skills/auth-hub-integration/

# ou pessoal (vale em todos os seus projetos):
~/.claude/skills/auth-hub-integration/

Também há o pacote auth-hub-integration.skill (um arquivo único) para instalar com um clique em clientes que oferecem o botão Save skill.

Como acionar

Skills no Claude Code disparam pela descrição, não por comando. Depois de instalada, abra o Claude Code no repositório do app e descreva a tarefa — ex.: "meu app novo precisa usar o login central e criar o usuário no meu banco a partir do token". A skill assume e segue o passo a passo.

📦
Repositório

Repositório de skills: github.com/Innoscience-tech/skills-tech.

O que muda ao plugar um app novo

Onde O que fazer Tamanho
Hub (admin) cadastrar o projeto com redirect_url + client_secret e habilitar p/ as orgs config, sem código
Backend do app callback, exchange, cadastro local, RLS, validação, refresh é onde mora tudo — a skill entrega pronto
Front do app redirecionar quem não está logado p/ {HUB_FRONT_URL}/?projeto=<slug> ~1 linha, sem tela de login

Deploy

Backend (Railway / Railpack)

O app é um pacote (app/main.py), então o start command é explícito no railpack.json, junto do pin do Python (o asyncpg 0.29 não compila no 3.13):

railpack.json
{
  "$schema": "https://schema.railpack.com",
  "packages": { "python": "3.12" },
  "deploy": {
    "startCommand": "uvicorn app.main:app --host 0.0.0.0 --port $PORT"
  }
}

Migrações do banco

Banco novo: aplique schema.sql. Banco existente: rode as migrações em ordem, conforme a versão (todas idempotentes):

ordem das migrações
migrate_002_multitenancy.sql   # organizations, org_projects, one_time_tokens…
migrate_003_sso.sql            # sso_codes + redirect_url/client_secret
migrate_004_user_name.sql      # users.name

Rodar localmente

Terminal (da raiz do auth-hub)
.venv\Scripts\python.exe -m uvicorn app.main:app --reload --port 8000