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
{
  "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:

ClaimNomeDescrição
issIssuerEntidade que emitiu o token (ex: https://auth.exemplo.com)
subSubjectIdentificador único do sujeito (ex: UUID do usuário)
audAudienceDestinatário(s) esperado(s) do token — DEVE ser validado
expExpiration TimeTimestamp UNIX após o qual o token é inválido
nbfNot BeforeTimestamp UNIX antes do qual o token NÃO DEVE ser aceito
iatIssued AtTimestamp UNIX de emissão do token
jtiJWT IDIdentificador ú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

AspectoSessionsJWT
EstadoStateful (requer store)Stateless (autocontido)
RevogaçãoInstantânea (delete da sessão)Difícil (requer blocklist ou TTL curto)
EscalabilidadeLimitada pelo storeExcelente (sem lookup server-side)
TamanhoCookie pequeno (~32 bytes)Token grande (~800+ bytes com claims)
Cross-domainComplexo (CORS + cookies)Simples (header Authorization)
SegurançaDados no servidorClaims expostas no payload (base64, NÃO criptografia)
Mobile/APIRequer cookie supportFunciona 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

  1. Computacionalmente caro: custo ajustável (work factor/cost factor)
  2. Memory-hard: requer memória significativa, dificultando ataques com GPUs/ASICs
  3. Salt incorporado: cada hash inclui salt único, prevenindo rainbow tables
  4. 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

AspectoAPI KeysBearer Tokens (JWT/OAuth)
IdentificaAplicação/projetoUsuário ou serviço autenticado
DuraçãoLonga (meses/anos)Curta (minutos/horas)
EscopoGeralmente amploGranular (scopes/permissions)
RevogaçãoManual (regenerar key)Automática (expiração)
Uso principalRate limiting, billing, analyticsAutenticação e autorização
TransporteHeader X-API-Key ou query paramHeader 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

  1. Não confie na rede: valide autenticação em cada serviço, não apenas no perímetro
  2. Princípio do menor privilégio: tokens com scopes mínimos necessários, duração mínima
  3. Verificação contínua: re-valide a postura de segurança ao longo da sessão, não apenas no login
  4. Assuma comprometimento: projete sistemas para conter o blast radius quando (não se) uma credencial vazar
  5. 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