@riosaude/sso-kit
v0.4.4
Published
Migra aplicações existentes para o SSO RioSaúde: gera a integração da API NestJS e do front (Vite + React ou Next.js), inclusive webhook, verifica cada etapa de verdade e acompanha a evolução numa página
Maintainers
Readme
@riosaude/sso-kit
Migra uma aplicação já existente para o SSO RioSaúde. Para começar um
sistema novo do zero, use @riosaude/create-sso-app —
o sso-kit é para quem já tem uma aplicação NestJS rodando — com ou sem
autenticação própria — e precisa passar a entrar pelo SSO.
Comece aqui
Pré-requisitos: API em NestJS; front, se houver, em Vite + React ou
Next.js App Router; Node 20+; projeto versionado no git (comece numa
branch própria, ex.: integracao-sso).
- Escolha o modelo de permissão. Se o sistema atende várias unidades
(hospitais, unidades de saúde) e o acesso ou as permissões dependem da
unidade, use
scoped; senão,flat. Na dúvida, pergunte ao administrador do SSO — com o modelo errado a sessão chega sem permissões. Ver flat vs scoped. - Gere a integração, na pasta da API:
O comando pergunta o slug do sistema no SSO, o modelo, se há front, a pasta do front e o framework do front (Vite ou Next.js). Instala as dependências, cria onpx @riosaude/sso-kit add.enve mostra o que falta. - Ajuste as URLs para o endereço que o navegador usa —
localhostsó serve se o navegador estiver na mesma máquina:- no
.envda API:APP_URL,SSO_REDIRECT_URIeWEB_URL; - no
.env.localdo front:VITE_SSO_API_URL(Vite) ouNEXT_PUBLIC_SSO_API_URL(Next.js), com o prefixo da API (ex.:http://10.0.0.5:3100/api/v1). Se o projeto já tem a sua própria (VITE_API_URL/NEXT_PUBLIC_API_URL), o kit reaproveita e o código gerado lê as duas — não fique com duas variáveis para a mesma API. Se o seu front descobre a URL da API em tempo de execução (ex.: umconfig.jsonpor ambiente), façasrc/sso/apiusar a mesma origem: variável de build e configuração de runtime divergem no dia em que uma muda.
- no
- Peça o cadastro ao administrador do SSO. Rode
npx @riosaude/sso-kit status --abrir: a seção Dados para o cadastro no SSO traz slug,redirect_uris,post_logout_redirect_urisewebhook_urlprontos, com botão para copiar e enviar. Você recebe de voltaSSO_CLIENT_SECRETeSSO_WEBHOOK_SECRET. - Preencha os dois secrets no
.env, marque com@Public()as rotas da API que precisam continuar abertas e ajuste o front — a sessão passa a ser um cookie, não um token: ver Ajustes no front. - Suba a API e o front e prove a integração:
Corrija o que a página apontar até só restarem as etapas manuais.npx @riosaude/sso-kit status --live - Teste com um usuário real — entrar no sistema e, com a tela aberta,
ter o acesso revogado no painel (a sessão deve cair sozinha) — e registre:
A integração está concluída quando a página mostra Integração concluída. Commite onpx @riosaude/sso-kit confirmar login-real npx @riosaude/sso-kit confirmar revogacao-real.sso-kit/estado.jsonpara o time acompanhar.
Conforme o seu caso
Só o que se aplicar ao seu sistema — num app sem login nem usuários próprios, nenhum destes itens é necessário para concluir a integração.
- Quer restringir telas ou rotas por permissão? Sem isso, qualquer pessoa
com acesso ao sistema no SSO usa tudo (quem não tem acesso já é barrado
pelo próprio SSO). Para restringir, decore as rotas com
@RequirePermission('recurso.acao'); no front,ssoSession.hasPermission('recurso.acao')(Vite) ouuseSsoSession((s) => s.hasPermission)('recurso.acao')(Next.js) esconde o que o usuário não pode usar. Para cadastrar permissões e perfis no SSO sem digitar no painel, vejasso-kit rbac. - O app já tinha login próprio? Remova a tela de login antiga, a guarda
JWT/Passport e os endpoints de autenticação que o SSO substituiu. O
statusaponta rastros encontrados (@nestjs/passport,JwtStrategy…) numa etapa opcional. - Tabelas do domínio referenciam usuários (auditoria, "criado por")? Espelhe o usuário no banco local no login — ver Ponto de extensão.
- Ao levar para produção:
- peça um cadastro novo no SSO de produção (
https://sso2.riosaude.rio.br), com as URLs públicas do sistema — os secrets são outros; - sirva API e front em HTTPS: com
NODE_ENV=productiono cookie de sessão sai comSecuree não trafega em HTTP; - com mais de uma instância da API, troque o armazenamento de sessões em memória por Redis ou tabela (ver Sessão, cookies e tela de login).
- peça um cadastro novo no SSO de produção (
Ajustes no front
O add gera a sessão, a tela de login com a identidade do SSO e o gate que
protege o app. O que ele não reescreve é o código do seu app — estes
ajustes são seus, e a página do status aponta os que faltam.
Vite + React
Gerado em src/sso/, sem dependência nova. O add envolve o <App /> do
src/main.tsx:
<SsoSessionGate>
<App />
</SsoSessionGate>O gate fica fora do router e exibe sozinho a tela de login em /login —
não é preciso criar rota. Por padrão todo o app exige login; para deixar
páginas abertas, passe rotasPublicas={['/', '/sobre']}.
- Chamadas à API mandam o cookie. Com
fetch, use ossoFetchdesrc/sso/apioucredentials: 'include'. Com axios, crie a instância comwithCredentials: truee remova o headerAuthorizatione o token dolocalStorage:export const api = axios.create({ baseURL: import.meta.env.VITE_SSO_API_URL, withCredentials: true }); - Troque a sua tela/modal de login por um link para
/logine o botão Sair porssoSession.logout()(logout único no SSO). Remova cadastro, troca de senha e gestão de usuários — isso passa a ser no painel do SSO. - Quem é o usuário:
useSsoSession((s) => s.user)em componentes;ssoSession.hasPermission('recurso.acao')para esconder o que ele não pode usar. Rotas que liberavam acesso por um campo do usuário (ex.:tipo) passam a olhar os perfis/permissões da sessão. - Só no modelo
scoped(multiunidade): se o usuário tem mais de uma unidade, o gate pede a escolha antes de abrir o app. Ponha<SsoSeletorUnidade />(desrc/sso/SsoSeletorUnidade) no cabeçalho para trocar de unidade. OssoFetchjá enviax-unidade-id; no axios, acrescente:
Quando a unidade ativa muda, recarregue os dados da tela (ex.: invalidar as queries do React Query).api.interceptors.request.use((config) => { const unidade = ssoSession.get().activeUnitId; if (unidade) config.headers.set('x-unidade-id', unidade); return config; }); - Rotas e menus por setor (opcional,
scoped): se o sistema organiza o trabalho por setor,ssoSession.setores()devolve os setores do usuário na unidade ativa, cada um comslug. Monte as rotas com ele (ex.:/:setor/admin) em vez de uma lista fixa: setor novo cadastrado no painel do SSO (Estrutura → unidade → Setores → Novo setor) já aparece no app, e o acesso vem do setor concedido ao usuário.
Next.js (App Router)
Gerado em lib/sso/, components/sso/ e app/login/; instala zustand. O
add envolve o {children} do layout raiz com <SsoSessionGate>.
- Chamadas à API mandam o cookie:
ssoApiFetchdelib/sso/api,fetchcomcredentials: 'include'ou axios comwithCredentials: true— sem headerAuthorization. - Login e logout: link para
/login; sair comuseSsoSession((s) => s.logout)(). - Quem é o usuário:
useSsoSession((s) => s.user)euseSsoSession((s) => s.hasPermission)('recurso.acao'). - Só no modelo
scoped: unidade ativa emuseSsoSession((s) => s.activeUnitId), troca comuseSsoSession((s) => s.setActiveUnit)(id);ssoApiFetchenviax-unidade-idna leitura da sessão — nas suas chamadas, mande o mesmo header. Setores comslugpara rotas e menus:useSsoSession((s) => s.setores)().
Como funciona
No espírito do shadcn/ui: o add copia código de verdade para dentro do seu
projeto, e o status prova — contra o código, o SSO e o app rodando — o que
já está pronto, guardando a evolução até a conclusão.
npx @riosaude/sso-kit add # gera o módulo de autenticação + webhook
npx @riosaude/sso-kit status --live # verifica tudo e atualiza a página de acompanhamento
npx @riosaude/sso-kit confirmar login-real
npx @riosaude/sso-kit doctor # mesmas verificações, só leitura (CI)Acompanhamento: sso-kit status
Cada execução verifica as etapas da integração, grava o resultado em
.sso-kit/estado.json e gera .sso-kit/integracao.html, uma página que
funciona offline:
- Resumo — etapas obrigatórias concluídas, falhas e próximos passos com a evidência e o como resolver
- Etapas por fase — status de cada uma, desde quando, quando foi verificada
- Evolução — gráfico do percentual concluído por verificação e histórico com quem rodou, em qual commit e o que mudou
- Conclusão — o veredito (concluída / em andamento / com falhas) e o status final de cada etapa com a evidência
Commite o estado.json: é ele que carrega a evolução para o time. A página é
derivada dele e fica no .gitignore — quem quiser vê-la roda sso-kit status
--abrir. Se o código roda numa máquina e o navegador está em outra, use
sso-kit status --servir (porta 8099 por padrão) e abra
http://<endereço-da-máquina>:8099/.
De onde vem a conclusão
Nada é marcado como concluído por declaração quando dá para provar:
| Fase | Como é verificada |
|---|---|
| Dependências, código, .env, frontend | lendo o projeto, a cada execução |
| Cadastro no SSO | --live: sonda o SSO sem criar sessão — /oauth/authorize sem seguir o redirect distingue client inexistente, redirect_uri não cadastrada e cadastro correto; /oauth/token com code inválido prova o client_secret (secret errado = invalid_client, certo = invalid_grant) |
| Aplicação em execução | --live: chama o login do app e confere o redirect; envia ao webhook um evento com assinatura inválida (tem que recusar) e um assinado com o SSO_WEBHOOK_SECRET para um usuário fictício (tem que aceitar) |
| Usuários com acesso | --live com SSO_API_TOKEN: consulta a API v1; sem token, confirmação manual |
| Login real e revogação real | só uma pessoa atesta: sso-kit confirmar login-real / confirmar revogacao-real — registra quem, quando e o commit |
Sem --live, etapas de rede mantêm o último resultado real com a data em que
foram verificadas; o que nunca rodou fica não verificada (não conta como
concluída nem como falha). Etapas confirmadas por pessoa aparecem sempre
como "confirmada manualmente", separadas das provadas automaticamente.
npx @riosaude/sso-kit status --live --exigir-conclusao # sai com 1 se não estiver concluída
npx @riosaude/sso-kit confirmar revogacao-real --nota "sessão caiu em ~2s"
npx @riosaude/sso-kit confirmar login-real --desfazersso-kit rbac
npx @riosaude/sso-kit rbac [--dir <api>] [--slug <slug>]Gera scripts/sync-rbac.ts e os comandos rbac:sync / rbac:push. O script
sobe o contexto do Nest, varre os controllers e monta o catálogo a partir das
rotas com @RequirePermission — cada permissão sai com as rotas que ela
libera, e as rotas sem permissão aparecem à parte, para nada passar
despercebido. Depois ele publica tudo no SSO (PUT /api/v1/rbac/sync).
npm run rbac:sync # só lista (dry-run)
npm run rbac:push # lista e publica
npm run rbac:push -- --prune # publica e remove permissões órfãs no SSO- Publicar exige
SSO_ISSUERe um token de API do sistema, criado no painel em Sistemas → Tokens (não é oclient_secret: esse serve só para o login). A variável do token é nomeada a partir do slug, ex.:MEU_SISTEMA_SSO_API_TOKEN. - O que é automático: permissões e as rotas de cada uma. O que não é:
quais permissões cada perfil tem — isso é decisão de negócio e fica na
constante
ROLES, no topo do script, versionada com o código. rbac:pushsubstitui o conjunto de permissões de cada perfil declarado, e a mudança propaga na hora para quem tem o perfil (revogação + webhook).- Se o acesso do seu sistema é por unidade/setor, não crie um perfil por
setor: o setor chega na claim com o
slug, e setor novo passa a funcionar sem mexer no script.
sso-kit doctor
As mesmas verificações do status, sem gravar estado nem página. Sai com
código 1 se alguma etapa obrigatória estiver em falha — dá para usar como gate
de pipeline.
npx @riosaude/sso-kit doctor --dir ../minha-api --live
npx @riosaude/sso-kit doctor --jsonsso-kit add
Gera src/sso/ no projeto NestJS: login OIDC+PKCE, sessão local (cookie
httpOnly), logout único, webhook (user.revoked e
user.permissions_updated) e guardas de autenticação/autorização. A própria
SsoModule se registra como APP_GUARD.
npx @riosaude/sso-kit add
npx @riosaude/sso-kit add --dir ../minha-api --web ../minha-web --front vite --slug meu-sistema --model scoped --yesPergunta só o que não dá para inferir: slug do sistema no SSO, modelo
de permissão (flat ou scoped, ver abaixo), se há front e o framework
do front — Vite ou Next.js, já sugerindo o que detectou (--front vite|next
para rodar sem perguntas). Depois faz sozinho, sem sobrescrever nada seu:
| Onde | O que |
|---|---|
| app.module.ts | importa SsoModule.register({}) |
| main.ts | import 'dotenv/config' (se nada carregava o .env), cookie-parser, rawBody: true, credentials: true no enableCors({...}) quando há front |
| .env | cria ou completa as chaves — APP_SECRET já gerado, valores existentes nunca trocados — e garante .env no .gitignore |
| dependências | instala só as que faltam, com o npm/pnpm/yarn do projeto (--sem-instalar pula) |
| front Vite | src/sso/ (sessão, gate, login, escolha de unidade no scoped), envolve o <App /> do main.tsx e grava VITE_SSO_API_URL no .env.local |
| front Next.js | lib/sso/, components/sso/, app/login, envolve o {children} do layout raiz e grava NEXT_PUBLIC_SSO_API_URL no .env.local |
| visual | logos e fonte do painel do SSO em public/sso-kit/ |
Arquivo que já existe sai ao lado como .sso-kit-novo; patch que não
reconhece o formato do arquivo não é aplicado e vira etapa pendente na página.
Ao final roda o primeiro status.
O que continua sendo do dev (e a página aponta cada um):
SSO_CLIENT_SECRETeSSO_WEBHOOK_SECRET— só o painel do SSO os mostra;APP_URL/WEB_URLcom o endereço que o navegador enxerga;- os ajustes no front — a etapa lista arquivo e
quantidade de
fetch/axios sem cookie; - toda rota da API passa a exigir sessão: marque com
@Public()o que precisa continuar aberto (health check, por exemplo).
Sessão, cookies e tela de login
- Sessão no servidor. O cookie leva só um id assinado; as claims ficam na
API. A revogação (
user.revoked) apaga a sessão e a tela aberta cai sozinha pelo canal SSE. O armazenamento é em memória: vale para uma instância, e reiniciar a API encerra as sessões (o próximo acesso refaz o login no SSO em silêncio). Com réplicas, troque oMapdoSessionServicepor Redis ou tabela. - Cookies com nome da aplicação (
sk_<slug>_sessao,sk_<slug>_pkce…;SSO_COOKIE_PREFIXpara trocar). Cookie não separa por porta: com nome genérico, dois sistemas no mesmo host derrubam a sessão um do outro — e se o outro for o próprio SSO (que usasso_sessioncomSecure), o navegador bloqueia o cookie do app e o login entra em laço. - Tela de login e de "verificando sessão" com a identidade do painel do SSO
(logos e Inter em
public/sso-kit/, CSS Module isolado — não interfere no CSS do app). - Trava de laço: se o login conclui no SSO mas a sessão não chega à API, a tela para após 3 idas e explica as causas prováveis, em vez de girar.
Ponto de extensão: espelhar o usuário localmente
O add não tenta adivinhar o schema do seu banco. SsoModule.register()
aceita um onLogin, chamado logo após o token ser validado e antes de criar a
sessão. Use o id do SSO (claims.sub) como id do usuário local: as FKs do
domínio continuam válidas sem de-para. O moduleRef dá acesso aos providers
do app:
// src/app.module.ts — TypeORM
import { DataSource } from 'typeorm';
SsoModule.register({
onLogin: async (claims, { moduleRef }) => {
const usuarios = moduleRef.get(DataSource, { strict: false }).getRepository(Usuario);
await usuarios.upsert({ id: claims.sub, nome: claims.nome ?? '' }, ['id']);
},
})// src/app.module.ts — Prisma
SsoModule.register({
onLogin: async (claims, { moduleRef }) => {
const prisma = moduleRef.get(PrismaService, { strict: false });
await prisma.usuario.upsert({
where: { id: claims.sub },
update: { nome: claims.nome ?? '' },
create: { id: claims.sub, nome: claims.nome ?? '' },
});
},
})Se onLogin lançar erro, o login falha e o usuário volta para
/login?erro=sso (o motivo fica no log da API).
Por que dois comandos, e por que add não edita tudo sozinho
Verificar é seguro de automatizar por completo — é só leitura. Editar
app.module.ts/main.ts de um projeto que cada dev estruturou à sua
maneira não é: um regex "esperto" que adivinha errado quebra build em
silêncio. Por isso o add só edita automaticamente o que reconhece com alta
confiança (import do módulo, cookie-parser, rawBody) e, para o resto,
deixa a etapa pendente na página — nunca finge ter terminado algo que não fez.
Modelo flat vs scoped — qual escolher
| | flat | scoped |
|---|---|---|
| Permissões | lista única por usuário | uma lista por unidade/entidade |
| Claim do token usada | roles/permissions | access (E13) |
| Header x-unidade-id | não usado | seletor de unidade, validado contra o token |
| Quando usar | a maioria dos sistemas | sistema multi-unidade com permissão variando por unidade (ex.: hospitais) |
Se o sistema no SSO distribui permissões por unidade e o app usa flat, a sessão chega com permissions vazio — qualquer rota com @RequirePermission responde 403. Nesse caso o modelo certo é scoped.
No scoped, a lista de unidades do usuário vem do SSO (é o acesso concedido
no painel), não de uma tabela local — o front escolhe a unidade ativa e a API
confere x-unidade-id contra o token em toda requisição.
Dá para migrar de flat para scoped depois — são módulos independentes,
troque src/sso/ pelo outro modelo e ajuste quem lê session.permissions
(flat) para session.scopes (scoped).
