Exemplo de autenticação Node

Esta página conduz a integração em cinco etapas, uma de cada vez. Comece digitando seu e-mail corporativo para descobrir a configuração SAML. O BFF recebe somente o domínio normalizado; o e-mail completo nunca sai do navegador.

Pré-requisitos: instalar o pacote e preparar os clientes

Preparar PortalAuth

Browser: configure o registry e instale o pacote privado antes de inicializar o cliente.

Arquivo · .npmrc
@iniciador-de-pagamentos:registry=https://npm.pkg.github.com
Terminal · npm
npm install @iniciador-de-pagamentos/auth-sdk@0.2.0
Browser · TypeScript
import { PortalAuth } from "@iniciador-de-pagamentos/auth-sdk/browser";

const portalBaseUrl = "https://auth-example.sec.inic.dev";
const discovery = await fetch(new URL("/auth/discover", portalBaseUrl), {
  method: "POST",
  headers: { "content-type": "application/json" },
  credentials: "same-origin",
  body: JSON.stringify({ email_domain: "example.com", origin: portalBaseUrl }),
}).then(async (response) => {
  if (!response.ok) throw new Error("Sign-in configuration unavailable");
  return response.json();
});

const auth = PortalAuth.fromPublicSignInConfiguration(discovery.configuration);

Preparar cliente authd

BFF: use o entrypoint de servidor. O token vem de ADC por padrão; injete um TokenSource quando necessário.

Servidor · TypeScript / Go
import { readFile } from "node:fs/promises";
import { connectAuthd } from "@iniciador-de-pagamentos/auth-sdk/server";

const [serverCA, clientCertificate, clientPrivateKey] = await Promise.all([
  readFile("/var/run/secrets/authd/server-ca.pem"),
  readFile("/var/run/secrets/authd/client-cert.pem"),
  readFile("/var/run/secrets/authd/client-key.pem"),
]);
const authd = await connectAuthd({
  target, serverCA, clientCertificate, clientPrivateKey,
  cloudRunAudience, targetPrincipal,
});

const context = await authd.sessions.validateSession({ sid, audience });
  1. 1. Descoberta
  2. 2. SAML
  3. 3. JWT
  4. 4. Sessão
  5. 5. Claims

1. Descoberta

concluído ✓

O que acontece: o navegador envia só o domínio do e-mail e o BFF chama GetPublicSignInConfiguration para devolver a configuração SAML do tenant.

O que esperar: a lista de provedores SAML do tenant.

Ver código
Browser · TypeScript
const response = await fetch("/auth/discover", {
  method: "POST",
  headers: { "content-type": "application/json" },
  credentials: "same-origin",
  body: JSON.stringify({ email_domain: "example.com", origin: location.origin }),
});
const { configuration } = await response.json();
const auth = PortalAuth.fromPublicSignInConfiguration(configuration);

2. Autenticação SAML no GCIP

concluído ✓

O que acontece: o login vai por redirect contra o GCIP, nunca por popup.

O que esperar: uma identidade Firebase autenticada no tenant.

Aguardando descoberta…

Ver código
Browser · TypeScript
await auth.signInWithRedirect(provider.value);

// No retorno do provedor, já na carga da página:
const result = await auth.getRedirectResult();
const unsubscribe = auth.onChange((claims, user) => render(user));

3. Credencial inicial: JWT bearer do GCIP

concluído ✓

O que acontece: o BFF valida o ID token e mostra as entradas de access que ele carrega.

O que esperar: um painel com a identidade e os escopos, presets e grants efetivos do token.

As claims são um snapshot do momento de emissão. Atualize o token para receber claims reconciliados depois.

Ainda não validado.
Ver código
Browser · TypeScript
await auth.refresh();                       // claims reconciliados
const firebaseIDToken = await auth.token();

await fetch("/api/jwt/me", {
  headers: { Authorization: `Bearer ${firebaseIDToken}` },
  credentials: "same-origin",
});

4. Credencial posterior: token bearer de sessão do authd

concluído ✓

O que acontece: o ID token é trocado por uma sessão authd (CreateSession), que depois é validada e revogada.

O que esperar: cookie __Host-auth_example HttpOnly no navegador, com o sid, e sessão validada no servidor a cada requisição.

Nesta arquitetura, o campo chamado sid pelo protocolo é o token secreto. Ele viaja só nesse cookie HttpOnly, que o JavaScript da página não lê, e expira junto com a sessão. Revogar apaga o cookie só depois de o authd confirmar.

Nenhuma sessão criada.
Ver código
Servidor · TypeScript
const created = await authd.sessions.createSession({ firebaseIdToken, origin, discoveryToken });
const context = await authd.sessions.validateSession({ sid: created.sid, audience });
await authd.sessions.revokeCurrentSession({ sid: created.sid, requestId });

5. Claims de autorização e checagem de permissão

concluído ✓

O que acontece: o navegador decodifica access por escopo e o BFF autoriza cada rota com a tabela da aplicação e o recurso resolvido.

O que esperar: escopos, presets e grants efetivos legíveis, além de uma montagem que separa rotas públicas das rotas autenticadas.

O JWT carrega entradas [kind, scope_id, preset_id, grants]. O middleware nunca usa IDs crus da URL como verdade: a aplicação resolve o tenant, a organização ou o customer real antes de chamar can.

Claims ainda não lidos.

Use modulesOfAnyScope apenas para descoberta. Para autorizar uma rota, passe o escopo resolvido e o namespace do tenant a hasModule, hasVariant ou modulesOf.

Autorização · TypeScript (workspace)
import { can, hasModule, hasVariant, modulesOfAnyScope, type AccessEntry, type Resolved } from "@iniciador-de-pagamentos/auth-sdk/browser";

const access: AccessEntry[] = [
  ["t", "", "preset-viewer", [9010004]],
  ["o", "org-A", "preset-admin", [9010006]],
];
const customerX: Resolved = {
  kind: "customer", customerId: "customer-X", organizationId: "org-A",
  tenantId: "tenant-home", tenantNamespace: "tenant-home-ns",
};

can(access, customerX, "tenant-home-ns", 1, 4); // true: inherited read access
hasModule({ access }, "pay_by_bank", customerX, "tenant-home-ns"); // true
hasVariant({ access }, "pay_by_bank", "sdk_headless", customerX, "tenant-home-ns"); // true via wildcard
modulesOfAnyScope({ access }); // discovery only; not an authorization decision
Servidor · Hono (workspace)
// Workspace-only example: @auth/verifier is a monorepo package.
import { Hono } from "hono";
import { applicationEndpointsProvider } from "@iniciador-de-pagamentos/auth-sdk/server";
import { authenticate, createAccessMiddleware, type AuthVariables } from "@auth/verifier";

const endpoints = applicationEndpointsProvider(authorizationClient, applicationId);
const accessMiddleware = createAccessMiddleware({
  endpoints,
  resolve: async (c) => {
    const resource = await resourceStore.findCustomerById(c.req.param("customerId"));
    if (resource === undefined) throw new Error("customer not found");
    return {
      kind: "customer", customerId: resource.id, organizationId: resource.organizationId,
      tenantId: resource.tenantId, tenantNamespace: resource.tenantNamespace,
    };
  },
});
const app = new Hono<{ Variables: AuthVariables }>();

// PUBLIC: do not mount authenticate() or access middleware.
app.get("/health", (c) => c.text("ok"));
// Authenticated with access: authenticate() runs first.
app.use("/customers/*", authenticate());
app.use("/customers/*", accessMiddleware);
Ver código
Browser · TypeScript
const claims = await auth.getClaims();

claims.access?.forEach(([kind, scopeID, presetID, grants]) => {
  console.log({ kind, scopeID, presetID, grants });
});
Avançado: catálogo de módulos

Os códigos MM e as integrações vêm do catálogo do SDK; autorização acontece por access e pela tabela da aplicação.

Carregando catálogo…