← All posts

Um BFF para muitos inquilinos, e a única requisição que chega sem cookie

Authagonal·July 28, 2026

Ensinaram as aplicações de página única a guardar os seus próprios tokens. A aplicação executa o fluxo OAuth no navegador, recebe de volta um token de acesso e normalmente um token de atualização, guarda-os em algum lugar no JavaScript e anexa-os a cada chamada de API. É bem documentado, é o que a maioria dos tutoriais mostra, e coloca a sua credencial mais duradoura no único lugar que você não consegue defender: um ambiente de execução que executa alegremente qualquer coisa que consiga chegar à página. Uma injeção bem-sucedida, uma dependência comprometida em algum ponto do seu build, e ninguém precisa mais aplicar phishing em ninguém. Leem o token e vão embora, e ele continua funcionando até expirar.

O padrão backend-for-frontend coloca a credencial fora de alcance. O navegador conversa com um pequeno servidor que pertence à sua aplicação, e esse servidor é o cliente confidencial OAuth. Ele executa a troca de código, guarda os tokens de acesso e de atualização, e não entrega ao navegador nada além de um cookie opaco. Esta é uma descrição de como construímos o nosso, e da única parte que se revelou genuinamente interessante: fazê-lo servir mais de um inquilino.

Com o que o navegador fica no fim

Um cookie chamado __Host-agbff, marcado como HttpOnly, SameSite=Lax, com escopo em todo o caminho e sem expiração, de modo que morre com a sessão do navegador. Seu valor são 256 bits de aleatoriedade e nada mais. Não é um token, não se decodifica em nada, e roubá-lo do fio não é algo que você consiga fazer a um cookie __Host- sobre TLS.

Tudo o que é real fica no lado do servidor, num registro de sessão dentro de um cache distribuído: o token de acesso, o token de atualização, o token de identidade, quando o token de acesso expira e a qual inquilino a sessão pertence. O login em si é um fluxo de código de autorização comum com PKCE, executado por um cliente confidencial que se autentica no endpoint de tokens com o seu segredo. O token de identidade é validado como deve ser na volta: emissor, audiência, assinatura contra as chaves publicadas, tempo de vida, e depois uma comparação em tempo constante do nonce com o valor guardado antes do redirecionamento. Só depois de tudo isso é que existe uma sessão e um cookie é definido.

Um cabeçalho cuja existência é verificada, e nada mais

O navegador chama o BFF para obter as suas próprias informações de usuário e para alcançar a API intermediada, e essas chamadas carregam um cabeçalho personalizado, x-authagonal-bff. Seu valor é irrelevante. A presença é toda a verificação.

Isso parece preguiçoso e não é. Toda a classe de falsificação de requisição entre sites depende de um envio de formulário, de uma tag de imagem ou de uma navegação que outra origem consegue disparar enquanto o seu cookie vai a reboque, e nenhuma dessas coisas consegue definir um cabeçalho personalizado. No momento em que um JavaScript atacante tenta adicionar um, ele deixa de ser uma requisição simples e passa a ser uma com verificação prévia, que a sua política de CORS recusa. O cabeçalho não é um segredo a ser adivinhado, é a prova de que a requisição veio de código e não de marcação.

Ele é exigido nas chamadas feitas por script e deliberadamente não nas que são, por natureza, navegações de nível superior: iniciar um login, voltar do provedor de identidade, seguir um link de logout. Exigir um cabeçalho personalizado numa navegação do navegador apenas quebraria a navegação.

Atualizar, exatamente uma vez

Cada requisição intermediada pode descobrir que o token de acesso está prestes a expirar, e uma página movimentada faz várias requisições ao mesmo tempo. Atualize todas de forma ingênua e você tem um pequeno desastre: várias atualizações simultâneas com um token de atualização rotativo, cada uma invalidando as outras, terminando com o usuário desconectado pelo seu próprio tráfego.

Por isso a atualização é de voo único, por sessão. A primeira requisição a passar toma uma comporta indexada pelo id de sessão, e todas as outras esperam. A parte sutil é o que as que esperam fazem quando finalmente entram: releem a sessão do armazenamento antes de decidir qualquer coisa, porque a requisição que segurou a comporta primeiro muito provavelmente já atualizou, e o valor que elas seguravam ao entrar na fila está desatualizado. Atualize uma vez, e então todos usam o resultado.

Ele também é ciente da rotação, no sentido chato e necessário. Se o endpoint de tokens devolve um novo token de atualização, ele substitui o armazenado. Se a atualização falha com um erro de token, a sessão é apagada e o usuário é desconectado, porque um token de atualização que foi rejeitado não vai ser aceito na próxima tentativa, e tentar de novo é apenas uma forma mais lenta de ser desconectado.

Um BFF, muitos inquilinos

Um BFF de inquilino único resolve tudo a partir da configuração na inicialização: uma autoridade, um id de cliente, um segredo. Servir muitos inquilinos a partir de uma mesma implantação quebra isso, porque agora o BFF tem que descobrir a qual emissor pertence um login, e tem que fazê-lo antes de qualquer sessão existir.

A seleção acontece no endpoint de login, através de um parâmetro de consulta que você escolhe nomear, então pode ser ?slug= ou ?org= ou como quer que o seu produto já chame um cliente. Um resolvedor transforma essa chave numa configuração de inquilino: autoridade, id de cliente, segredo, escopos. Se não resolver em nada, o login é recusado ali mesmo.

Depois a chave tem que sobreviver a uma ida e volta até um provedor de identidade que não sabe nada sobre ela, e retornar. Ainda não há sessão para guardá-la, então ela viaja no cookie de correlação, o cookie criptografado de curta duração que já carrega, exatamente por essa razão, o verificador PKCE, o state e o nonce. Quinze minutos, um por tentativa de login. Na volta o cookie é descriptografado, o inquilino é resolvido de novo a partir da chave dentro dele, e o código é trocado contra o endpoint de tokens daquele inquilino. Só então a chave de inquilino é escrita na sessão, onde se torna a resposta duradoura.

A partir desse ponto, cada caminho resolve novamente a partir da sessão em vez de qualquer coisa que o navegador tenha dito: atualização, logout, o proxy. E quando um inquilino deixa de resolver, porque foi apagado ou desativado, a sessão é destruída em vez de recair silenciosamente sobre algum inquilino padrão. Um inquilino não resolvível é um logout, não um dar de ombros.

O logout back-channel é onde este design justifica a sua existência. O provedor de identidade envia por POST um token de logout diretamente para o seu servidor quando uma sessão termina em outro lugar. Sem cookie. Sem sessão. Sem navegador algum envolvido. Num BFF de inquilino único isso não tem nada de especial, já que só há um emissor de onde ele poderia vir. Num BFF multi-inquilino você tem que responder "para qual inquilino é isto?" antes de poder responder "isto é sequer real?", e o único material disponível é o próprio token, que você não verificou e portanto não pode confiar.

A ordem que torna isto seguro merece ser enunciada com precisão. Leia o claim de emissor do token não verificado e use-o para exatamente uma coisa: escolher qual configuração de inquilino carregar. Depois verifique a assinatura do token contra as chaves publicadas daquele inquilino e o seu id de cliente como audiência. Um emissor forjado escolhe o inquilino que finge ser, e então falha na verificação contra as chaves daquele inquilino, porque o atacante não as tem. Nada nunca foi aceito com base no claim não confiável. O claim apenas decidiu quem pode julgá-lo, e o juiz é uma âncora de confiança que já tínhamos.

Depois disso vêm as verificações que fazem de um token de logout um token de logout: ele não deve carregar um nonce, já que não está autenticando ninguém; deve carregar o evento de logout back-channel; e deve identificar ou uma sessão específica ou um sujeito. Um id de sessão mata uma sessão. Um sujeito mata todas as sessões que aquele usuário tem, que é a forma que emitimos, porque "desconecte-me de todos os lugares" é o que as pessoas realmente querem dizer quando clicam em sair de todos os lugares. É por isso que o armazenamento de sessões mantém um índice por id de sessão e outro por sujeito, em vez de apenas por valor de cookie.

Inquilino único continua entediante

Tudo isso fica inerte no caso comum. O resolvedor padrão devolve a mesma configuração não importa qual chave lhe seja entregue, e a sua resolução por emissor devolve essa configuração incondicionalmente, porque só há uma. Um BFF de inquilino único se comporta exatamente como se comportava antes de a junção existir: nenhum parâmetro de inquilino, nada a mais no cookie, nenhum novo modo de falha. A maquinaria multi-inquilino é opcional e invisível até você nomear um parâmetro de consulta.

Separe a seleção da confiança

Quando você torna algo multi-inquilino, o exercício que vale a pena fazer é enumerar cada ponto de entrada e perguntar o que identifica o inquilino em cada um. Navegações do navegador carregam um cookie que você emitiu e controla. Callbacks de redirecionamento carregam um state que você assinou. Callbacks de servidor para servidor carregam apenas o que quem chamou escolheu enviar, e quem chamou pode estar mentindo.

O ponto de entrada incômodo, aquele que não carrega nada digno de confiança, é aquele cujo design decide se o sistema inteiro é sólido, e costuma ser aquele em que ninguém pensa até tarde. O padrão que nos tirou dessa se generaliza: separe a seleção da confiança. Não há problema em rotear com base em dados não confiáveis desde que o roteamento apenas escolha qual âncora de confiança tem permissão para tomar a decisão final, e a decisão final seja tomada por algo que o atacante não consegue forjar.

Se você prefere não escrever tudo isso por conta própria, Authagonal fornece o BFF para .NET e para Node, e o portal vai provisionar o cliente confidencial de que ele precisa, URI de redirecionamento, endpoint de logout back-channel e tudo mais, em um único clique.