Autenticação JWT e OAuth
Autenticação e Autorização — Fundamentos Formais
Antes de discutir mecanismos, é necessário distinguir dois conceitos que são frequentemente — e incorretamente — usados como sinônimos.
Autenticação (authentication) é o processo de verificar a identidade de uma entidade. Responde à pergunta: “Quem é você?”. Envolve a apresentação e validação de credenciais (senha, certificado, token biométrico).
Autorização (authorization) é o processo de determinar se uma entidade autenticada tem permissão para executar uma ação específica sobre um recurso. Responde à pergunta: “Você tem permissão para fazer isso?”.
Em termos de protocolo HTTP, autenticação tipicamente ocorre no header Authorization ou via cookies de sessão. Autorização é implementada no lado do servidor através de políticas (RBAC, ABAC, ReBAC) que avaliam os atributos do sujeito autenticado contra as regras de acesso do recurso.
Session-Based Authentication
O modelo clássico de autenticação na web é baseado em sessões server-side.
// Fluxo de sessão server-side com Express + Redis
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');
const redisClient = createClient({ url: process.env.REDIS_URL });
redisClient.connect();
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET, // usado para assinar o cookie sid
name: '__Host-sid', // prefixo __Host- força Secure + Path=/
resave: false, // não regravar sessão se não houve alteração
saveUninitialized: false, // não criar sessão para requests não autenticados
cookie: {
httpOnly: true, // inacessível via document.cookie (XSS)
secure: true, // apenas HTTPS
sameSite: 'lax', // proteção contra CSRF em requests cross-origin
maxAge: 1800000, // 30 minutos
domain: undefined, // restrito ao domínio exato (sem subdomínios)
},
}));
Como funciona: após autenticação bem-sucedida, o servidor cria um registro de sessão no store (Redis, PostgreSQL, Memcached), gera um identificador opaco (session ID), e envia esse ID ao cliente via cookie Set-Cookie. Cada request subsequente inclui automaticamente o cookie, e o servidor consulta o store para recuperar os dados da sessão.
Vantagens: revogação instantânea (basta deletar a sessão do store), dados sensíveis nunca saem do servidor, tamanho mínimo do cookie (apenas o session ID).
Desvantagens: estado server-side (requer store compartilhado em arquiteturas distribuídas), latência adicional por lookup no store a cada request, complexidade de scaling horizontal.
Proteção CSRF em sessões
Cookies são enviados automaticamente pelo browser em qualquer request para o domínio — incluindo requests originados de sites maliciosos. Isso possibilita ataques CSRF (Cross-Site Request Forgery).
// Implementação de CSRF token com double-submit cookie pattern
const crypto = require('crypto');
function generateCsrfToken() {
return crypto.randomBytes(32).toString('hex');
}
// Middleware: gera token e inclui em cookie + header
app.use((req, res, next) => {
if (req.method === 'GET') {
const token = generateCsrfToken();
req.session.csrfToken = token;
res.cookie('csrf-token', token, { httpOnly: false, sameSite: 'strict' });
}
next();
});
// Middleware: valida token em requests de mutação
app.use((req, res, next) => {
if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(req.method)) {
const headerToken = req.headers['x-csrf-token'];
const sessionToken = req.session.csrfToken;
if (!headerToken || !sessionToken ||
!crypto.timingSafeEqual(Buffer.from(headerToken), Buffer.from(sessionToken))) {
return res.status(403).json({ error: 'CSRF_TOKEN_INVALID' });
}
}
next();
});
O uso de crypto.timingSafeEqual é crítico: comparações com === são vulneráveis a timing attacks, onde um atacante pode inferir bytes corretos medindo o tempo de resposta.
JWT — JSON Web Token (RFC 7519)
JWT é um formato compacto e URL-safe para representar claims entre duas partes. A estrutura é:
BASE64URL(header) + "." + BASE64URL(payload) + "." + SIGNATURE
Header
{
"alg": "RS256",
"typ": "JWT",
"kid": "2024-01-signing-key"
}
O campo kid (Key ID) é essencial em sistemas que fazem rotação de chaves — permite ao verificador identificar qual chave pública usar sem tentar todas.
Payload (Claims)
Claims registradas conforme RFC 7519, seção 4.1:
| Claim | Nome | Descrição |
|---|---|---|
iss | Issuer | Entidade que emitiu o token (ex: https://auth.exemplo.com) |
sub | Subject | Identificador único do sujeito (ex: UUID do usuário) |
aud | Audience | Destinatário(s) esperado(s) do token — DEVE ser validado |
exp | Expiration Time | Timestamp UNIX após o qual o token é inválido |
nbf | Not Before | Timestamp UNIX antes do qual o token NÃO DEVE ser aceito |
iat | Issued At | Timestamp UNIX de emissão do token |
jti | JWT ID | Identificador único do token — usado para prevenir replay attacks |
{
"iss": "https://auth.exemplo.com",
"sub": "550e8400-e29b-41d4-a716-446655440000",
"aud": "https://api.exemplo.com",
"exp": 1704067200,
"iat": 1704066300,
"jti": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"role": "admin",
"permissions": ["users:read", "users:write"]
}
Algoritmos de Assinatura
const jwt = require('jsonwebtoken');
const fs = require('fs');
// HS256 — HMAC-SHA256 (simétrico)
// A MESMA chave assina e verifica. Simples, mas a chave precisa ser
// compartilhada entre emissor e verificador — inviável em arquiteturas
// com múltiplos serviços independentes.
const accessTokenHS256 = jwt.sign(payload, process.env.JWT_SECRET, {
algorithm: 'HS256',
expiresIn: '15m',
});
// RS256 — RSA-SHA256 (assimétrico)
// Chave privada assina, chave pública verifica. Ideal para microsserviços:
// o auth server mantém a chave privada; serviços consumidores usam a
// chave pública (distribuída via JWKS endpoint) para validar tokens.
// Tamanho de chave mínimo recomendado: 2048 bits (4096 para longa duração).
const privateKey = fs.readFileSync('./keys/rsa-private.pem');
const accessTokenRS256 = jwt.sign(payload, privateKey, {
algorithm: 'RS256',
expiresIn: '15m',
keyid: '2024-01-rsa',
});
// ES256 — ECDSA com curva P-256 (assimétrico)
// Mesma segurança do RS256 com chaves e tokens significativamente menores.
// Uma chave ECDSA P-256 (256 bits) oferece segurança comparável a RSA 3072 bits.
// Preferido para ambientes com restrição de bandwidth (mobile, IoT).
const ecPrivateKey = fs.readFileSync('./keys/ec-private.pem');
const accessTokenES256 = jwt.sign(payload, ecPrivateKey, {
algorithm: 'ES256',
expiresIn: '15m',
keyid: '2024-01-ec',
});
Regra prática de escolha: use ES256 como padrão para novos sistemas. Use RS256 quando há requisitos de compatibilidade com sistemas legados. Use HS256 apenas em cenários monolíticos onde a chave nunca precisa ser compartilhada.
JWKS — JSON Web Key Set
Em arquiteturas com RS256/ES256, as chaves públicas são distribuídas via um endpoint JWKS:
// GET https://auth.exemplo.com/.well-known/jwks.json
// Resposta:
{
"keys": [
{
"kty": "EC",
"crv": "P-256",
"kid": "2024-01-ec",
"use": "sig",
"x": "f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU",
"y": "x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0"
}
]
}
// Verificação de token com JWKS no serviço consumidor
const jwksClient = require('jwks-rsa');
const client = jwksClient({
jwksUri: 'https://auth.exemplo.com/.well-known/jwks.json',
cache: true, // cache de chaves em memória
cacheMaxAge: 600000, // 10 minutos
rateLimit: true, // previne abuse do endpoint JWKS
});
function getSigningKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
if (err) return callback(err);
callback(null, key.getPublicKey());
});
}
// Middleware de verificação
function verifyJWT(req, res, next) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) return res.status(401).json({ error: 'TOKEN_MISSING' });
jwt.verify(token, getSigningKey, {
algorithms: ['ES256', 'RS256'], // whitelist explícita de algoritmos
issuer: 'https://auth.exemplo.com',
audience: 'https://api.exemplo.com',
clockTolerance: 30, // 30s de tolerância para clock skew
}, (err, decoded) => {
if (err) {
const errorMap = {
'TokenExpiredError': { status: 401, code: 'TOKEN_EXPIRED' },
'JsonWebTokenError': { status: 401, code: 'TOKEN_INVALID' },
'NotBeforeError': { status: 401, code: 'TOKEN_NOT_ACTIVE' },
};
const mapped = errorMap[err.name] || { status: 401, code: 'AUTH_FAILED' };
return res.status(mapped.status).json({ error: mapped.code });
}
req.user = decoded;
next();
});
}
A whitelist de algoritmos (algorithms: ['ES256', 'RS256']) é uma defesa crítica contra o ataque de algorithm confusion, onde um atacante altera o header do JWT para "alg": "none" ou troca de assimétrico para simétrico, usando a chave pública como secret HMAC.
JWT vs Sessions — Trade-offs
| Aspecto | Sessions | JWT |
|---|---|---|
| Estado | Stateful (requer store) | Stateless (autocontido) |
| Revogação | Instantânea (delete da sessão) | Difícil (requer blocklist ou TTL curto) |
| Escalabilidade | Limitada pelo store | Excelente (sem lookup server-side) |
| Tamanho | Cookie pequeno (~32 bytes) | Token grande (~800+ bytes com claims) |
| Cross-domain | Complexo (CORS + cookies) | Simples (header Authorization) |
| Segurança | Dados no servidor | Claims expostas no payload (base64, NÃO criptografia) |
| Mobile/API | Requer cookie support | Funciona em qualquer client |
Decisão arquitetural: para aplicações web monolíticas com browser como único client, sessions com Redis são mais simples e seguros. Para APIs consumidas por múltiplos clients (SPA, mobile, serviços), JWT com refresh tokens é a escolha pragmática.
Refresh Tokens — Rotação e Detecção de Roubo
Refresh tokens resolvem o dilema de segurança: access tokens curtos (15min) minimizam o impacto de vazamento, mas refresh tokens longos (7-30 dias) são alvos valiosos. A mitigação exige rotação de tokens.
const crypto = require('crypto');
// Modelo de dados para refresh tokens
// CREATE TABLE refresh_tokens (
// id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
// user_id UUID NOT NULL REFERENCES users(id),
// token_hash VARCHAR(64) NOT NULL UNIQUE,
// family_id UUID NOT NULL, -- agrupa tokens da mesma cadeia
// is_revoked BOOLEAN DEFAULT FALSE,
// expires_at TIMESTAMPTZ NOT NULL,
// created_at TIMESTAMPTZ DEFAULT NOW(),
// replaced_by UUID REFERENCES refresh_tokens(id)
// );
async function rotateRefreshToken(oldRefreshToken) {
// 1. Hash do token recebido para busca no banco
const tokenHash = crypto
.createHash('sha256')
.update(oldRefreshToken)
.digest('hex');
const storedToken = await db.query(
'SELECT * FROM refresh_tokens WHERE token_hash = $1',
[tokenHash]
);
if (!storedToken) {
throw new AuthError('REFRESH_TOKEN_NOT_FOUND');
}
// 2. DETECÇÃO DE ROUBO: se o token já foi revogado, significa que
// alguém está usando um token antigo — possivelmente roubado.
// Revoga TODA a família de tokens como medida de segurança.
if (storedToken.is_revoked) {
await db.query(
'UPDATE refresh_tokens SET is_revoked = TRUE WHERE family_id = $1',
[storedToken.family_id]
);
// Alerta de segurança — possível comprometimento da conta
await securityAlerts.notify(storedToken.user_id, 'REFRESH_TOKEN_REUSE');
throw new AuthError('TOKEN_FAMILY_REVOKED');
}
// 3. Verificar expiração
if (new Date(storedToken.expires_at) < new Date()) {
throw new AuthError('REFRESH_TOKEN_EXPIRED');
}
// 4. Gerar novo refresh token (rotação)
const newRefreshToken = crypto.randomBytes(64).toString('base64url');
const newTokenHash = crypto
.createHash('sha256')
.update(newRefreshToken)
.digest('hex');
// 5. Revogar o token antigo e criar o novo, atomicamente
await db.transaction(async (tx) => {
await tx.query(
'UPDATE refresh_tokens SET is_revoked = TRUE, replaced_by = $1 WHERE id = $2',
[newTokenHash, storedToken.id]
);
await tx.query(
`INSERT INTO refresh_tokens (user_id, token_hash, family_id, expires_at)
VALUES ($1, $2, $3, NOW() + INTERVAL '7 days')`,
[storedToken.user_id, newTokenHash, storedToken.family_id]
);
});
// 6. Gerar novo access token
const accessToken = jwt.sign(
{ sub: storedToken.user_id },
ecPrivateKey,
{ algorithm: 'ES256', expiresIn: '15m' }
);
return { accessToken, refreshToken: newRefreshToken };
}
O conceito de token family é fundamental: todos os refresh tokens gerados a partir de um login inicial compartilham o mesmo family_id. Quando um token já revogado é reutilizado, a família inteira é invalidada. Isso detecta cenários onde um atacante roubou um refresh token e tenta usá-lo após o usuário legítimo já ter feito a rotação.
OAuth 2.0 — Flows e Quando Usar Cada Um
OAuth 2.0 (RFC 6749) é um framework de delegação de autorização — permite que um usuário conceda a uma aplicação terceira acesso limitado aos seus recursos sem compartilhar credenciais.
Authorization Code Flow + PKCE (RFC 7636)
O flow mais seguro para aplicações com interação do usuário. PKCE (Proof Key for Code Exchange) é obrigatório para SPAs e apps nativos (OAuth 2.1 o torna obrigatório para todos os clients).
const crypto = require('crypto');
// --- PASSO 1: Cliente gera code_verifier e code_challenge ---
// code_verifier: string aleatória de 43-128 caracteres
const codeVerifier = crypto.randomBytes(64).toString('base64url');
// code_challenge: SHA256(code_verifier), enviado no request de autorização
const codeChallenge = crypto
.createHash('sha256')
.update(codeVerifier)
.digest('base64url');
// --- PASSO 2: Redirect para o Authorization Server ---
const authUrl = new URL('https://auth.exemplo.com/authorize');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', 'minha-spa');
authUrl.searchParams.set('redirect_uri', 'https://app.exemplo.com/callback');
authUrl.searchParams.set('scope', 'openid profile email');
authUrl.searchParams.set('state', crypto.randomBytes(16).toString('hex')); // anti-CSRF
authUrl.searchParams.set('code_challenge', codeChallenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
// --- PASSO 3: Após autenticação, o auth server redireciona com ?code=... ---
// --- PASSO 4: Cliente troca o code por tokens ---
async function exchangeCodeForTokens(authorizationCode) {
const response = await fetch('https://auth.exemplo.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: authorizationCode,
redirect_uri: 'https://app.exemplo.com/callback',
client_id: 'minha-spa',
code_verifier: codeVerifier, // prova que quem iniciou o flow é quem está trocando
}),
});
// Resposta contém access_token, refresh_token, id_token (se OpenID Connect)
return response.json();
}
PKCE impede o ataque de authorization code interception: mesmo que um atacante intercepte o code no redirect, ele não consegue trocá-lo por tokens sem o code_verifier original.
Client Credentials Flow
Para comunicação machine-to-machine (serviço-a-serviço), sem envolvimento de usuário.
// Serviço A precisa chamar Serviço B
async function getServiceToken() {
const response = await fetch('https://auth.exemplo.com/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
// client_id:client_secret em Base64 no header Authorization
'Authorization': `Basic ${Buffer.from(
`${process.env.CLIENT_ID}:${process.env.CLIENT_SECRET}`
).toString('base64')}`,
},
body: new URLSearchParams({
grant_type: 'client_credentials',
scope: 'service:read service:write',
}),
});
return response.json(); // { access_token, token_type, expires_in }
}
Device Code Flow (RFC 8628)
Para dispositivos com input limitado (Smart TVs, CLIs, IoT). O dispositivo exibe um código curto; o usuário o digita em outro dispositivo com browser.
Dispositivo → POST /device/code
Resposta: { device_code, user_code: "ABCD-1234", verification_uri }
Dispositivo exibe: "Acesse https://auth.exemplo.com/device e digite ABCD-1234"
Dispositivo faz polling → POST /token com device_code
Até o usuário autorizar ou o código expirar.
OpenID Connect (OIDC)
OIDC é uma camada de identidade construída sobre OAuth 2.0. Enquanto OAuth 2.0 lida apenas com autorização (acesso a recursos), OIDC adiciona autenticação (identidade do usuário).
ID Token
O ID Token é um JWT que contém claims sobre a identidade do usuário autenticado:
{
"iss": "https://auth.exemplo.com",
"sub": "550e8400-e29b-41d4-a716-446655440000",
"aud": "minha-spa",
"exp": 1704067200,
"iat": 1704066300,
"nonce": "abc123def456",
"name": "João Silva",
"email": "joao@exemplo.com",
"email_verified": true,
"at_hash": "HK6E_P6Dh8Y93mRNtsDB1Q"
}
O campo at_hash é o hash da primeira metade do access token — vincula o ID token ao access token emitido junto, prevenindo token substitution.
Discovery Document
Todo provedor OIDC expõe um documento de descoberta em /.well-known/openid-configuration:
GET https://auth.exemplo.com/.well-known/openid-configuration
{
"issuer": "https://auth.exemplo.com",
"authorization_endpoint": "https://auth.exemplo.com/authorize",
"token_endpoint": "https://auth.exemplo.com/token",
"userinfo_endpoint": "https://auth.exemplo.com/userinfo",
"jwks_uri": "https://auth.exemplo.com/.well-known/jwks.json",
"scopes_supported": ["openid", "profile", "email"],
"response_types_supported": ["code"],
"id_token_signing_alg_values_supported": ["RS256", "ES256"]
}
Isso permite que clients se configurem automaticamente — fundamental para federação de identidade em larga escala.
Armazenamento Seguro de Tokens
A escolha de onde armazenar tokens no client é uma das decisões de segurança mais impactantes.
┌──────────────────────────────────────────────────────────────────┐
│ COMPARAÇÃO DE ARMAZENAMENTO NO BROWSER │
├─────────────────────┬────────────────────┬───────────────────────┤
│ │ httpOnly Cookie │ localStorage │
├─────────────────────┼────────────────────┼───────────────────────┤
│ Acessível via JS │ NÃO │ SIM │
│ Vulnerável a XSS │ NÃO (token) │ SIM (token exposto) │
│ Vulnerável a CSRF │ SIM (mitigável) │ NÃO │
│ Enviado automático │ SIM (pelo browser) │ NÃO (manual) │
│ Cross-origin │ Restrito │ Restrito (SOP) │
│ Tamanho máximo │ ~4KB │ ~5-10MB │
└─────────────────────┴────────────────────┴───────────────────────┘
Recomendação: refresh tokens SEMPRE em cookies httpOnly + Secure + SameSite=Strict. Access tokens de curta duração podem ser mantidos em memória (variável JavaScript) — não sobrevivem a refresh da página, mas isso é resolvido pelo silent refresh via refresh token cookie.
// Configuração segura de cookie para refresh token
res.cookie('__Host-refresh', refreshToken, {
httpOnly: true, // impede acesso via document.cookie
secure: true, // transmissão apenas via HTTPS
sameSite: 'strict', // não enviado em requests cross-origin
path: '/api/auth', // restrito apenas às rotas de autenticação
maxAge: 604800000, // 7 dias em milissegundos
// __Host- prefix: força Secure=true, Path=/, sem Domain
// O browser rejeita o cookie se essas condições não forem atendidas
});
O prefixo __Host- no nome do cookie é uma defesa adicional: o browser recusa o cookie se Secure não for true ou se Domain estiver definido. Isso previne ataques de cookie fixation via subdomínios comprometidos.
Password Hashing — Por Que NUNCA SHA256
Funções de hash criptográficas genéricas (SHA-256, SHA-512, MD5) são projetadas para serem rápidas. Isso é exatamente o oposto do que se deseja para hashing de senhas: um atacante com GPU moderna calcula bilhões de SHA-256 por segundo.
Propriedades de um bom algoritmo de password hashing
- Computacionalmente caro: custo ajustável (work factor/cost factor)
- Memory-hard: requer memória significativa, dificultando ataques com GPUs/ASICs
- Salt incorporado: cada hash inclui salt único, prevenindo rainbow tables
- Resistente a paralelismo: dificulta ataques massivamente paralelos
Comparação de algoritmos
const bcrypt = require('bcrypt');
const argon2 = require('argon2');
// --- bcrypt ---
// Cost factor 12 = 2^12 = 4096 iterações internas
// Cada incremento de 1 DOBRA o tempo de computação
// Limitação: trunca senhas em 72 bytes
const BCRYPT_COST = 12;
const bcryptHash = await bcrypt.hash(password, BCRYPT_COST);
// Resultado: $2b$12$LJ3m4ys3Lg.Ry5i3s5F8eOYHzWAOL.XRlZtx/7pMYBh9J7Xk6q
const isValid = await bcrypt.compare(candidatePassword, bcryptHash);
// --- Argon2id (recomendação OWASP atual) ---
// Argon2 é o vencedor do Password Hashing Competition (2015)
// Argon2id combina resistência a side-channel attacks (Argon2i)
// e resistência a GPU cracking (Argon2d)
const argon2Hash = await argon2.hash(password, {
type: argon2.argon2id, // variante híbrida (recomendada)
memoryCost: 65536, // 64MB de memória — força o atacante a alocar
timeCost: 3, // 3 iterações
parallelism: 4, // 4 threads paralelas
hashLength: 32, // tamanho do hash resultante em bytes
saltLength: 16, // salt de 128 bits
});
// Resultado: $argon2id$v=19$m=65536,t=3,p=4$c29tZXNhbHQ$hash...
const isValidArgon2 = await argon2.verify(argon2Hash, candidatePassword);
Parâmetros recomendados pela OWASP (2024): Argon2id com memória >= 19 MiB, iterações >= 2, paralelismo = 1. Ajuste para que cada hash leve ~250ms-1s no seu hardware de produção.
Por que SHA256 é inadequado: um atacante com 8 GPUs RTX 4090 calcula ~100 bilhões de SHA256/s. Uma senha de 8 caracteres alfanuméricos (62^8 = ~218 trilhões de combinações) seria quebrada por força bruta em ~36 minutos. Com Argon2id a 250ms por tentativa, a mesma senha levaria ~1.7 milhões de anos.
Multi-Factor Authentication (MFA)
TOTP — Time-Based One-Time Password (RFC 6238)
TOTP gera códigos de 6-8 dígitos baseados em um segredo compartilhado e o tempo atual.
const { authenticator } = require('otplib');
// Geração do segredo (durante setup do MFA)
const secret = authenticator.generateSecret(); // base32 encoded
// Resultado: "JBSWY3DPEHPK3PXP"
// Gerar URI para QR code (compatível com Google Authenticator, Authy, etc.)
const otpauthUri = authenticator.keyuri(
'joao@exemplo.com',
'MinhaApp',
secret
);
// otpauth://totp/MinhaApp:joao@exemplo.com?secret=JBSWY3DPEHPK3PXP&issuer=MinhaApp
// Verificação do código TOTP durante login
function verifyTOTP(userSecret, submittedCode) {
// authenticator.check internamente aceita window de ±1 step (±30s)
// para compensar dessincronização de relógio
const isValid = authenticator.check(submittedCode, userSecret);
if (!isValid) {
// Implementar rate limiting: máximo de 5 tentativas por minuto
// para prevenir brute force (10^6 combinações para 6 dígitos)
throw new AuthError('INVALID_TOTP_CODE');
}
return true;
}
WebAuthn / FIDO2 / Passkeys
WebAuthn é o padrão W3C para autenticação forte baseada em criptografia assimétrica. O dispositivo do usuário gera um par de chaves; a chave privada nunca sai do dispositivo.
// Servidor: gerar opções de registro
const { generateRegistrationOptions, verifyRegistrationResponse } =
require('@simplewebauthn/server');
const registrationOptions = await generateRegistrationOptions({
rpName: 'Minha App',
rpID: 'exemplo.com',
userID: user.id,
userName: user.email,
attestationType: 'none', // simplifica — sem attestation de hardware
authenticatorSelection: {
residentKey: 'preferred', // suporte a passkeys (discoverable credentials)
userVerification: 'preferred', // biometria/PIN quando disponível
},
excludeCredentials: existingCredentials, // previne registro duplicado
});
// O browser chama navigator.credentials.create() com essas opções
// O autenticador (biometria, security key, passkey na nuvem) gera o par de chaves
// O servidor recebe e verifica a resposta:
const verification = await verifyRegistrationResponse({
response: clientResponse,
expectedChallenge: storedChallenge,
expectedOrigin: 'https://exemplo.com',
expectedRPID: 'exemplo.com',
});
Passkeys são a evolução de WebAuthn: credentials sincronizáveis entre dispositivos via iCloud Keychain, Google Password Manager ou Windows Hello. Eliminam completamente a necessidade de senhas.
API Keys vs Bearer Tokens
| Aspecto | API Keys | Bearer Tokens (JWT/OAuth) |
|---|---|---|
| Identifica | Aplicação/projeto | Usuário ou serviço autenticado |
| Duração | Longa (meses/anos) | Curta (minutos/horas) |
| Escopo | Geralmente amplo | Granular (scopes/permissions) |
| Revogação | Manual (regenerar key) | Automática (expiração) |
| Uso principal | Rate limiting, billing, analytics | Autenticação e autorização |
| Transporte | Header X-API-Key ou query param | Header Authorization: Bearer |
// Middleware que combina API key (identificação) + Bearer token (autenticação)
async function apiMiddleware(req, res, next) {
// 1. Verificar API key para rate limiting e billing
const apiKey = req.headers['x-api-key'];
if (!apiKey) {
return res.status(401).json({ error: 'API_KEY_REQUIRED' });
}
const apiKeyHash = crypto.createHash('sha256').update(apiKey).digest('hex');
const keyRecord = await db.query(
'SELECT * FROM api_keys WHERE key_hash = $1 AND is_active = TRUE',
[apiKeyHash]
);
if (!keyRecord) {
return res.status(401).json({ error: 'INVALID_API_KEY' });
}
// Rate limiting baseado na API key (não no IP — que pode ser compartilhado)
const rateLimitKey = `rate:${keyRecord.id}`;
const requests = await redis.incr(rateLimitKey);
if (requests === 1) await redis.expire(rateLimitKey, 60);
if (requests > keyRecord.rate_limit_per_minute) {
res.set('Retry-After', '60');
return res.status(429).json({ error: 'RATE_LIMIT_EXCEEDED' });
}
// 2. Verificar Bearer token para autenticação do usuário
const token = req.headers.authorization?.replace('Bearer ', '');
if (token) {
try {
req.user = jwt.verify(token, publicKey, {
algorithms: ['ES256'],
issuer: 'https://auth.exemplo.com',
});
} catch {
return res.status(401).json({ error: 'INVALID_TOKEN' });
}
}
req.apiKey = keyRecord;
next();
}
Segurança de API keys: armazene apenas o hash (SHA-256) da key no banco. Exiba a key completa apenas uma vez no momento da criação. Isso segue o mesmo princípio de senhas — se o banco for comprometido, as keys não são expostas.
Zero-Trust Architecture — Autenticação em Profundidade
O modelo zero-trust opera sob o princípio “never trust, always verify”. Nenhum request é considerado confiável baseado apenas na sua origem de rede.
mTLS — Mutual TLS
Em TLS padrão, apenas o servidor apresenta certificado. Em mTLS, ambas as partes (client e server) se autenticam mutuamente via certificados X.509.
Fluxo mTLS:
1. Client inicia TLS handshake
2. Server apresenta seu certificado → client valida contra CA
3. Server solicita certificado do client (CertificateRequest)
4. Client apresenta seu certificado → server valida contra CA
5. Canal autenticado bidirecional estabelecido
Caso de uso: comunicação serviço-a-serviço em service mesh
# Exemplo: Istio PeerAuthentication — exige mTLS em todo o namespace
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
name: default
namespace: production
spec:
mtls:
mode: STRICT # rejeita qualquer conexão sem mTLS válido
JWT Validation — Gateway vs Service Level
Em arquiteturas com API Gateway, há dois modelos de validação:
Modelo 1: Validação apenas no gateway (mais simples, menos seguro)
┌────────┐ ┌─────────┐ ┌──────────┐
│ Client ├────►│ Gateway ├────►│ Serviço │
│ │ JWT │ valida │ ──► │ confia │
│ │ │ JWT │ │ no GW │
└────────┘ └─────────┘ └──────────┘
Risco: se um atacante bypassa o gateway, o serviço aceita qualquer request.
Modelo 2: Validação no gateway + no serviço (defense in depth)
┌────────┐ ┌─────────┐ ┌──────────┐
│ Client ├────►│ Gateway ├────►│ Serviço │
│ │ JWT │ valida │ JWT │ re-valida│
│ │ │ JWT │ ──► │ JWT │
└────────┘ └─────────┘ └──────────┘
O gateway pode adicionar headers internos (X-User-Id, X-User-Roles)
mas o serviço TAMBÉM valida o JWT original como defesa em profundidade.
// Middleware de validação em serviço (defense in depth)
function serviceAuthMiddleware(req, res, next) {
// Mesmo que o gateway já tenha validado, re-validar localmente
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'TOKEN_MISSING' });
}
try {
const decoded = jwt.verify(token, publicKey, {
algorithms: ['ES256'],
issuer: 'https://auth.exemplo.com',
audience: 'https://api.exemplo.com',
});
// Validações adicionais específicas do serviço
if (!decoded.permissions?.includes('orders:read')) {
return res.status(403).json({ error: 'INSUFFICIENT_PERMISSIONS' });
}
req.user = decoded;
next();
} catch (err) {
return res.status(401).json({ error: 'TOKEN_INVALID' });
}
}
Princípios Zero-Trust para APIs
- Não confie na rede: valide autenticação em cada serviço, não apenas no perímetro
- Princípio do menor privilégio: tokens com scopes mínimos necessários, duração mínima
- Verificação contínua: re-valide a postura de segurança ao longo da sessão, não apenas no login
- Assuma comprometimento: projete sistemas para conter o blast radius quando (não se) uma credencial vazar
- Criptografe em trânsito e em repouso: mTLS entre serviços, secrets em vaults (HashiCorp Vault, AWS Secrets Manager)
Checklist de Segurança — Implementação em Produção
AUTENTICAÇÃO
✓ Senhas hasheadas com Argon2id ou bcrypt (cost >= 12)
✓ Rate limiting em endpoints de login (ex: 5 tentativas/minuto)
✓ Timing-safe comparison para tokens e hashes
✓ MFA oferecido (TOTP ou WebAuthn) para contas sensíveis
TOKENS
✓ Access tokens: curta duração (5-15 minutos)
✓ Refresh tokens: rotação a cada uso, com detecção de reuso
✓ Whitelist explícita de algoritmos na verificação de JWT
✓ Claims iss, aud, exp SEMPRE validadas
✓ jti para prevenir replay attacks em operações sensíveis
ARMAZENAMENTO
✓ Refresh tokens em cookies httpOnly + Secure + SameSite
✓ Prefixo __Host- em cookies de segurança
✓ NUNCA armazenar tokens sensíveis em localStorage
✓ API keys armazenadas como hash no banco de dados
INFRAESTRUTURA
✓ HTTPS obrigatório (HSTS header com includeSubDomains)
✓ mTLS entre serviços em ambientes de produção
✓ Rotação periódica de chaves de assinatura (com suporte a kid/JWKS)
✓ Secrets gerenciados por vault, não em variáveis de ambiente hardcoded
✓ Logs de autenticação para auditoria (sem logar tokens ou senhas)
Referencias e Fontes
- RFC 7519 (JWT) — https://datatracker.ietf.org/doc/html/rfc7519 — Especificacao formal do JSON Web Token, definindo estrutura, claims e assinatura
- RFC 6749 (OAuth 2.0) — https://datatracker.ietf.org/doc/html/rfc6749 — Framework de autorizacao OAuth 2.0, base para flows de autenticacao modernos
- OWASP Authentication Cheat Sheet — https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html — Guia de boas praticas de autenticacao, incluindo armazenamento de senhas e protecao contra ataques