Lab 12 — Autenticação e sessão que não guardam o que não devem
O problema, e a empresa que o tem
A Loja Doze é a mesma equipe de duas pessoas do L03: uma API .NET 8 em ECS Fargate, atrás de um ALB, que o L11 já decidiu manter direto — sem API Gateway nesta banda. O que falta é login: hoje ele existe, funciona, e está prestes a quebrar por um motivo que ninguém vai associar à causa certa.
O código de autenticação segue o template padrão de projeto ASP.NET Core: formulário de login, AddAuthentication().AddCookie() e um dicionário de sessões em memória. Funciona perfeitamente com uma task. A equipe está prestes a ligar a escala automática do L06 para aguentar uma campanha, e ninguém testou o que acontece com duas.
O que acontece é isto: metade das requisições autenticadas passa a devolver 401 ou redirecionar para o login, de forma aparentemente aleatória. Não é aleatório — é qual task recebeu a requisição. A sessão nunca existiu fora do processo que a criou, e o ALB não tem como saber disso ao escolher o alvo.
O que este laboratório NÃO é
Não é autorização fina por recurso nem multi-tenant com isolamento de dados entre lojas — isso é o L38, e ele depende deste. Também não é autenticação adaptativa nem MFA obrigatório por risco de login — fica para um módulo futuro dedicado a isso. Aqui o problema é só um: tirar o estado de sessão do contêiner sem reescrever o cadastro de usuários.
O que você vai conseguir fazer
Objetivos verificáveis: cada um se prova com um comando na seção de implantação, não com a sensação de ter entendido.
- Explicar por que sessão em memória derruba metade das requisições ao passar de 1 para 2 tasks, sem nenhuma configuração errada.
- Nomear os três tokens do Cognito e dizer qual vai no cabeçalho Authorization.
- Provar, com os payloads decodificados, que o token de acesso não tem `aud` e o de identidade não tem `client_id`.
- Configurar validação local de JWT no ASP.NET Core sem cair no erro de audiência que o padrão da biblioteca causa com token de acesso do Cognito.
- Checar a claim `token_use` e recusar um token de identidade usado como credencial de API.
- Medir o atraso real entre revogar uma sessão e o token parar de servir.
- Distinguir pool de usuários de pool de identidades e justificar por que este laboratório usa só o primeiro.
- Decidir onde o token vive no navegador, e explicar o risco que cada escolha aceita.
O que a certificação cobra disto
| Conceito | Certificação | Como aparece aqui | O que dominar |
|---|---|---|---|
| Pool de usuários vs pool de identidades | DVA-C02, SAA-C03 | só pool de usuários; a API nunca troca token por credencial da AWS | qual dos dois autentica e qual troca token por credencial temporária |
| Claims obrigatórias na validação de JWT | DVA-C02 | iss, client_id/aud, exp e token_use conferidos explicitamente no código | quais claims cada tipo de token carrega, e por que token_use é a mais esquecida |
| JWKS e chave pública por `kid` | DVA-C02, SAA-C03 | cache de chave pública na task, sem chamada ao Cognito por requisição | por que validação local escala, e o que ela não garante (revogação imediata) |
| Token de acesso vs token de identidade | DVA-C02 | só o de acesso vai no cabeçalho Authorization | por que o de identidade não serve como credencial de API |
| Revogação de token | DVA-C02 | o atraso entre revogar e o token parar de servir, medido | que o atraso é a validade do token de acesso, não zero |
| Autenticador nativo do ALB (`authenticate-cognito`) | DVA-C02, SAA-C03 | por que este módulo não usa — vira alternativa na decisão | quando essa opção é a escolha certa, e quando atrapalha |
| Escopo de OAuth e autorização fina | DVA-C02 | policy do ASP.NET Core baseada em claim de escopo do resource server | diferença entre autenticado e autorizado |
| PKCE em cliente público | DVA-C02, SAP-C02 | app client sem secret, fluxo de código de autorização com PKCE | por que PKCE substitui o segredo que um cliente público não pode guardar |
Onde isto costuma ser cobrado errado
A pergunta clássica dá um trecho de configuração com `ValidateAudience = true` e um token de acesso do Cognito, e pede por que toda chamada retorna 401 mesmo com token válido. A resposta não é sobre expiração nem sobre chave errada: é que o token de acesso não tem a claim `aud` — só o token de identidade tem. Validar audiência num token que não a carrega falha sempre, não às vezes.
Requisitos, e como cada um muda o desenho
Requisito não funcional que não aparece numa linha de configuração é intenção. A coluna da direita é onde cada um deixou marca.
| Requisito | Valor declarado | O que ele decide no desenho |
|---|---|---|
| Estado no contêiner | zero | proíbe sessão em memória; obriga identidade auto-contida (o token) e validação local |
| Escalar sem derrubar login | de 1 para N tasks, sem afinidade | grupo de destino com `stickiness` desligada; qualquer task valida qualquer token |
| Equipe não mantém código de senha | delegado ao serviço de identidade | pool de usuários do Cognito com hash, política de senha, MFA e recuperação prontos |
| Latência de autenticação | sem chamada de rede por requisição | validação de assinatura local, com chave pública em cache indexado por `kid` |
| Revogação | aceitável com atraso limitado à validade do token | validade curta de token de acesso (15 min); NÃO justifica consultar o Cognito por requisição |
| Cliente é aplicação pública (SPA) | sem lugar seguro para segredo | app client sem secret; fluxo de código de autorização com PKCE |
| Auditoria de sessão encerrada | rastreável | CloudTrail e eventos de revogação consultáveis por identidade e por instante |
| Front já decidido no L11 | ALB direto, sem API Gateway nesta banda | a validação de token acontece na task, não num autorizador de borda |
Arquitetura mínima: o login que morre com a segunda task
Este é o desenho que a Loja Doze tem hoje, e ele é legítimo como ponto de partida: autentica de verdade, com poucas linhas. O laboratório começa por medir o efeito de escalar — porque um número torna o defeito discutível, e "às vezes desloga" não.
- → POST /login com usuário e senha
- → encaminha para a única task saudável
- → confere o hash da senha do usuário
- → cifra o cookie com chave criada na partida
- → Set-Cookie com o identificador de sessão
- Fora da AWS
- Rede e entrega
- Compute
- Segurança e identidade
- Banco de dados
Este desenho autentica de verdade, e é por isso que ele chegou até aqui: formulário, cookie e sessão em memória são o que o template de projeto entrega. Repare, percorrendo os passos, que não há nada configurado errado — há duas coisas guardadas no lugar errado, e as duas só aparecem quando o número de tasks passa de um.
- O servidor sabe quem você é; o navegador, não. O cookie carrega um identificador opaco. Quem traduz esse identificador em "usuária Ana, loja 12, perfil gerente" é um dicionário que vive na memória do processo. A identidade do cliente passou a ser propriedade de uma task específica, e é essa inversão — não o volume — que vai cobrar depois.
- O cookie é cifrado por uma chave que nasce com a task. No ASP.NET Core, o cookie de autenticação é protegido pelo chaveiro do Data Protection. Sem configuração explícita, esse chaveiro é gerado na partida e gravado no sistema de arquivos do contêiner. Logo: mesmo um cookie que carregasse a identidade inteira seria ilegível para qualquer outra task, porque a chave é diferente.
- Com uma task, tudo isso funciona — e é por isso que sobrevive. Nenhum teste reprova este desenho. O login entra, o cookie volta, a sessão persiste. O defeito é latente: ele não está no código, está na suposição de que existe um só servidor. Enquanto a suposição valer, o desenho é correto.
- Passar de uma para duas tasks derruba metade das requisições. O ALB distribui entre alvos saudáveis. A requisição que cair na task que não emitiu o cookie encontra um identificador que não existe no dicionário dela e um cookie cifrado com chave que ela não tem. O sintoma é um 401 ou um redirecionamento para o login em cerca de metade das requisições — e é exatamente o dia em que a escala automática do L06 é ligada.
- A senha virou responsabilidade da equipe. Hash com parâmetro atualizado, política de complexidade, bloqueio por tentativa, recuperação por e-mail, segundo fator, detecção de credencial vazada: cada item é código a escrever e a manter. Nenhum deles é o negócio da empresa, e todos eles falham em silêncio quando envelhecem.
- Por que alguém publica assim. Porque é o menor número de linhas que funciona: duas chamadas no arquivo de inicialização entregam sessão e cookie de autenticação prontos. E porque, com uma task, funciona mesmo. O remendo clássico que vem depois — ligar sessão pegajosa no grupo de destino — mantém o estado no lugar errado e apenas esconde a fatura por mais um trimestre.
# Rode com desired_count=1, faça login, guarde o cookie. Depois suba para 2.
for i in $(seq 1 100); do
curl -s -o /dev/null -w "%{http_code}\n" -b cookie.txt "$URL/api/pedidos"
done | sort | uniq -c
# Com 1 task: 100 respostas "200".
# Com 2 tasks, MESMO cookie, sem sessão pegajosa: cerca de 50 respostas "200" e
# 50 "401" — a moeda decide se a requisição caiu na task que conhece o cookie.Isto não é um bug intermitente — é uma moeda
Com duas tasks e sem afinidade, cada requisição tem cerca de 50% de chance de cair na task que não conhece aquela sessão. A equipe que investiga isso como "bug intermitente" pode gastar dias, porque o sintoma parece aleatório — mas é determinístico pela escolha de alvo do balanceador. Ligar sessão pegajosa faz o sintoma sumir sem corrigir a causa, e é o remendo que este laboratório substitui.
Arquitetura para produção
Cada peça nova abaixo rastreia a uma linha da tabela de requisitos. Se você não conseguir apontar o requisito, a peça é adorno — e este desenho não tem nenhuma.
- → código de autorização com PKCE
- → troca o código pelos três tokens
- → Authorization: Bearer <token de acesso>
- → escolhe qualquer alvo saudável
- → requisição sem nada que a amarre a esta task
- → a mesma requisição funciona aqui, e é isso que se prova
- → busca a chave pública uma vez por `kid`
- → cada task tem o seu cache de chaves
- → registro de revogação e de entrada
- → contagem de 401 rotulada pelo motivo
- Fora da AWS
- Segurança e identidade
- Rede e entrega
- Compute
- Gestão e governança
A diferença estrutural não é uma caixa a mais: é que o fluxo de login deixou de passar pela sua aplicação. A senha nunca chega à task, e a task nunca escreve quem entrou. Percorra os passos e repare que a última peça — a validade curta do token — existe para pagar o preço que a validação local cobra.
- O navegador fala com o serviço de identidade, não com a sua API. A senha vai do navegador para o pool de usuários e nunca atravessa a sua task. Isso apaga de uma vez a tabela de senhas, o código de bloqueio por tentativa e o fluxo de recuperação — e é a peça que rastreia ao requisito "a equipe não guarda credencial".
- Três tokens, três perguntas diferentes. O de identidade responde "quem é esta pessoa" e serve para a interface mostrar o nome. O de acesso responde "o que este portador pode fazer" e é o único que vai no cabeçalho de autorização. O de atualização responde "posso continuar sem pedir senha de novo" e nunca sai do lugar mais protegido que você tiver. Trocar o primeiro pelo segundo é o erro mais comum da banda.
- A requisição carrega a identidade; nenhuma task carrega. O que chega ao alvo é um token assinado que contém emissor, cliente, expiração, uso e escopos. Não há consulta a dicionário de sessões, porque não há dicionário: o estado voltou para quem é dono dele.
- A validação é local, e o cache de chaves é o que a torna local. A task busca o conjunto de chaves públicas do pool uma vez, indexa por `kid` e guarda em memória. A partir daí, cada requisição custa uma verificação de assinatura RSA — trabalho de processador, sem ida à rede. É isso que faz a autenticação escalar junto com as tasks em vez de estrangular num ponto central.
- Qualquer alvo serve, e é assim que se prova que o estado saiu. Com afinidade desligada, requisições consecutivas do mesmo cliente caem em tasks diferentes e todas respondem igual. Se ligar sessão pegajosa faz o sistema funcionar, o estado ainda está no contêiner e o laboratório não terminou.
- O preço: revogar deixa de ter efeito imediato. Quem valida assinatura localmente não pergunta ao emissor se o token ainda vale — e a documentação da AWS é explícita: um token revogado continua passando em qualquer biblioteca que confira assinatura e expiração. A revogação corta a renovação, não o token em curso. O único mecanismo que limita o dano é a validade curta do token de acesso.
- Um 401 não é um só motivo, e o painel precisa distinguir. Assinatura inválida indica token forjado ou de outro pool. Expiração indica relógio ou renovação que falhou. Uso indevido — token de identidade no lugar do de acesso — indica cliente mal escrito, e é o mais frequente na primeira semana. Sem rótulo por motivo, os três somam num número que não decide nada.
A diferença estrutural em relação ao desenho anterior não é uma caixa a mais: é que o fluxo de login deixou de passar pela sua aplicação, e a task deixou de escrever qualquer coisa sobre quem entrou. Tudo o mais — JWKS em cache, grupo de destino sem afinidade, alarme de 401 por motivo — existe para sustentar essa mudança.
O critério de aceite, numa frase
Se você matar a task que atendeu o login e a próxima requisição do mesmo cliente for atendida por outra task sem erro nenhum, o estado saiu do contêiner. Qualquer outro teste é secundário a este.
O caminho de um login, ponta a ponta
Os nomes dos tokens não são jargão intercambiável: cada um responde a uma pergunta diferente, e usar o errado no lugar certo é o erro mais comum da banda. O de identidade responde "quem é esta pessoa"; o de acesso responde "o que este portador pode fazer"; o de atualização responde "posso continuar sem pedir senha de novo".
A distinção que o nome parecido esconde
Pool de usuários e pool de identidades resolvem problemas diferentes, apesar do nome quase igual. Se a pergunta é "quem é este usuário e o que ele pode fazer na MINHA API", é pool de usuários — este laboratório. Se a pergunta é "como este navegador consegue uma credencial da AWS para chamar um serviço da AWS diretamente", é pool de identidades — assunto que não aparece aqui porque a Loja Doze não tem esse caso.
O payload a seguir mostra o mesmo login gerando os dois tokens que interessam à API — repare no que cada um tem, e no que falta em cada um.
// Claims decodificadas de um token de ACESSO do Cognito (exemplo
// ilustrativo, sem segredo nenhum: token de acesso nao carrega senha).
// Repare na AUSENCIA de "aud" e na PRESENCA de "client_id".
{
"sub": "3f2a1c4e-58b1-4a11-9c2e-1a2b3c4d5e6f",
"iss": "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_ExemploXX",
"client_id": "1a2b3c4d5e6f7g8h9i0j",
"token_use": "access",
"scope": "https://api.ffv-lab.exemplo/pedidos.leitura https://api.ffv-lab.exemplo/pedidos.escrita",
"auth_time": 1754575200,
"exp": 1754576100,
"iat": 1754575200,
"jti": "8b7e1a2c-91a0-4f7e-8b1a-2c3d4e5f6a7b",
"origin_jti": "4d3f9a1b-6c5d-4e3f-9a1b-6c5d4e3f9a1b",
"username": "ana@lojadoze.exemplo"
}
// O de IDENTIDADE do MESMO login. Repare: tem "aud", NAO tem "client_id",
// e carrega atributo de perfil — e por isso que ele nao serve para
// autorizar chamada de API.
{
"sub": "3f2a1c4e-58b1-4a11-9c2e-1a2b3c4d5e6f",
"iss": "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_ExemploXX",
"aud": "1a2b3c4d5e6f7g8h9i0j",
"token_use": "id",
"auth_time": 1754575200,
"exp": 1754576100,
"iat": 1754575200,
"email": "ana@lojadoze.exemplo",
"email_verified": true,
"custom:loja_id": "loja-12"
}
O erro mais comum da banda, em uma frase
Enviar o token de identidade no cabeçalho Authorization porque ele "também tem os dados do usuário". Ele tem — mas não tem `client_id`, não carrega escopo de autorização, e sua `token_use` vale `id`. Uma API que não checa essa claim aceita os dois tokens como se fossem a mesma coisa, e passa a autorizar com uma credencial que nunca foi desenhada para autorizar.
As decisões, e o que se perde em cada uma
📋 Uma API .NET 8 em ECS Fargate atrás de ALB, que precisa passar de uma para várias tasks sem derrubar o login, com uma equipe pequena que não quer manter código de senha, segundo fator e recuperação de conta.
A escolha resolve os dois problemas do desenho mínimo de uma vez. O estado sai do contêiner porque a identidade passa a viajar assinada na requisição — então qualquer task atende qualquer cliente, e a escala automática deixa de ser um evento de indisponibilidade. E a credencial sai da responsabilidade da equipe, porque hash, política de senha, segundo fator e recuperação passam a ser do serviço de identidade. A validação local é o que mantém isso barato: uma verificação de assinatura por requisição, sem ida à rede e sem ponto central que estrangule. O preço é declarado e não escondido — revogar deixa de ter efeito imediato — e ele se administra com validade curta de token de acesso.
Alt: Sessão distribuída em Redis (ElastiCache) — Resolve o "escalar derruba o login" mantendo a sessão no servidor, só num servidor compartilhado. É legítimo, e às vezes é a resposta certa — dá revogação instantânea, que é justamente o que o token não dá. Cobra um recurso novo por hora ligada, uma dependência na rota crítica de TODA requisição autenticada e um cache que, ao cair, desloga a base inteira. O L13 trata desse cache; aqui ele seria a solução mais cara para o problema declarado.
Alt: Autenticador do próprio ALB (`authenticate-cognito`) — Tira o fluxo de login do seu código: o balanceador redireciona, troca o código, mantém cookie de sessão e entrega afirmações ao alvo em cabeçalhos `x-amzn-oidc-*`. Excelente para aplicação de navegador e para painel interno. Mas ele não passa o token de identidade ao alvo, cria uma sessão por cookie que precisa de listener HTTPS, e para chamada de programa a programa ou de aplicação de página única ele atrapalha em vez de ajudar. Tem seção própria neste módulo.
Alt: Autorizador de JWT no API Gateway — Move a validação para a borda e sua aplicação passa a receber as afirmações já verificadas. É a evolução natural quando existe API Gateway na frente por causa de cota e chave de API — mas a decisão do L11 para esta banda foi ALB direto, sem API Gateway. Aqui a validação entra no seu código, e é como se aprende o que a borda faria por baixo.
Alt: Sessão pegajosa no grupo de destino — É o remendo de uma linha, e ele funciona por um tempo. Mantém o estado no lugar errado: a task que cai desloga os clientes dela, a distribuição de carga desequilibra, e a implantação sem indisponibilidade do L03 volta a ser visível para quem estava logado. Trata o sintoma e preserva a causa.
| Decisão | Escolha | Alternativas | Motivo | O que se perde |
|---|---|---|---|---|
| Onde a senha existe | só no pool de usuários do Cognito | tabela própria com hash; provedor de identidade externo (SAML/OIDC) | a equipe não escreve nem mantém código de credencial | perde controle fino sobre o formulário de login sem usar o Hosted UI customizado |
| Tipo de pool | pool de usuários (autenticação) | pool de identidades (credencial temporária da AWS) | a API é sua, não um serviço da AWS chamado direto do navegador | nada aqui; seria diferente se o cliente precisasse falar direto com S3 ou DynamoDB |
| Onde a assinatura é conferida | na task, localmente, com JWKS em cache | no ALB (`authenticate-cognito`); no API Gateway (autorizador JWT) | sem ponto central que estrangule, sem chamada de rede por requisição | a validação vira responsabilidade de cada linguagem/serviço que a implementa |
| Validade do token de acesso | 15 minutos | 60 minutos (padrão do console); 5 minutos; 24 horas | é o único termo que limita o atraso de revogação — curto por decisão, não por acidente | renovação mais frequente; exige que o token de atualização funcione de verdade |
| Claim de audiência | checagem manual de `client_id`, com `ValidateAudience = false` | deixar o padrão `ValidateAudience = true` do middleware | o token de acesso do Cognito não tem `aud` — tem `client_id`; o padrão derruba tudo | nada; é a correção de um comportamento que não fazia sentido para este token |
| Sessão pegajosa no grupo de destino | desligada | ligada, como remendo de sessão em memória | é o critério de aceite: qualquer alvo tem de servir qualquer cliente | nada aqui; seria necessária se houvesse estado local, o que deixou de existir |
A dívida que a validação local cria, e que este módulo não esconde
Revogar uma conta comprometida não corta o token de acesso em curso — corta a renovação. Enquanto esse token não expirar, ele continua servindo em qualquer validação que confira só assinatura e expiração, que é exatamente o que a task faz. A mitigação não é "validar mais": é manter a validade do token de acesso curta o bastante para que o pior caso seja aceitável — 15 minutos aqui, contra os 60 do padrão do console.
Construir: o pool de usuários e o cliente que não guarda segredo
O Terraform abaixo cria o pool, o domínio do Hosted UI (necessário para o fluxo de código de autorização), os escopos próprios do recurso e o app client público — sem `generate_secret`, porque um navegador não tem onde esconder um segredo de verdade.
# cognito.tf — o pool de usuarios, e o cliente que nao pode guardar segredo
resource "aws_cognito_user_pool" "usuarios" {
name = "${var.projeto}-usuarios"
# E-mail e a identidade que a loja usa para logar; nao existe nome de
# usuario separado que ninguem lembra.
username_attributes = ["email"]
auto_verified_attributes = ["email"]
password_policy {
minimum_length = 12
require_lowercase = true
require_uppercase = true
require_numbers = true
require_symbols = true
}
# MFA opcional aqui. Autenticacao adaptativa por risco fica fora do escopo
# deste modulo — misturar os dois assuntos dilui o nucleo, que e tirar o
# estado do conteiner.
mfa_configuration = "OPTIONAL"
software_token_mfa_configuration {
enabled = true
}
account_recovery_setting {
recovery_mechanism {
name = "verified_email"
priority = 1
}
}
# Sem isto, um "terraform destroy" apaga a base de usuarios sem aviso.
# E o unico recurso deste modulo em que isso importa de verdade.
deletion_protection = "ACTIVE"
tags = local.tags
}
# Dominio do Hosted UI: e por onde o fluxo de codigo de autorizacao com
# PKCE acontece. O prefixo tem de ser unico GLOBALMENTE entre contas AWS.
resource "aws_cognito_user_pool_domain" "login" {
domain = "${var.projeto}-${data.aws_caller_identity.atual.account_id}"
user_pool_id = aws_cognito_user_pool.usuarios.id
}
# Escopos proprios, alem dos padrao (openid/email). Sao o que autoriza por
# ACAO, nao so por identidade — a diferenca que a secao de autorizacao usa.
resource "aws_cognito_resource_server" "api" {
identifier = "https://api.${var.projeto}.exemplo"
name = "API de pedidos"
user_pool_id = aws_cognito_user_pool.usuarios.id
scope {
scope_name = "pedidos.leitura"
scope_description = "ler pedidos da loja autenticada"
}
scope {
scope_name = "pedidos.escrita"
scope_description = "criar e alterar pedidos"
}
}
resource "aws_cognito_user_pool_client" "spa" {
name = "${var.projeto}-spa"
user_pool_id = aws_cognito_user_pool.usuarios.id
# SEM secret. Um navegador nao tem onde guardar um segredo de verdade —
# ele acaba no bundle JavaScript, legivel por qualquer um. PKCE
# substitui o segredo por um par verificador/desafio gerado a cada login.
generate_secret = false
allowed_oauth_flows_user_pool_client = true
allowed_oauth_flows = ["code"]
allowed_oauth_scopes = [
"openid", "email",
"${aws_cognito_resource_server.api.identifier}/pedidos.leitura",
"${aws_cognito_resource_server.api.identifier}/pedidos.escrita",
]
supported_identity_providers = ["COGNITO"]
callback_urls = ["https://loja.${var.projeto}.exemplo/autenticado"]
logout_urls = ["https://loja.${var.projeto}.exemplo/"]
# Nao revela SE o e-mail existe — reduz enumeracao de conta, ao custo de
# uma mensagem de erro um pouco menos especifica no login.
prevent_user_existence_errors = "ENABLED"
# As tres validades, explicitas. O valor do token de acesso e o UNICO
# termo que voce controla na formula de atraso de revogacao — por isso
# ele fica curto aqui, abaixo do padrao do console.
access_token_validity = 15
id_token_validity = 15
refresh_token_validity = 30
token_validity_units {
access_token = "minutes"
id_token = "minutes"
refresh_token = "days"
}
# Explicito, embora ja seja o padrao em app client novo: e o que faz
# RevokeToken e GlobalSignOut terem efeito sobre o token de atualizacao.
enable_token_revocation = true
}
output "autoridade_do_pool" {
value = "https://cognito-idp.${var.regiao}.amazonaws.com/${aws_cognito_user_pool.usuarios.id}"
description = "Authority que o middleware JwtBearer usa para achar o JWKS via OIDC discovery"
}
Por que a validade do token de acesso saiu do padrão
O console cria app clients com token de acesso e de identidade válidos por 60 minutos. Este laboratório declara 15 minutos, de propósito: é o único número que limita o atraso entre revogar uma conta e o token realmente parar de servir. Não existe valor "certo" — existe o valor que a sua tolerância a esse atraso permite, balanceado contra quantas vezes o cliente precisa renovar.
Construir: o grupo de destino que prova que o estado saiu
O ALB não muda de papel: ele continua só encaminhando, decisão que o L11 já tomou. O que muda aqui é uma linha — `stickiness.enabled = false` — e ela é o motivo pelo qual este laboratório funciona como prova, não só como configuração.
# alb.tf — grupo de destino sem afinidade, e listener que so encaminha
resource "aws_lb_target_group" "api" {
name = "${var.projeto}-api"
port = 8080
protocol = "HTTP"
vpc_id = aws_vpc.principal.id
target_type = "ip" # awsvpc: o alvo e o IP da task, nao a instancia
health_check {
path = "/health/ready"
interval = 15
healthy_threshold = 2
unhealthy_threshold = 2
matcher = "200"
}
# O criterio de aceite deste laboratorio, em uma linha de Terraform: se
# qualquer task serve qualquer cliente, o estado saiu do conteiner.
# Afinidade ligada esconderia um bug de sessao em memoria atras de
# "funciona na pratica" — e foi exatamente o que sustentou o desenho
# minimo por tanto tempo.
stickiness {
enabled = false
type = "lb_cookie"
}
}
resource "aws_lb_listener_rule" "api" {
listener_arn = aws_lb_listener.https.arn
priority = 10
action {
# Decisao registrada na secao de decisoes: o ALB SO encaminha. Ele nao
# usa a acao nativa "authenticate-cognito" porque ela nao entrega o
# token de acesso ao alvo em forma utilizavel pela API, cria sessao
# por cookie e exige listener HTTPS dedicado — um desenho diferente
# do que este modulo ensina, e coberto na secao de alternativas.
type = "forward"
target_group_arn = aws_lb_target_group.api.arn
}
condition {
path_pattern {
values = ["/api/*"]
}
}
}
Por que não usar `authenticate-cognito` no próprio ALB
A ação nativa do ALB resolve o login sem escrever código, e é a escolha certa para muita aplicação de navegador. Ela não entra aqui porque não entrega o token de acesso ao alvo em forma utilizável pela API — o alvo recebe afirmações em cabeçalhos `x-amzn-oidc-*`, não o JWT original — e porque exige um listener HTTPS dedicado à autenticação. Para uma API que também atende cliente programático (o caso do resto desta série), validar o token no próprio código é o que generaliza.
Construir: o middleware que confia na assinatura, não na rede
É o núcleo do laboratório. `Authority` aponta para o pool, e a partir dela a biblioteca descobre o `jwks_uri` sozinha e cuida do cache de chave por `kid` — nenhum código de cache aparece aqui porque ele já vem pronto. O que precisa de código manual são as duas checagens que o padrão da biblioteca não faz por você.
// Program.cs — a task nao guarda quem entrou; ela CONFERE quem entrou
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args);
var authority = builder.Configuration["Cognito:Authority"]!; // https://cognito-idp.{regiao}.amazonaws.com/{poolId}
var clienteEsperado = builder.Configuration["Cognito:ClientId"]!;
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(o =>
{
// A Authority basta para a biblioteca achar
// {authority}/.well-known/openid-configuration, de la o jwks_uri, e
// fazer o cache de chave publica por `kid` sozinha, via um
// ConfigurationManager interno com atualizacao automatica. Nao ha
// codigo de cache neste arquivo porque ele ja vem pronto na
// biblioteca — e e esse mecanismo que torna a validacao local.
o.Authority = authority;
o.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = authority,
// O ACHADO deste laboratorio: o token de ACESSO do Cognito nao
// tem a claim `aud` — ele tem `client_id`. Deixar isto em true
// (o padrao da biblioteca) derruba toda chamada com erro de
// audiencia, mesmo com um token perfeitamente valido. A
// conferencia do cliente acontece abaixo, manualmente, na
// claim certa.
ValidateAudience = false,
ValidateLifetime = true,
// O padrao da biblioteca e 5 minutos. Reduzido para bater com
// a formula de atraso de revogacao da secao anterior: cada
// segundo aqui e atraso que voce paga sem ganhar nada, porque
// os relogios das tasks em Fargate ja sincronizam por NTP.
ClockSkew = TimeSpan.FromSeconds(30),
};
o.Events = new JwtBearerEvents
{
OnTokenValidated = ctx =>
{
var principal = ctx.Principal!;
// `token_use` e a claim que a validacao padrao NUNCA
// confere. Sem isto, um token de IDENTIDADE — que tem
// assinatura, emissor e expiracao igualmente validos —
// passaria como credencial de API.
var uso = principal.FindFirstValue("token_use");
if (uso != "access")
{
ctx.Fail($"token_use precisa ser access; recebido: {uso}");
return Task.CompletedTask;
}
// client_id substitui aud no token de acesso. Sem esta
// checagem, um token de acesso valido emitido para OUTRO
// app client do mesmo pool passaria tambem.
var cliente = principal.FindFirstValue("client_id");
if (cliente != clienteEsperado)
{
ctx.Fail("client_id nao confere com o app client esperado");
return Task.CompletedTask;
}
return Task.CompletedTask;
},
};
});
builder.Services.AddAuthorization(o =>
{
// Autenticado != autorizado. O escopo do resource server e quem
// decide o que este portador pode fazer — o pool so prova quem ele e.
o.AddPolicy("PedidosLeitura", p =>
p.RequireClaim("scope", "https://api.ffv-lab.exemplo/pedidos.leitura"));
o.AddPolicy("PedidosEscrita", p =>
p.RequireClaim("scope", "https://api.ffv-lab.exemplo/pedidos.escrita"));
});
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
// Nenhuma linha aqui grava sessao em lugar nenhum. O que autoriza esta
// chamada e o token que chegou NESTA requisicao — a proxima pode cair em
// outra task e o resultado e identico.
app.MapGet("/api/pedidos", async (AppDb db, ClaimsPrincipal usuario) =>
Results.Ok(await db.Pedidos
.Where(ped => ped.LojaId == usuario.FindFirstValue("custom:loja_id"))
.ToListAsync()))
.RequireAuthorization("PedidosLeitura");
app.Run();
Nunca desligue a validação de assinatura para "resolver rápido"
Quando um deploy quebra com erro de token, a tentação de definir `SignatureValidator` para sempre aceitar, "só para destravar", existe — e ela abre a API para qualquer JWT bem formado, de qualquer emissor, sem verificação nenhuma. Isso não é um atalho temporário: é aceitar credencial forjada. O erro quase sempre está em `ValidateAudience` ou em `token_use`, não na assinatura em si.
Implantar, e provar com número — não com sensação
Cinco provas. Nenhuma delas aceita "parece que funcionou" como resultado.
# provas.sh — cinco medicoes; nenhuma conclusao vem de "parece que funcionou"
REGIAO=us-east-1
POOL_ID=$(terraform output -raw pool_id)
CLIENT_ID=$(terraform output -raw client_id)
API="https://$(terraform output -raw dominio)/api/pedidos"
# ── Prova 1: o token de acesso NAO tem aud, o de identidade tem ─────────────
# Pega um par de tokens reais (apos login manual pelo Hosted UI) e decodifica
# so o payload (segundo segmento) de cada um.
echo "$TOKEN_DE_ACESSO" | cut -d. -f2 | base64 -d 2>/dev/null | jq 'has("aud"), has("client_id")'
echo "$TOKEN_DE_IDENTIDADE" | cut -d. -f2 | base64 -d 2>/dev/null | jq 'has("aud"), has("client_id")'
# Esperado: token de acesso -> false, true. Token de identidade -> true, false.
# ── Prova 2: qualquer task serve qualquer cliente ────────────────────────────
# 200 requisicoes com o MESMO token, contra um servico com 2+ tasks.
for i in $(seq 1 200); do
curl -s -o /dev/null -w '%{http_code} ' -H "Authorization: Bearer $TOKEN_DE_ACESSO" "$API"
done | tr ' ' '\n' | sort | uniq -c
# Na Cadencia, com stickiness desligada: 200 respostas "200", zero "401" —
# contra ~50% de 401 no desenho minimo com 2 tasks (secao de arquitetura).
# ── Prova 3: token de identidade e recusado como credencial de API ──────────
curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $TOKEN_DE_IDENTIDADE" "$API"
# Esperado: 401. Se vier 200, a checagem de token_use nao esta ativa.
# ── Prova 4: revogar corta a RENOVACAO, nao o token de acesso em curso ──────
INICIO=$(date +%s)
aws cognito-idp admin-user-global-sign-out --user-pool-id "$POOL_ID" \
--username ana@lojadoze.exemplo --region "$REGIAO"
curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $TOKEN_DE_ACESSO" "$API"
# Esperado: 200 — mesmo apos o sign-out global, o token de acesso em curso
# continua servindo ate expirar. Repita a cada minuto e anote o instante em
# que passa a dar 401: deve bater com "iat + access_token_validity" (15 min).
while curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $TOKEN_DE_ACESSO" "$API" \
| grep -q 200; do sleep 30; done
FIM=$(date +%s); echo "atraso de revogacao observado: $(( (FIM-INICIO)/60 )) min"
# ── Prova 5: a chave publica e buscada uma vez, nao a cada requisicao ───────
# Compare a duracao da primeira chamada (cache frio) com a media das 50
# seguintes (cache quente) na mesma task, medindo so o tempo de middleware.
curl -s -o /dev/null -w 'primeira: %{time_total}s\n' -H "Authorization: Bearer $TOKEN_DE_ACESSO" "$API"
for i in $(seq 1 50); do
curl -s -o /dev/null -w '%{time_total}\n' -H "Authorization: Bearer $TOKEN_DE_ACESSO" "$API"
done | awk '{s+=$1; n++} END {print "media das seguintes: " s/n "s"}'
# Na Cadencia: primeira chamada em 0,31 s (inclui buscar o JWKS); as 50
# seguintes em 0,004 s de media — a diferenca e a ida a rede que some.
| Prova | Comando | Resultado que aprova | O que reprova, e o que significa |
|---|---|---|---|
| 1 · Estrutura das claims | decodificar os dois payloads | acesso sem `aud`; identidade sem `client_id` | se os dois tiverem as mesmas claims, algo no fluxo de emissão está errado |
| 2 · Qualquer task serve qualquer cliente | 200 chamadas com o mesmo token | 100% de respostas 200, contra 2+ tasks | qualquer 401 aqui indica afinidade ligada ou estado escondido em algum lugar |
| 3 · Token de identidade é recusado | 1 chamada com o token errado | 401 | se vier 200, a checagem de `token_use` não está no caminho de validação |
| 4 · O atraso de revogação é mensurável | sign-out global + laço até 401 | atraso perto da validade do token de acesso (15 min) | atraso muito maior indica `ClockSkew` alto ou validade mal configurada |
| 5 · O JWKS é buscado uma vez, não sempre | primeira chamada vs média de 50 | primeira ordens de grandeza mais lenta que a média | se todas as chamadas tiverem o mesmo tempo, o cache não está funcionando |
Quebrar de propósito: três falhas e o diagnóstico
As três são bugs reais de configuração, não falhas de infraestrutura — e as três se escondem atrás de mensagens de erro que não apontam para a causa.
| Falha | Como provocar | Sintoma | Onde olhar | Correção |
|---|---|---|---|---|
| Audiência validada num token que não a tem | deixe `ValidateAudience = true` (o padrão) e chame a API com token de acesso | 401 em toda chamada, mesmo com token recém-emitido e válido | log do middleware mostra falha de validação de audiência (`aud` ausente) | `ValidateAudience = false` e checar `client_id` manualmente no evento |
| Token de identidade aceito como credencial de API | remova a checagem de `token_use` e envie o token de identidade no cabeçalho | a API autoriza normalmente, mas sem escopo nenhum de recurso associado | nenhum log acusa — é preciso comparar `token_use` no payload decodificado | checar `token_use == "access"` no `OnTokenValidated`, sempre |
| JWKS travado num cache manual sem atualização | copie a chave pública uma vez para um arquivo estático em vez de usar `Authority` | tudo funciona até o Cognito rotacionar a chave de assinatura; depois, 401 em massa | erro de assinatura inválida súbito, sem nenhuma mudança de código correspondente | usar `Authority` e o `ConfigurationManager` automático da biblioteca, nunca cópia manual |
A pergunta que resolve as três
O erro é sobre QUEM (assinatura, emissor, expiração) ou sobre O QUÊ (audiência, uso, escopo)? As claims de identidade do emissor raramente falham depois que o primeiro deploy funciona. As claims de contexto — `aud`, `client_id`, `token_use` — são as que mudam de comportamento entre ambientes e entre tipos de token, e são onde a maioria dos bugs desta seção realmente mora.
Sua API .NET recebe um token no cabeçalho Authorization e apenas confere assinatura, emissor e expiração — nada mais. Um cliente mal configurado envia o token de identidade em vez do de acesso. O que acontece?
Segurança: onde o token vive, e por que isso é decisão
Tirar a sessão do contêiner resolve o problema de escala e cria um novo: agora existe uma credencial completa guardada no navegador, e onde ela mora decide contra qual ataque ela fica exposta.
| Onde guardar | Exposto a | Protegido de | Por que entra ou não aqui |
|---|---|---|---|
| localStorage / sessionStorage | leitura por qualquer script — inclusive XSS | nada relevante | evitar sempre; é o padrão de tutorial e o mais fácil de atacar |
| Variável em memória da aplicação (SPA) | ainda XSS, mas some ao fechar a aba | não sobrevive a reload sem novo login ou renovação silenciosa | escolha deste laboratório para o token de acesso |
| Cookie `HttpOnly` + `SameSite=Strict` | CSRF se mal configurado; exige um backend que o defina | leitura por JavaScript, inclusive XSS | correto quando existe um backend-for-frontend; fora do escopo aqui, que é API pura consumida por SPA |
| Risco | Probabilidade | Impacto | Controle preventivo | Detecção | Resposta |
|---|---|---|---|---|---|
| Token de acesso roubado via XSS | média | alto | manter o token em memória, nunca em `localStorage`/`sessionStorage` | padrão de uso do token a partir de IP/agente incomum no CloudTrail | revogar com `GlobalSignOut`, forçar novo login, corrigir a vulnerabilidade de XSS |
| Token de identidade aceito como credencial de API | média | alto | checar `token_use == access` em todo endpoint protegido | métrica de 401 rotulada por motivo mostrando picos de "uso indevido" | corrigir o validador; auditar quem consumiu com o token errado |
| `ValidateAudience` padrão derruba toda chamada | alta | médio | `ValidateAudience = false` + checagem manual de `client_id` | taxa de 401 com motivo de audiência nos logs de diagnóstico do middleware | corrigir configuração — não é incidente de segurança, é bug de integração |
| Refresh token de vida longa vazado | baixa | alto | refresh token só no lado que pode protegê-lo; nunca em `localStorage` | uso do endpoint de token fora do padrão habitual do usuário | revogar todos os tokens do usuário com `AdminUserGlobalSignOut` |
| Escopo concedido além do necessário ao app client | média | médio | `allowed_oauth_scopes` mínimo; escopos próprios no resource server | revisão periódica da configuração do app client | reduzir escopo e forçar reautenticação dos clientes afetados |
| Cache de JWKS nunca atualizado | baixa | médio | usar o `ConfigurationManager` automático da biblioteca, nunca cópia manual sem TTL | pico de falha de assinatura logo após rotação de chave no pool | forçar atualização do cache; nunca desligar a validação de assinatura para contornar |
Observabilidade: as perguntas que o painel tem de responder
Um painel de autenticação tem função estreita: dizer se o login está funcionando e por que uma chamada autenticada está falhando. Métrica que não ajuda nessas duas decisões pertence a outro painel.
| Pergunta | Métrica ou consulta | O que significa mudar | Limiar inicial |
|---|---|---|---|
| Quantos 401 por motivo distinto? | log estruturado rotulado (assinatura/expirado/token_use/audiência) | pico de "uso indevido" indica cliente mal escrito; pico de "expirado" indica renovação falhando | acima de 1% das chamadas autenticadas |
| A cache de JWKS está funcionando? | contagem de chamadas ao `jwks_uri` nos logs de saída da task | deveria ser baixa e estável por task; disparo indica cache não persistindo entre requisições | mais de poucas dezenas por hora por task |
| O login está funcionando? | taxa de sucesso do fluxo de código no Hosted UI | queda súbita indica problema no app client, no domínio ou no provedor | defina com base no volume diário observado; qualquer queda abrupta investiga |
| Alguém está sendo negado por escopo, não por token inválido? | 403 separado de 401 no log da aplicação | 403 é autorização; 401 é autenticação — confundir os dois atrasa o diagnóstico | qualquer 403 concentrado num único cliente |
| Quanto tempo leva entre revogar e o token parar de servir? | eventos de revogação no CloudTrail cruzados com 401 por expiração | mede o atraso real, não o teórico | maior que a validade configurada do token de acesso é bug |
| A latência de validação mudou? | duração do middleware de autenticação, se instrumentado | alta persistente indica falha na atualização automática do cache de chaves | p99 poucos milissegundos acima da verificação RSA em si |
A métrica que mais economiza tempo de diagnóstico
Separar 401 por motivo é o maior retorno por linha de código nesta seção. Sem rótulo, todo 401 vira "problema de login" — com rótulo, "token expirado" aponta para renovação, "audiência inválida" aponta para configuração, e "uso indevido" aponta para um cliente mandando o token errado. São três investigações diferentes atrás do mesmo código HTTP.
Escala: 10, 10 mil, 1 milhão — e falha de AZ
| Volume | O que acontece com a autenticação | O que passa a doer | O que fazer |
|---|---|---|---|
| 10 logins/dia, 2 tasks | validação local, cache de JWKS quente na maior parte do tempo | nada; é o cenário do laboratório | nada |
| 500 logins/dia, 8 tasks | cada task busca o JWKS uma vez ao subir; depois, zero rede | nenhum gargalo novo — validação escala linearmente com o número de tasks | nada a mudar; é o ponto forte deste desenho |
| Pico de campanha, escala de 2 para 20 tasks | cada task nova busca o JWKS ao iniciar | 20 chamadas simultâneas ao `jwks_uri` na subida — pequenas, mas simultâneas | nada de especial: é tráfego desprezível comparado ao de autenticação de usuário |
| 5 mil requisições/s autenticadas | CPU da task gasta em verificação RSA por requisição | a verificação de assinatura vira linha visível no perfil de CPU | medir; se dominar o perfil, considerar cache de resultado de validação por token dentro do TTL curto |
| Falha de AZ | tasks da AZ afetada somem; as restantes continuam validando sozinhas | nada específico de autenticação — é o mesmo comportamento do L01/L02 | nenhum: é exatamente o ganho deste laboratório. Nenhuma sessão se perde, porque nenhuma existia na task |
| Cognito indisponível (raro, mas possível) | logins novos falham; sessões já autenticadas continuam | usuário já logado não percebe nada até o token expirar e precisar renovar | é o motivo pelo qual validação local importa: a dependência crítica só entra no login, não em cada requisição |
O efeito que só aparece quando você procura
Falha de AZ com sessão em memória derruba a sessão de quem estava naquela task. Falha de AZ com token validado localmente não derruba sessão nenhuma — o cliente redireciona para outra task, que valida o mesmo token do mesmo jeito. É o mesmo requisito do L02 (AZ como domínio de falha), agora aplicado à identidade.
Custo: o que este laboratório acrescenta à fatura
O Cognito cobra por usuário ativo mensal (o modelo, não o valor — confira no AWS Pricing Calculator). A parte que mais importa aqui não está na fatura do Cognito: está em quantas chamadas você faz A ELE por requisição — e a resposta certa é zero.
| Cenário | Volume | O que acrescenta | Tendência | Otimização |
|---|---|---|---|---|
| Protótipo | 30 usuários cadastrados, poucos logins/dia | usuários ativos mensais dentro da faixa gratuita típica | desprezível | nenhuma; otimizar aqui é gastar atenção onde não há dinheiro |
| Produção pequena | 30 lojas, ~400 logins/dia | usuários ativos mensais na casa de dezenas; validação local sem custo por requisição | baixa e previsível | nenhuma ação necessária |
| Alta escala | milhares de usuários, milhões de requisições/dia | usuários ativos mensais crescem; requisições validadas continuam sem custo de API | cresce com usuários ativos, não com tráfego | garantir que NENHUM caminho chame `GetUser` ou `InitiateAuth` por requisição — é o antipadrão que inverte esse modelo |
O antipadrão que transforma custo previsível em imprevisível
Chamar uma API do Cognito (como `GetUser`) para validar cada requisição — em vez de validar a assinatura localmente — reintroduz exatamente o que este laboratório removeu: uma chamada de rede por requisição, sujeita a cota e a throttling, e que cobra por transação em vez de por usuário. É o erro mais caro desta seção, porque ele passa despercebido até o volume crescer.
Well-Architected nos seis pilares
| Pilar | Situação ao fim deste laboratório | Risco que fica | Melhoria | Prioridade |
|---|---|---|---|---|
| Excelência operacional | 401 rotulado por motivo; atraso de revogação medido, não estimado | painel ainda depende de log estruturado manual, sem alarme automático por motivo | alarme do CloudWatch sobre a métrica de "uso indevido" (L08) | média |
| Segurança | senha delegada ao Cognito; token validado localmente com claims corretas | revogação não é instantânea; token em memória do navegador ainda é alvo de XSS | MFA obrigatório por risco e detecção de credencial comprometida | alta |
| Confiabilidade | qualquer task serve qualquer cliente; falha de AZ não derruba sessão | Cognito é dependência crítica do LOGIN, ainda que não de cada requisição | nenhuma ação isolada resolve; é trade-off aceito e documentado | baixa |
| Eficiência de performance | validação em memória, sem ida à rede por requisição | verificação RSA por requisição pode dominar CPU em volume muito alto | medir antes de otimizar; considerar cache de resultado dentro do TTL curto | baixa |
| Otimização de custos | cobrança por usuário ativo, não por requisição validada | nenhuma chamada de API do Cognito no caminho crítico — é o que preserva o modelo | nenhuma ação; é o estado desejado | baixa |
| Sustentabilidade | nenhum recurso extra ligado; a mudança é de configuração e código | nenhum identificado | nenhuma ação necessária | baixa |
Evolução em níveis: o que muda, e o que passa a doer
A terceira arquitetura não é um desenho: é a resposta a QUANDO trocar de desenho. Cada nível resolve um risco e compra outro.
Cookie de sessão em memória, uma task, login funcionando por suposição de servidor único. É onde a Loja Doze estava.Pool de usuários do Cognito, PKCE, validação local de JWT com checagem de `token_use` e `client_id`, token de acesso curto.Escopos e grupos do Cognito viram políticas de autorização reais na aplicação, com auditoria de quem pode o quê.Autorizador JWT no API Gateway (quando a banda evolui para lá, L26) valida antes de a requisição chegar à aplicação.Isolamento entre lojas por linha, schema ou conta, e login federado com provedor de identidade da empresa cliente (L38).Autenticação adaptativa: risco calculado por padrão de acesso (dispositivo, geografia, horário) decide se pede segundo fator.A ordem não é negociável, e o motivo é concreto
Autorização fina (nível 3) depende de o token já carregar escopo e grupo de forma confiável — que depende da validação correta do nível 2. Quem tenta multi-tenant (nível 5) sem ter a validação básica de claims resolvida monta isolamento sobre uma identidade que ainda pode ser forjada pelo bug do nível anterior.
Onde IA entra nesta arquitetura, e onde não entra
Neste módulo, IA não resolve o problema central, e forçá-la seria o antipadrão que a própria série critica. Validar assinatura, conferir `iss`, `client_id` e `token_use` é aritmética criptográfica determinística — um modelo não melhora nenhuma dessas checagens, e usá-lo aqui trocaria uma decisão binária e auditável por uma probabilística e mais lenta.
Há um lugar onde IA acrescenta valor real em autenticação, e o Cognito já oferece uma versão disso pronta: autenticação baseada em risco, que pontua um login pelo padrão de dispositivo, geografia e horário e decide se exige segundo fator. Não é o assunto deste laboratório — fica para um módulo futuro dedicado a risco e MFA adaptativo.
| Pergunta | Resposta honesta para este módulo |
|---|---|
| Qual problema a IA resolveria? | graduar o risco de um login específico, não validar um token |
| Por que uma regra não bastaria? | para risco simples, uma regra basta — "país novo para este usuário, exigir segundo fator" cobre boa parte. Risco combinando muitos sinais é onde um modelo passa a valer mais que a regra |
| De onde viriam os dados? | CloudTrail de autenticação, histórico de dispositivo e geografia por usuário — o Cognito com autenticação adaptativa já coleta isso |
| Qual o risco? | bloquear usuário legítimo por falso positivo, ou deixar passar sessão comprometida por falso negativo; qualquer um exige caminho de contestação |
| Por que não agora? | a Loja Doze tem 30 lojas e um padrão de acesso estável — não há sinal suficiente para treinar nem para justificar a complexidade ainda |
O uso de IA que parece atraente e é armadilha aqui
Pedir a um modelo para "decidir se este token é válido" substitui uma verificação criptográfica exata — a assinatura RSA confere ou não confere — por um julgamento probabilístico que pode errar com confiança. Segurança de token é exatamente onde a resposta determinística vence: ou a chave pública confirma a assinatura, ou não confirma. IA entra depois, na pergunta "este login parece você", não na pergunta "este token é autêntico".
Anti-padrões deste laboratório
| Anti-padrão | Por que alguém faz | Por que é problema | Sintoma em produção | Forma correta | Quando é aceitável |
|---|---|---|---|---|---|
| Deixar `ValidateAudience = true` com token de acesso do Cognito | é o padrão da biblioteca, e ninguém lê a diferença entre `aud` e `client_id` antes de configurar | o token de acesso do Cognito nunca tem `aud`; a validação falha sempre, não às vezes | 401 em toda chamada autenticada, mesmo com token recém-emitido | `ValidateAudience = false` e checagem manual de `client_id` | nunca — é sempre um bug para este tipo de token |
| Não checar `token_use` | parece redundante: "se o token é válido, por que checar mais uma claim?" | token de identidade e de acesso são igualmente válidos em assinatura; só `token_use` os distingue | API aceita token de identidade como credencial, sem escopo de recurso associado | checar `token_use == "access"` em todo endpoint protegido | nunca; é a única linha que separa os dois tipos de token |
| Sessão pegajosa como correção de sessão em memória | é uma linha de configuração e o sintoma desaparece na hora | mantém o estado no contêiner errado; task que cai desloga quem estava nela | usuário deslogado sem motivo aparente quando uma task é substituída (deploy, AZ, escala) | tirar o estado do contêiner com token, não prender o cliente à task | nunca em produção; tolerável só em protótipo descartável |
| Guardar token de acesso em `localStorage` | é a forma mais simples de persistir entre recarregamentos de página | qualquer script injetado por XSS lê `localStorage` e rouba o token inteiro | sessão sequestrada sem nenhum log de acesso não autorizado à API — o token é legítimo | token em memória da aplicação, com renovação silenciosa via token de atualização protegido | só em protótipo local, nunca com dado real de usuário |
| Copiar a chave pública do JWKS para um arquivo estático | evita uma chamada de rede na primeira requisição e "parece" mais rápido | quando o Cognito rotaciona a chave de assinatura, a cópia fica obsoleta e tudo passa a falhar | 401 em massa, súbito, sem nenhuma mudança de código correspondente no dia | usar `Authority` com o `ConfigurationManager` automático da biblioteca | nunca; o custo de rede é uma chamada por `kid`, não por requisição |
| Validade de token de acesso muito longa "para não incomodar o usuário" | reduz quantas vezes o usuário precisa renovar, e parece melhor experiência | alarga diretamente o atraso máximo de revogação — é o único termo que você controla | conta comprometida continua com acesso válido por horas depois do desligamento | validade curta (minutos), com renovação silenciosa via token de atualização | só se a revogação instantânea for genuinamente irrelevante para o produto |
Quando algo não funciona
| Sintoma | Causa provável | Como investigar | Onde olhar | Correção |
|---|---|---|---|---|
| 401 em toda chamada, mesmo com token recém-emitido | `ValidateAudience = true` com token de acesso, que não tem `aud` | decodifique o payload do token e confira se `aud` existe | log do middleware com o código de erro de audiência | `ValidateAudience = false` e checar `client_id` manualmente |
| 401 só para alguns usuários, sem padrão aparente | token de identidade sendo enviado por engano em vez do de acesso | compare `token_use` no payload do token que está falhando | código do cliente que decide qual token usar no cabeçalho Authorization | usar sempre o token de acesso para chamadas de API |
| Sessão cai sempre que uma task é substituída | estado de sessão ainda em memória, apesar da migração para token | confira se algum caminho antigo de `AddCookie` ainda está ativo | configuração de autenticação no `Program.cs` | remover o esquema de cookie de sessão em memória por completo |
| 401 em massa após um período sem nenhum deploy | rotação de chave de assinatura no pool, com JWKS copiado manualmente e desatualizado | compare o `kid` do token que falha com o conteúdo atual do `jwks_uri` | configuração de `Authority`/cache de chave na task | usar o `ConfigurationManager` automático da biblioteca, nunca cópia manual |
| Revogação parece não ter efeito nenhum | medição feita antes do prazo esperado — o token de acesso ainda não expirou | compare o instante da revogação com `iat + access_token_validity` do token | validade configurada do app client e horário da revogação | nada a corrigir: é o comportamento documentado, não um bug |
| Login funciona, mas toda chamada de API dá 403 | usuário autenticado sem o escopo exigido pela política de autorização | confira a claim `scope` do token contra a policy da rota | `allowed_oauth_scopes` do app client e a definição da `AddPolicy` | conceder o escopo correto no app client ou ajustar a policy da rota |
A pergunta que resolve metade destes casos
O erro é sobre autenticação ou sobre autorização? 401 significa que a identidade não foi estabelecida — token ausente, expirado, mal assinado ou com claim de contexto errada. 403 significa que a identidade FOI estabelecida e não tem permissão para aquela ação. Tratar os dois como "erro de login" atrasa o diagnóstico em toda esta seção.
Limpeza: o que o destroy não leva
Este laboratório acrescenta um recurso que resiste ao terraform destroy de propósito, e um cujo nome fica temporariamente reservado mesmo depois de apagado.
# limpar.sh — a ordem importa, e dois destes recursos sobrevivem ao destroy
# 1. Derrube o que o Terraform administra.
terraform destroy -auto-approve
# 2. DOMINIO DO HOSTED UI: a liberacao do prefixo NAO e imediata. Se voce
# tentar recriar com o mesmo nome minutos depois, a criacao falha com
# "domain already associated" — a AWS ainda esta processando a remocao.
aws cognito-idp describe-user-pool-domain --domain "$PREFIXO_ANTIGO" \
--region us-east-1 2>/dev/null && echo "dominio ainda em liberacao"
# 3. USUARIOS DO POOL: deletion_protection = ACTIVE bloqueia o destroy de
# proposito. Sem desligar, o comando 1 falha e tudo o mais fica de pe.
aws cognito-idp update-user-pool --user-pool-id "$POOL_ID" \
--deletion-protection INACTIVE --region us-east-1
# 4. GRUPO DE LOGS: tem retencao propria e nao pertence ao ciclo do pool
# nem do servico. Fica cobrando ingestao retida depois de tudo apagado.
aws logs delete-log-group --log-group-name /ecs/ffv-lab-api 2>/dev/null || true
# 5. E o que veio do L01 e do L11 e continua cobrando por hora: ALB, RDS,
# NAT Gateway e o Elastic IP dele. Se voce nao vai seguir para o L38,
# rode a limpeza deles tambem.
aws ec2 describe-addresses --query "Addresses[?AssociationId==null].PublicIp" \
--output table
# 6. Prova final: nada com nome do projeto de pe, e o pool realmente sumiu.
aws resourcegroupstaggingapi get-resources \
--tag-filters Key=Projeto,Values=ffv-lab --query "ResourceTagMappingList[].ResourceARN" --output table
aws cognito-idp list-user-pools --max-results 20 \
--query "UserPools[?contains(Name, 'ffv-lab')].Name" --output table
| Recurso | Sai no destroy? | Cobra parado? | Por que fica |
|---|---|---|---|
| Pool de usuários | não, se `deletion_protection = ACTIVE` | não | a proteção existe de propósito; é preciso desligá-la antes do destroy |
| Domínio do Hosted UI | sim, mas com atraso | não | o prefixo fica temporariamente reservado; recriar com o mesmo nome logo depois falha |
| Usuários cadastrados | sim, junto com o pool | não | não há custo por usuário parado — só por usuário ATIVO no mês |
| Grupo de logs | depende de `skip_destroy` | sim, retenção | tem ciclo próprio; sobrevive ao serviço que o alimentava |
| ALB e grupo de destino | sim | sim, por hora | vêm do L01/L11; confira se o listener foi removido antes |
| Elastic IP do NAT (L01/L02) | sim | sim, justamente quando ocioso | cobra por NÃO estar associado; é o campeão de fatura esquecida |
Resumo: problema, peça e motivo
| Problema | Peça | Por que ela, e não outra |
|---|---|---|
| Sessão em memória derruba login ao escalar | token assinado, validado localmente | a identidade viaja com o cliente; nenhuma task precisa lembrar dele |
| Equipe mantendo código de senha | pool de usuários do Cognito | hash, política, MFA e recuperação passam a ser do serviço de identidade |
| Autenticação a cada requisição seria cara | chave pública em cache por `kid` | uma busca por chave, não uma chamada de API por requisição |
| Token de acesso sem `aud` | checagem manual de `client_id` | a validação padrão da biblioteca assume uma claim que este token não tem |
| Token de identidade usado como API key | checagem de `token_use == access` | é a única claim que distingue os dois tipos com a mesma assinatura válida |
| Cliente é uma SPA sem lugar seguro para segredo | app client sem secret + PKCE | PKCE substitui o segredo por um desafio gerado a cada login |
| Revogar precisa de efeito limitado no tempo | validade curta de token de acesso | é o único termo que você controla na fórmula de atraso de revogação |
| Estado escondido em sessão pegajosa | `stickiness` desligada no grupo de destino | é o teste que prova que o estado realmente saiu do contêiner |
| Falha | O que a protege | O que ela NÃO protege |
|---|---|---|
| Escalar de 1 para N tasks derrubando login | token auto-contido, validado em qualquer task | latência de rede na primeira busca de chave — que é pequena e única por task |
| Token de identidade usado como credencial | checagem de `token_use` | cliente que envia o token certo, mas com escopo insuficiente — isso é autorização |
| Conta comprometida com acesso indefinido | validade curta de token de acesso | o token que já está em uso até ele expirar — a revogação não é instantânea |
| Chave de assinatura rotacionada quebrando validação | `Authority` com atualização automática | cópia manual de chave, que este laboratório evita por decisão explícita |
| Token roubado por XSS | nada específico deste laboratório | é a vulnerabilidade de XSS em si que precisa ser corrigida — token em memória reduz a janela, não a elimina |
- O navegador redireciona ao pool de usuários com um desafio PKCE.
- O pool autentica a senha e nunca a entrega à sua aplicação.
- O navegador troca o código de autorização pelos três tokens.
- A requisição de API leva só o token de acesso, no cabeçalho Authorization.
- A task lê o `kid` do cabeçalho do token e busca a chave pública, uma vez, em cache.
- A task confere assinatura, emissor, `client_id` e `token_use` — nessa ordem.
- Autorizado, o pedido segue para a política de escopo da aplicação.
- Qualquer outra task, com o mesmo token, chega ao mesmo resultado.
- Se a conta for desligada, o token em curso continua servindo até expirar.
- A renovação seguinte, com o token de atualização, é onde a revogação finalmente pega.
Perguntas frequentes
❓ Posso usar o token de identidade para chamar minha API?
❓ Por que minha API retorna 401 mesmo com o token de acesso válido?
❓ Quanto tempo um token revogado continua funcionando?
❓ Preciso de pool de identidades além do pool de usuários?
❓ Onde devo guardar o token de acesso no navegador?
❓ Sessão pegajosa no ALB resolve o problema de escalar o login?
❓ O token de acesso maior por causa do jti aumenta o custo?
❓ Preciso de um client secret no cliente de app do Cognito?
Fixando
Você configura o middleware JwtBearer do ASP.NET Core com Authority apontando para o pool de usuários e não mexe em mais nada. Toda chamada com um token de acesso válido retorna 401. Qual é a causa mais provável?
Um usuário é desligado com GlobalSignOut às 14h00. O token de acesso dele, emitido às 13h50 com validade de 1 hora, ainda é apresentado às 14h20 numa chamada validada localmente por assinatura e expiração. O que acontece?
Conhecimentos, próximo módulo e documentação
| Item | Conteúdo |
|---|---|
| Conhecimentos anteriores necessários | L01 no ar; L11 concluído com a decisão de ALB direto; noção básica de OAuth 2.0/OIDC ajuda |
| Conhecimentos adquiridos | os três tokens do Cognito e o que cada um responde; a ausência de aud no token de acesso; as claims obrigatórias na validação local; o trade-off entre validação local e revogação imediata; pool de usuários vs pool de identidades |
| Limitação que fica | revogação não é instantânea — o atraso é a validade do token de acesso; e a decisão de onde o token vive no navegador continua exigindo atenção contínua contra XSS |
| Próximo exemplo recomendado | L38 — multi-tenant: isolamento entre lojas por linha, schema ou conta, usando os grupos e escopos que este laboratório introduziu |
| Também habilitado por este módulo | L26 (API 100% serverless) pode mover a validação para um autorizador de borda |
| Data da última validação técnica | 7 de agosto de 2026 |
Documentação oficial consultada: Verifying JSON web tokens — as claims obrigatórias (iss, aud/client_id, exp, token_use), a estrutura do JWT e a URI do JWKS; Understanding user pool JSON web tokens (JWTs) — o que cada tipo de token carrega e as claims origin_jti/jti quando a revogação está habilitada; e Ending user sessions with token revocation — a frase que fundamenta o núcleo deste laboratório, sobre token revogado continuar válido em validação local. Os valores de preço não aparecem por decisão: use o AWS Pricing Calculator, porque preço varia por região e envelhece mais rápido que o conteúdo.
O que não foi verificado, e você deve conferir na sua conta
O valor padrão de 60 minutos para token de acesso e de identidade, e de 30 dias para o de atualização, é o padrão observado no console em app clients novos — confirme no seu pool, porque a AWS pode alterar padrões entre regiões ou ao longo do tempo. Da mesma forma, os números de latência da seção de provas (0,31 s na primeira chamada, 0,004 s nas seguintes) são de uma medição de exemplo, e servem como ordem de grandeza — meça na sua rede e na sua região antes de usá-los como referência.
Terminou de ler?
Marcar como concluído registra o XP, mantém sua sequência e coloca 3 cartas deste módulo na fila de revisão espaçada.
Próximos passos sugeridos
Temas deste módulo
Discussão
Carregando comentários…