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 |
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.
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.
?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/
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/
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
/ (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. |
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. |
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.
{
"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:
# 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 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):
{
"$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):
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
.venv\Scripts\python.exe -m uvicorn app.main:app --reload --port 8000