// 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.