// como o auth funciona

Uma autoridade decide quem entra e o que pode.

O Auth guarda, em um lugar só, a resposta para duas perguntas: quem é esta pessoa e o que ela pode fazer agora. Esta página percorre o caminho inteiro de uma requisição e, depois, mostra o roteiro para ligar um sistema novo. Role a página — o diagrama acompanha cada passo.

// parte 1

O caminho de uma requisição

Sete passos, do primeiro clique até a revogação.

sign-in ID token ID token CreateSession sid grava sessão cookie HttpOnly ValidateSession contexto lê contexto EvaluateProductAccess muda a autorização revoga sessões Navegadorauth-sdk/browser GCIPemite o ID token BFFservidor da aplicação authdgRPC privado · mTLS PostgreSQLsessões e autorização Auth Adminpainel e MCP BFF sem estado por usuário
Passo 01 · Os atores
  1. Os atores

    Seis peças participam. O navegador roda a aplicação e a SDK @iniciador-de-pagamentos/auth-sdk/browser. O BFF é o servidor da própria aplicação. O authd é o control plane: gRPC privado, alcançável só por mTLS. O GCIP (Firebase) emite o ID token. O PostgreSQL guarda sessões, o registro de aplicações e a autorização. O Auth Admin é o painel dos operadores, com os mesmos comandos expostos por MCP.

  2. Entrar

    O navegador pede ao GCIP um ID token, por magic link, SSO ou SAML. Antes disso o BFF faz a descoberta pública: recebe apenas o domínio do e-mail e a origem, e devolve a configuração de sign-in do tenant. O navegador nunca escolhe o tenant, e PortalAuth recusa qualquer provedor que a descoberta não tenha devolvido.

  3. A troca

    O navegador entrega o ID token ao seu próprio BFF. O BFF chama CreateSession no authd pelo caminho de workload: certificado de cliente mTLS emitido pela Service CA, mais um ID token do Cloud Run. O application_id e a audiência vêm do caller registrado, não do pedido, e a tupla aplicação / audiência / origem precisa existir ativa no registro. O authd devolve um sid.

  4. O sid não chega ao JavaScript

    Nesta API o sid é o próprio token bearer opaco da sessão: quem o tem fala pela sessão, mas só por este BFF, porque o authd exige a mesma aplicação e audiência do chamador. O BFF grava o sid no cookie __Host-auth_example, HttpOnly, Secure, SameSite=Lax, Path=/, sem Domain, com a expiração da sessão. O JavaScript da página nunca o lê, e o BFF não guarda nada por usuário. A alternativa é um store server-side cifrado, com só um handle opaco no cookie.

  5. Cada requisição

    A cada requisição o BFF chama Validator.validate(sid, audience). O validador mantém um cache em processo com chave SHA-256 do sid mais a audiência, e manda o known_context_etag quando já tem contexto guardado; o authd responde ContextUnchanged ou o contexto inteiro. validateWithGrace reaproveita um contexto validado há no máximo 30 segundos, só diante de UNAVAILABLE ou DEADLINE_EXCEEDED, e só em leituras comuns de produto.

    O contexto traz também o e-mail normalizado da pessoa, capturado quando o authd emitiu a sessão: uma troca de e-mail só aparece na sessão seguinte. É dado pessoal, então nunca vai para log.

  6. Autorização

    Autorizar é perguntar por uma ação em uma instância de produto: EvaluateProductAccess recebe o sid, o id da instância, o etag do contexto e a ação. O acesso é sempre por pessoa, através de uma concessão explícita (ProductAssignment), com escopo de organização, cliente ou instância. A negação é uniforme: produto de outra aplicação é indistinguível de instância inexistente.

    Os três escopos gravam. ORGANIZATION vale para as instâncias daquele produto na organização, inclusive as criadas depois, porque a concessão guarda a organização e não a instância. CUSTOMER vale para as instâncias daquele cliente. PRODUCT_INSTANCE vale para exatamente uma instância, tenha ela cliente ou não. Instância presa a um cliente ainda exige uma assinatura vigente do produto. Duas concessões que alcancem a mesma instância para a mesma ação negam: a avaliação lê duas linhas e não escolhe entre elas. E o acesso continua sendo por pessoa.

  7. Revogação e limites

    Toda mudança de autorização sobe a versão de autorização da identidade e revoga as sessões afetadas na mesma transação que grava a mudança. Na emissão, o authd conta as sessões ativas da tupla identidade + aplicação + audiência e revoga as mais antigas até sobrarem dez. E a sessão dura exatamente uma hora: o banco recusa qualquer linha cujo expires_at não seja created_at + interval '1 hour'.

// por que assim

O jeito ingênuo e o nosso

// jeito ingênuo

ID token como bearer em cada API

Cada API verifica a assinatura do token em processo, contra o JWKS do Google em cache. É rápido e não depende de mais ninguém.

O custo aparece na hora de tirar acesso: o token já emitido continua válido por até cerca de uma hora, a não ser que o consumidor ligue CHECK_REVOKED e pague uma consulta ao GCIP a cada verificação. E cada API vira uma autoridade — a decisão mora espalhada.

// nosso jeito

Sessão central no authd

A sessão vive no authd. A mudança de autorização revoga as sessões afetadas na mesma transação que grava a mudança, então a validação seguinte já falha.

Existe uma autoridade só, e o preço é uma chamada por requisição — amortizada pelo cache de contexto do validador e pela resposta ContextUnchanged, que não reenvia o contexto quando o etag continua o mesmo.

// avanço rápido

Em dez segundos

  1. O navegador descobre a configuração de sign-in pelo domínio do e-mail.
  2. O GCIP devolve um ID token.
  3. O BFF troca o ID token por um sid em CreateSession, sobre mTLS.
  4. O navegador leva o sid só num cookie __Host- HttpOnly; o JavaScript nunca o lê.
  5. Cada requisição passa por ValidateSession, com cache por SHA-256 do sid.
  6. Cada ação passa por EvaluateProductAccess, contra uma concessão explícita.
  7. Qualquer mudança de autorização revoga as sessões na mesma transação.

// parte 2

Ligar um sistema novo

Quatro tarefas, nesta ordem. As três primeiras são de operador; a última é código.

Isto é o que main implementa e o que o portal publicado já faz de ponta a ponta: scripts/auth-example-prod-e2e.sh só conclui com 201 na troca de sessão, 200 na validação e 401 depois da revogação. Levar o control plane para outro consumidor continua sendo execução de operador: o bloqueio está aberto em #36 e o roteiro é docs/authd-prod-bring-up.md.

// tarefa 01

Registrar a aplicação

No Auth Admin, em Aplicações internas, crie a aplicação e depois acrescente suas audiências e origens. Toda escrita é revisão e confirmação: a revisão devolve o request_id, e a confirmação repete o mesmo valor, de modo que uma chamada repetida replica a criação em vez de criar uma segunda aplicação.

  • O id da aplicação é gerado pelo painel; o tipo é imutável depois de criado.
  • Uma aplicação admite no máximo 32 audiências e 64 origens.
  • Tudo isso também existe por MCP, uma ferramenta por rota do painel (applications_create_preview, applications_create_confirm, applications_origin_add, …), autenticado por chave de API delegada.
  • O authctl não existe mais; o painel e o MCP são os caminhos.

// tarefa 02

Catálogo do produto e concessões

O catálogo fica abaixo da aplicação, porque é a aplicação que delimita cada listagem.

  • Produto, com suas ações, papéis e funcionalidades, sob a aplicação.
  • Instâncias de produto, sob a organização do tenant.
  • Concessões de produto: uma por pessoa, apontando um papel do produto e um escopo.

Uma concessão é revogada com motivo registrado, e a revogação também é revisão mais confirmação. Reativar uma concessão revogada é a única volta — uma linha revogada impede uma nova no mesmo lugar.

// tarefa 03

Identidade de workload

O BFF só fala com o authd se o transporte provar quem ele é. São três peças, todas declaradas em Terraform:

  • Uma service account dedicada — não reaproveite a de outro serviço, porque o registro liga SAN a aplicação, audiência e principal IAM, e cada workload lê somente o próprio segredo.
  • Um certificado folha da Service CA no segredo <nome>-cert. Só o cert-rotator escreve versões: folhas de 12 horas, job a cada 4 horas. Antes da primeira rotação o serviço não conecta.
  • Uma linha em workload_registry_entries ligando o DNS SAN a {application_id, audience} e ao principal IAM, mais roles/run.invoker sobre o authd e roles/iam.serviceAccountTokenCreator sobre si mesma.

Se o serviço roda em outro projeto GCP, o caminho é o Private Service Connect: uma entrada em authd_psc_consumers (projeto, service account, aplicação, audiência, DNS SAN) do nosso lado, e do lado do consumidor o endpoint PSC, o DNS que aponta para ele e o roles/iam.serviceAccountOpenIdTokenCreator da service account sobre ela mesma. O procedimento completo está em docs/authd-prod-bring-up.md.

// tarefa 04

Código do BFF

Em TypeScript, com @iniciador-de-pagamentos/auth-sdk/server. Uma conexão e um Validator por processo: o validador é quem guarda o cache de contexto.

O BFF abaixo não guarda estado: o sid vai no cookie __Host-auth_example, montado e lido pelos auxiliares do SDK, e qualquer instância atende qualquer sessão. Quem preferir não ter o sid no navegador usa um store server-side cifrado com só um handle opaco no cookie; os requisitos estão no README do SDK.

Conexão e validador

import { randomUUID } from "node:crypto";
import {
  clearSessionCookie,
  connectAuthdWorkload,
  readSessionCookie,
  sessionCookie,
  ValidationError,
  Validator,
  type AuthContext,
} from "@iniciador-de-pagamentos/auth-sdk/server";

const authd = await connectAuthdWorkload({
  target: "authd.example.internal:443",
  serverCAPath: "/var/run/secrets/authd/server-ca.pem",
  clientBundlePath: "/var/run/secrets/authd/client-bundle.pem",
  cloudRunAudience: "https://authd.example.internal",
  targetPrincipal: "meu-bff@meu-projeto.iam.gserviceaccount.com",
}, { allowInitialUnavailable: true });

const validator = new Validator(authd.sessions);
const audience = "payments-api";

Sign-in: trocar o ID token pelo sid

// Devolve o valor de Set-Cookie. Sem expiração futura válida, revoga o sid e falha.
// Um UNAUTHENTICATED da revocação significa que o sid já não vale e é ignorado.
export async function signIn(firebaseIdToken: string, origin: string, discoveryToken: string): Promise<string> {
  const { sid, session } = await authd.sessions.createSession({ firebaseIdToken, origin, discoveryToken });
  try {
    return sessionCookie(sid, session?.expiresAt ?? new Date(Number.NaN));
  } catch (error) {
    try {
      await authd.sessions.revokeCurrentSession({ sid, requestId: randomUUID() });
    } catch (revokeError) {
      if ((revokeError as { code?: number }).code !== 16) throw revokeError;
    }
    throw error;
  }
}

Validação por requisição

export async function contextFor(cookieHeader: string | null): Promise<AuthContext | 401 | 403 | 503> {
  const sid = readSessionCookie(cookieHeader);
  if (sid === undefined) return 401;
  try {
    return await validator.validate(sid, audience);
  } catch (error) {
    if (!(error instanceof ValidationError)) throw error;
    if (error.code === 3 || error.code === 16) return 401;
    if (error.code === 7) return 403;
    return 503;
  }
}

E-mail de quem está conectado

// Capturado na emissão da sessão; uma troca de e-mail só aparece na sessão
// seguinte. Dado pessoal: nunca registre em log.
export function signedInEmail(context: AuthContext): string | undefined {
  return context.toProto().normalizedEmail || undefined;
}

Autorizar uma ação

export async function mayRead(context: AuthContext, sid: string, productInstanceId: string): Promise<boolean> {
  const contextEtag = context.toProto().session?.contextEtag;
  if (contextEtag === undefined) return false;
  try {
    await authd.authorization.evaluateProductAccess({ sessionSid: sid, productInstanceId, contextEtag, action: "read" });
    return true;
  } catch (error) {
    // 7 = PERMISSION_DENIED: o authd negou esta ação. Os demais códigos — requisição
    // inválida, pré-condição, falha de infraestrutura — sobem como erro para o BFF,
    // nunca como "sem acesso".
    if (typeof error === "object" && error !== null && "code" in error && error.code === 7) return false;
    throw error;
  }
}

Logout

// Devolve o Set-Cookie que apaga o cookie só depois de o authd confirmar a revogação.
// Se a chamada falhar, o erro sobe e o navegador mantém o cookie para tentar de novo.
export async function signOut(cookieHeader: string | null): Promise<string> {
  const sid = readSessionCookie(cookieHeader);
  if (sid !== undefined) await authd.sessions.revokeCurrentSession({ sid, requestId: randomUUID() });
  return clearSessionCookie();
}

Em Go, o pacote sdk/ deste repositório cobre três partes do caminho: authsdk.Validator (validação central com cache), authsdk.PortalSSOAdapter (descoberta e troca do ID token) e os auxiliares de cookie authsdk.SessionCookie, ReadSessionCookie e ClearSessionCookie. Ele não monta o transporte — quem disca é a aplicação — e não traz um auxiliar para EvaluateProductAccess.