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
| Pacote | Use para |
|---|---|
| @hyper.authio/express | Uma 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/server | Uma 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/client | Workers, 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/testing | Testes: um realm em memória no loopback que emite tokens, responde à introspecção e assina entregas de webhook. |
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 # testesAponte 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.
AUTHIO_ISSUER=https://authio.api.hyperio.tech/realms/acme
AUTHIO_CLIENT_ID=orders-worker
AUTHIO_CLIENT_SECRET=YOUR_CLIENT_SECRETProteja 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.
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ção | O que o authioBearer exige |
|---|---|
| Emissor e audiência | Exatamente 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 validade | Uma 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 token | A claim typ tem que ser Bearer, então ID tokens e refresh tokens são recusados. |
| Recusa | 401 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.
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,
)| Guard | Passa quando |
|---|---|
requireScope | Algum (ou, com 'all', todo) escopo está em scope. |
requireRole | Quem 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. |
requirePermission | Um dos papéis de quem chama concede o método no recurso, como orders / write, opcionalmente só das sources indicadas. |
requireGroup | Uma claim de grupos (groups por padrão) traz o grupo. |
requireClaim | A claim existe, ou traz um dos valores que você indicar. |
requireOrganization | org_id existe, ou bate com o alias ou o ID que você indicar. |
requireAuthenticationLevel / requireAuthenticationMethod | acr alcança o nível, ou amr traz o método (mfa, otp, swk…). |
requireClient | azp é 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.
| Forma | Vem de | Leia com |
|---|---|---|
| Escopos | scope, concedido ao client | user.scopes, hasScope() |
| Papéis e permissões | user_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 soltos | Claim 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 claim | Claim mappers (constante, propriedade do usuário) e as claims da própria pessoa, no access token | user.claims, claimValues(), hasClaim() |
| Organização e client | org_id, org_alias, azp | user.organizationId, user.organizationAlias, user.clientId |
| Força do login | acr, amr, auth_time, idp | user.authenticationLevel, user.authenticationMethods, user.authenticatedAt |
| Delegação | act, delegation_id | user.actor, user.isDelegated, user.delegationId |
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.
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.
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.
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
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
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
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.