npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@sbkbs/portal-auth

v1.0.0

Published

Integração do Portal Unificado SBK (Nexus) para produtos Node.js/Fastify: troca do código de autorização e validação do access_token via JWKS, com cache, rotação de chave e erros tipados.

Readme

@sbkbs/portal-auth

Integração padronizada com o Portal Unificado SBK (Nexus) para produtos Node.js/Fastify. Cobre o lado do produto no fluxo Authorization Code:

  • troca do código recebido no callback pelo access_token (POST /oauth/token), com a credencial de sistema;
  • validação do token: busca e cacheia o JWKS público do Nexus, resolve a chave pelo kid, verifica assinatura RS256, issuer, audience e expiração, e devolve claims tipadas.

Toda falha vira um erro tipado com o motivo, para o produto decidir a resposta sem parsear mensagens.

O problema que esta lib resolve

Sem uma biblioteca comum, cada produto que consome o Nexus implementa a troca de código e a validação do token do seu jeito, e é fácil esquecer uma das checagens. @sbkbs/portal-auth empacota o padrão correto (JWKS com cache, chave resolvida pelo kid, jwt.verify com algorithms: ['RS256'], issuer e audience), junto com a troca de código, numa lib reutilizável.

O sistema de destino deve validar o JWT recebido na troca (assinatura, aud e exp) antes de criar a sessão local. trocarCodigo faz as duas coisas numa chamada só, então não há como trocar o código e esquecer de validar.

Uso

Troca de código no callback (uso principal)

import { createPortalAuthClient, TrocaCodigoError, TokenInvalidoError } from '@sbkbs/portal-auth';

const portal = createPortalAuthClient({
  tokenUrl: 'https://<api-do-nexus>/oauth/token',
  jwksUrl: 'https://<api-do-nexus>/.well-known/jwks.json',
  issuer: 'portal-unificado-sbk',
  clientId: 'meu-sistema-id',              // sistemaId no Nexus; também vira a audience esperada
  clientSecret: () => obterClientSecret(), // string, ou função chamada a cada troca (rotação)
});

app.get('/auth/nexus/callback', async (request, reply) => {
  const { code } = request.query as { code?: string };
  try {
    const { claims } = await portal.trocarCodigo(code ?? '');
    // claims.sub, claims.email, claims.nome, claims.tenant_id, claims.permissao:
    // já validadas. Crie aqui a sessão local do produto.
  } catch (err) {
    if (err instanceof TrocaCodigoError || err instanceof TokenInvalidoError) {
      return reply.code(401).send({ erro: 'Acesso via Portal recusado', motivo: err.motivo });
    }
    throw err; // NexusIndisponivelError / JwksIndisponivelError: infraestrutura, normalmente 503
  }
});

Motivos de TrocaCodigoError:

  • codigo_invalido: o Nexus respondeu 400. Código inexistente, expirado (TTL de 30s), já utilizado ou emitido para outro sistema. Também é lançado sem chamar o Nexus quando o callback chega sem code.
  • credencial_invalida: o Nexus respondeu 401. client_id/client_secret recusados, ou sistema desativado no Nexus.
  • resposta_invalida: resposta 2xx sem access_token.

O token devolvido passa pela mesma validação de verifyToken (abaixo); se falhar, o erro é TokenInvalidoError. O client também expõe portal.verifyToken(token), com a mesma configuração.

Validação de token recebido como Bearer

import { createPortalAuthVerifier, TokenInvalidoError } from '@sbkbs/portal-auth';

const verifier = createPortalAuthVerifier({
  jwksUrl: 'https://<api-do-nexus>/.well-known/jwks.json',
  issuer: 'portal-unificado-sbk',
  audience: 'meu-sistema-id', // obrigatório: o sistemaId cadastrado no Nexus para este produto
});

// Em qualquer rota Fastify que receba Authorization: Bearer <access_token>:
try {
  const claims = await verifier.verifyToken(token);
  // claims.sub, claims.email, claims.nome, claims.tenant_id, claims.permissao
} catch (err) {
  if (err instanceof TokenInvalidoError) {
    // err.motivo: 'assinatura_invalida' | 'expirado' | 'issuer_invalido'
    //           | 'audience_invalida' | 'chave_nao_encontrada' | 'token_malformado'
    return reply.code(401).send({ erro: 'Token inválido', motivo: err.motivo });
  }
  throw err; // erro de infraestrutura (ex.: JwksIndisponivelError), não é sobre o token
}

Qual jwksUrl usar

O JWKS e o /oauth/token são servidos pelo backend do Nexus (host da API), não pelo host do painel: apontar para o painel devolve HTML, e a lib responde com JwksIndisponivelError. Os endereços de cada ambiente, inclusive o endereço interno para produtos no mesmo cluster, estão na documentação interna de integração com o Portal Unificado.

Por que audience é obrigatória

O Nexus assina os tokens de todos os sistemas com a mesma chave. O que diferencia um token emitido para Ofícios de um emitido para Subsídios é só a claim aud. Sem checar audience, um usuário com acesso a um sistema conseguiria usar o próprio token em outro. Por isso createPortalAuthVerifier recusa ser criado sem ela (lança TypeError). Se um serviço precisar aceitar tokens de mais de um sistema, passe uma lista: audience: ['sistema-a', 'sistema-b'].

Cache e rotação de chave

O JWKS é buscado uma vez e cacheado em memória por 10 minutos ajustável via cacheTtlMs. Quando chega um token com kid que não está no cache (o caso típico de rotação de chave no Nexus), a lib busca o JWKS de novo antes de rejeitar. Para que tokens com kid inventado não virem uma busca por request, essa busca forçada acontece no máximo uma vez a cada 30 segundos (intervaloMinimoRefreshMs). Buscas simultâneas compartilham a mesma requisição.

Plugin Fastify (opcional, secundário)

Para quem prefere não chamar verifyToken manualmente em cada rota, há um plugin fino que decora request.portalAuthClaims:

import { registerPortalAuth } from '@sbkbs/portal-auth';

await app.register(registerPortalAuth, {
  jwksUrl: 'https://<api-do-nexus>/.well-known/jwks.json',
  issuer: 'portal-unificado-sbk',
  audience: 'meu-sistema-id',
});

Isso adiciona um preHandler que vale para todas as rotas do escopo onde o plugin foi registrado (no exemplo acima, a aplicação inteira) e exige Authorization: Bearer <token>:

  • token ausente ou inválido: 401, com motivo no corpo quando o token foi lido;
  • JWKS do Nexus inacessível: 503 (não é culpa do token, e um 401 faria o cliente achar que precisa logar de novo).

Para liberar uma rota sem token, como um healthcheck, marque-a com config: { portalAuth: false }:

app.get('/health', { config: { portalAuth: false } }, async () => ({ ok: true }));

Nessa rota request.portalAuthClaims fica null. Para tratamento de erro customizado, ou qualquer necessidade fora desse caminho, use createPortalAuthVerifier diretamente.

Erros

  • TokenInvalidoError: o token foi lido, mas rejeitado. Tem um campo motivo discriminado (token_malformado, chave_nao_encontrada, assinatura_invalida, expirado, issuer_invalido, audience_invalida) para o chamador decidir a resposta/log sem parsear a mensagem.
  • TrocaCodigoError: a troca do código foi recusada. Campo motivo (codigo_invalido, credencial_invalida, resposta_invalida), ver acima.
  • JwksIndisponivelError: erro de infraestrutura, porque o endpoint JWKS do Nexus não respondeu (rede, timeout, status não-2xx, corpo inesperado). Não diz nada sobre a validade do token; normalmente deveria virar um 503, não um 401.
  • NexusIndisponivelError: erro de infraestrutura na troca, porque o /oauth/token não respondeu (rede, timeout, 5xx). Também deveria virar 503.

Desenvolvimento

npm install
npm run build   # tsc -> dist/
npm test        # vitest run
npm run typecheck

npm publish roda typecheck, testes e build antes (prepublishOnly).

Os testes mockam o fetch do JWKS e do /oauth/token (via a opção fetchImpl) e assinam tokens de teste com pares de chave RSA gerados na hora. Nenhuma chamada de rede real é feita.

Publicação

Publicado no npm público, no escopo @sbkbs (publishConfig.access: public). O pacote não contém segredos: a segurança depende das chaves de assinatura do Nexus e do client_secret de cada sistema, que ficam fora da lib.

npm publish   # roda typecheck, testes e build antes (prepublishOnly)