Pular para o conteúdo

Node.js

Proteja APIs Node.js, chame APIs de workers com client credentials e receba webhooks com os pacotes Node.js do Authio.

O SDK Node.js do Authio são quatro pacotes no npm, na organização @hyper.authio, versão 0.1.1, para Node.js 20.19 ou mais recente, como módulos ES e CommonJS. Eles leem todos os endpoints do documento de discovery do seu realm e já entendem as claims do Authio: escopos, os papéis e permissões de cada claim *_resource_access, grupos, organizações e a força do login.

Escolha um pacote

PacoteUse para
@hyper.authio/expressUma API em Express 4 ou 5: validação do bearer, guards de escopos, papéis, permissões, grupos, claims, organização e força do login, e uma rota de webhook assinada.
@hyper.authio/serverUma API em qualquer outro framework (Fastify, Hono, Nest, http puro): a mesma validação e as mesmas regras como funções, mais webhooks, repasse do token e on-behalf-of.
@hyper.authio/clientWorkers, CLIs e chamadas ao Authio de qualquer lugar, inclusive runtimes de edge: discovery, todos os grants de token, introspecção, revogação, UserInfo, um fetch com token de client credentials em cache e a verificação de webhooks.
@hyper.authio/testingTestes: um realm em memória no loopback que emite tokens, responde à introspecção e assina entregas de webhook.
TerminalBash
npm install @hyper.authio/express        # uma API em Express (traz server e client)
npm install @hyper.authio/server         # uma API em outro framework
npm install @hyper.authio/client         # um worker, uma CLI ou chamadas ao Authio
npm install -D @hyper.authio/testing     # testes
Apps Next.js entram com o @hyper.authio/provider-nextauth, e servidores MCP usam o @hyper.authio/mcp. Os dois estão na mesma organização.

Aponte para o seu realm

Todo pacote recebe o emissor do realm, {authority}/realms/{realm}, como issuer ou como authority mais realm. O documento de discovery é lido a partir dele, guardado em cache por uma hora e recusado se trouxer outro emissor; um endereço http é recusado, a não ser em loopback. Um client recebe também o clientId e a forma como se autentica: client_secret_basic (o padrão), client_secret_post, private_key_jwt ou none para um client público.

.envBash
AUTHIO_ISSUER=https://authio.api.hyperio.tech/realms/acme
AUTHIO_CLIENT_ID=orders-worker
AUTHIO_CLIENT_SECRET=YOUR_CLIENT_SECRET

Proteja uma API

Cadastre sua API como servidor de recurso, por exemplo https://api.example.com com os escopos orders:read e orders:write, e aceite só o identificador dela como audiência. Os clients pedem resource=https://api.example.com e os tokens deles o trazem em aud.

server.tsTypeScript
import express from 'express'
import { authioBearer, requirePermission, requireScope } from '@hyper.authio/express'

const app = express()

app.use(
  '/api',
  authioBearer({
    issuer: process.env.AUTHIO_ISSUER!,
    audiences: 'https://api.example.com',
  }),
)

app.get('/api/me', (req, res) => {
  res.json(req.authio!.user)
})

app.get('/api/orders', requireScope('orders:read'), requirePermission('orders', 'read'), (req, res) => {
  res.json([])
})

app.listen(3001)
VerificaçãoO que o authioBearer exige
Emissor e audiênciaExatamente o emissor do realm e uma das audiences. Um token endereçado a outro lugar é recusado com um error_description que diz qual audiência a API aceita.
Assinatura e validadeUma chave do JWKS do realm com algoritmo assimétrico (RS, PS, ES), lido de novo quando um token cita um kid desconhecido; exp e nbf com 30 segundos de clockSkew.
Tipo do tokenA claim typ tem que ser Bearer, então ID tokens e refresh tokens são recusados.
Recusa401 com WWW-Authenticate: Bearer quando não há token, e Bearer error="invalid_token" com o motivo quando ele não é válido. problemDetails: true acrescenta um corpo RFC 9457.

Exija escopos, papéis, permissões e grupos

Todo guard é um middleware do Express e também uma regra, então requireAuthio, allOf e anyOf os combinam.

routes.tsTypeScript
import {
  anyOf,
  requireAuthenticationLevel,
  requireAuthio,
  requireClaim,
  requireGroup,
  requireOrganization,
  requirePermission,
  requireRole,
  requireScope,
} from '@hyper.authio/express'

// Qualquer um dos escopos; passe 'all' para exigir todos.
app.get('/api/orders/export', requireScope(['orders:read', 'orders:export'], 'all'), exportOrders)

// Um papel atribuído à pessoa ou a um grupo dela, não a todo mundo do realm.
app.delete('/api/orders/:id', requireRole('orders-admin', { sources: ['user'] }), deleteOrder)

// Um grupo (claim mapper de grupo) e uma claim (qualquer mapper, ou a própria claim da pessoa).
app.get('/api/finance', requireGroup('finance'), requireClaim('department', 'finance'), report)

// Uma permissão e, além dela, um login forte ou a organização acme.
app.post(
  '/api/payouts',
  requireAuthio(requirePermission('payouts', 'create'), anyOf(requireAuthenticationLevel(2), requireOrganization('acme'))),
  payout,
)
GuardPassa quando
requireScopeAlgum (ou, com 'all', todo) escopo está em scope.
requireRoleQuem chama tem o papel, sem diferenciar maiúsculas. Com sources, só contam os papéis dessas claims: user (a pessoa e os grupos dela), realm, client. Exige o escopo roles.
requirePermissionUm dos papéis de quem chama concede o método no recurso, como orders / write, opcionalmente só das sources indicadas.
requireGroupUma claim de grupos (groups por padrão) traz o grupo.
requireClaimA claim existe, ou traz um dos valores que você indicar.
requireOrganizationorg_id existe, ou bate com o alias ou o ID que você indicar.
requireAuthenticationLevel / requireAuthenticationMethodacr alcança o nível, ou amr traz o método (mfa, otp, swk…).
requireClientazp é um dos clients — útil para admitir um único client de máquina.

Um escopo que falta responde 403 com WWW-Authenticate: Bearer error="insufficient_scope" e os escopos necessários; um login sem a força exigida responde insufficient_user_authentication, para o client levar a pessoa a um login mais forte. Qualquer outra recusa é um 403 simples.

Leia quem chama

req.authio.user é um AuthioUser, lido do mesmo jeito tanto quando o token foi validado localmente quanto por introspecção. Ele traz todas as formas de permissão que o Authio emite, e as claims brutas para o resto. O próprio access token fica fora do JSON e dos logs.

FormaVem deLeia com
Escoposscope, concedido ao clientuser.scopes, hasScope()
Papéis e permissõesuser_resource_access (a pessoa, direto ou por um grupo), realm_resource_access, client_resource_access, e consumer_resource_access e secret_resource_access nas credenciais de máquina, cada uma { papel: { recurso: [método] } }user.roles, user.permissions, hasRole(), hasPermission(), user.resourceAccess por fonte
Grupos e nomes de papel soltosClaim mappers de grupo ou de papel, com o nome que você deu (groups e role por padrão; mude com groupClaims e roleClaims)user.groups, hasGroup(), user.roles
Qualquer outra claimClaim mappers (constante, propriedade do usuário) e as claims da própria pessoa, no access tokenuser.claims, claimValues(), hasClaim()
Organização e clientorg_id, org_alias, azpuser.organizationId, user.organizationAlias, user.clientId
Força do loginacr, amr, auth_time, idpuser.authenticationLevel, user.authenticationMethods, user.authenticatedAt
Delegaçãoact, delegation_iduser.actor, user.isDelegated, user.delegationId
Sempre que puder, confira permissões em vez de nomes de papel: um papel pode ser renomeado no console, e o que o código depende é da permissão que ele concede.

Outros frameworks

O @hyper.authio/server tem a mesma validação e as mesmas regras sem o Express. Uma recusa é um AuthioBearerError (401) ou um AuthioForbiddenError (403), cada um com o valor de WWW-Authenticate a enviar.

auth.tsTypeScript
import { AuthioForbiddenError, authorize, createAuthioVerifier, requirePermission } from '@hyper.authio/server'

const verifier = createAuthioVerifier({
  issuer: process.env.AUTHIO_ISSUER!,
  audiences: 'https://api.example.com',
})

export async function caller(authorization: string | undefined) {
  // Lança AuthioBearerError: responda 401 com error.wwwAuthenticate.
  const user = await verifier.verifyAuthorizationHeader(authorization)
  const refusal = authorize(user, requirePermission('orders', 'read'))

  // AuthioForbiddenError: responda 403, com error.wwwAuthenticate quando houver.
  if (refusal) throw new AuthioForbiddenError(refusal)

  return user
}

Perceba revogações antes de o token expirar

Um token validado localmente continua válido até expirar, mesmo depois que a sessão terminou ou o usuário foi suspenso. introspection: 'activeCheck' também pergunta ao Authio se o token ainda está ativo, com cache de 30 segundos por token; 'introspectionOnly' valida só por introspecção, para tokens de referência ou criptografados. Os dois precisam do client confidencial da API, aquele que o servidor de recurso indica como seu client de introspecção: o Authio só informa como ativo um token endereçado a um servidor de recurso para esse client.

server.tsTypeScript
app.use(
  '/api',
  authioBearer({
    issuer: process.env.AUTHIO_ISSUER!,
    audiences: 'https://api.example.com',
    introspection: 'activeCheck',
    client: { clientId: process.env.AUTHIO_CLIENT_ID!, clientSecret: process.env.AUTHIO_CLIENT_SECRET },
  }),
)

Chame uma API de um worker

clientCredentialsFetch é um fetch que envia um token de client credentials para os escopos e o recurso que você indicar. O token fica em cache e é renovado um minuto antes de expirar, com um único pedido para chamadas simultâneas; num 401, ele é descartado e a chamada é repetida uma vez.

worker.tsTypeScript
import { createAuthioClient } from '@hyper.authio/client'

const authio = createAuthioClient({
  issuer: process.env.AUTHIO_ISSUER!,
  clientId: process.env.AUTHIO_CLIENT_ID!,
  clientSecret: process.env.AUTHIO_CLIENT_SECRET,
})

const ordersFetch = authio.clientCredentialsFetch({
  scopes: 'orders:read',
  resource: 'https://api.example.com',
})

const response = await ordersFetch('https://api.example.com/api/orders')

Para o segredo não trafegar, cadastre a chave pública do client e use clientAuthentication: 'private_key_jwt' com clientPrivateKey (PEM ou JWK) ou clientPrivateKeyPath, e clientKeyId: o client assina uma asserção de vida curta a cada pedido. O mesmo client cobre os outros grants: authio.tokens.authorizationCode, refresh (guarde o refresh token rotacionado que ele devolve), exchange para on-behalf-of, deviceAuthorization e pollDevice para o device flow, além de introspect, revoke e userInfo.

Chame outra API em nome de quem chamou

billing.tsTypeScript
import { createAuthioClient } from '@hyper.authio/client'
import { createOnBehalfOf } from '@hyper.authio/server'

const billing = createOnBehalfOf({
  client: createAuthioClient({ issuer, clientId, clientSecret }),
  resource: 'https://billing.example.com',
  scopes: 'invoices:read',
})

app.get('/api/invoices', async (req, res) => {
  const response = await billing.fetch(req.authio!.user)('https://billing.example.com/invoices')
  res.status(response.status).json(await response.json())
})

O token de quem chamou é trocado (RFC 8693) uma vez por token, recurso e escopos, e guardado em cache; o novo token mantém a pessoa em sub e cita a sua API em act. Para repassar o token sem mudança a uma API que aceita a mesma audiência, use createForwardingFetch(user).

Receba webhooks

server.tsTypeScript
import { authioWebhooks } from '@hyper.authio/express'

// Antes do express.json(): a assinatura é conferida sobre o corpo bruto.
app.post(
  '/webhooks/authio',
  authioWebhooks({ secret: process.env.AUTHIO_WEBHOOK_SECRET })
    .on('user_deleted', async (event) => forget(event.data.id))
    .on('*', (event) => console.log(event.type)),
)

A rota confere o X-Authio-Signature contra cada segredo configurado (passe secrets enquanto troca um), recusa uma assinatura com mais de cinco minutos e reconhece uma reentrega do mesmo X-Authio-Event-Id sem rodar os handlers de novo. Ela responde 200 tratado ou repetido, 400 ilegível, 401 assinatura inválida, 413 grande demais e 500 quando um handler lança erro, o que deixa o Authio entregar de novo. Os eventos estão em Webhooks.

Teste sem um realm

orders.test.tsTypeScript
import { FakeAuthio } from '@hyper.authio/testing'

const authio = await FakeAuthio.start({ realm: 'acme' })

const token = await authio.issueAccessToken({
  sub: 'u-1',
  audience: 'https://api.example.com',
  scope: 'orders:read',
  claims: { user_resource_access: { clerk: { orders: ['read'] } }, groups: ['finance'] },
})

// Aponte a API para authio.issuer, chame-a com o token e, no fim:
await authio.close()

O fake serve discovery, JWKS, todos os grants, introspecção, revogação e UserInfo no loopback, registra cada pedido e pode responder com erros programados. createWebhookDelivery monta uma entrega assinada para a rota de webhook.

Cookies de analytics

Gostaríamos de medir como o console é usado, com o Google Analytics, para enxergar em quais telas as pessoas travam. Isso grava um cookie que reconhece este navegador na sua próxima visita.

Recusar não muda mais nada. Os cookies que mantêm você conectado e lembram o seu idioma e o seu tema são necessários para o console funcionar, e ficam de qualquer forma. Ler a Política de Privacidade