Implementar API Keys no Node.js é uma forma comum de autenticar integrações entre sistemas, scripts, parceiros e serviços internos. Uma chave de API funciona como uma credencial: quem a possui pode realizar ações dentro do escopo concedido.
O problema é que muitas implementações tratam a chave como um simples texto salvo no banco. Isso facilita vazamentos, dificulta rotação e torna a revogação pouco confiável. Uma solução segura usa valores aleatórios, prefixos identificáveis, hash no banco, escopos, expiração, limites, auditoria e rotação sem interrupção.
Neste guia, você aprenderá a gerar chaves, armazenar somente o hash, identificar credenciais, validar com segurança, limitar permissões, aplicar rate limiting, rotacionar, revogar e testar o fluxo.
O que é uma API Key?
Uma API key é uma credencial emitida para uma aplicação, integração ou usuário técnico. Ela pode identificar quem chama a API e quais ações estão permitidas.
A OWASP API Security reúne riscos importantes para APIs. A RFC 6750 descreve o uso de bearer tokens e os riscos de credenciais que concedem acesso a quem as apresenta.
API key não é senha de usuário
Uma chave de API normalmente representa um cliente técnico, não uma pessoa. Ela não deve substituir autenticação de usuários em interfaces comuns, especialmente quando MFA, recuperação e gerenciamento de sessões são necessários.
Para usuários finais, consulte Sessões Seguras no Node.js e Passkeys no Node.js.
Formato da chave
Use um prefixo legível e uma parte secreta aleatória:
cf_live_4F8uYj...valor_aleatorio...O prefixo ajuda a identificar o ambiente e detectar vazamentos em repositórios, sem reduzir significativamente a segurança quando a parte secreta possui alta entropia.
Gerando uma chave
import { randomBytes } from 'node:crypto';
export function generateApiKey(environment = 'live') {
const secret = randomBytes(32).toString('base64url');
return `cf_${environment}_${secret}`;
}Use uma fonte criptograficamente segura. Não use timestamps, IDs incrementais ou UUIDs previsíveis.
Separando identificador e segredo
Uma estrutura útil contém um ID curto público e um segredo:
cf_live_k7W2.aV9...segredo...A parte antes do ponto localiza o registro no banco. A parte depois é verificada por hash.
Armazenando somente o hash
import { createHash } from 'node:crypto';
export function hashApiKey(secret) {
return createHash('sha256')
.update(secret, 'utf8')
.digest('hex');
}Como a chave é longa e aleatória, SHA-256 pode ser usado para indexar o segredo. Diferentemente de senhas humanas, a chave possui entropia suficiente para tornar ataques de dicionário impraticáveis.
Tabela de chaves
CREATE TABLE api_keys (
id UUID PRIMARY KEY,
key_prefix TEXT NOT NULL UNIQUE,
secret_hash TEXT NOT NULL UNIQUE,
owner_type TEXT NOT NULL,
owner_id UUID NOT NULL,
name TEXT NOT NULL,
status TEXT NOT NULL,
scopes TEXT[] NOT NULL,
created_at TIMESTAMPTZ NOT NULL,
expires_at TIMESTAMPTZ,
last_used_at TIMESTAMPTZ,
revoked_at TIMESTAMPTZ,
created_by UUID,
rotated_from UUID
);Não armazene a chave completa. Mostre o valor apenas uma vez, imediatamente após a criação.
Mostrando apenas uma vez
{
"id": "...",
"name": "Integração ERP",
"apiKey": "cf_live_k7W2.aV9...",
"warning": "Copie agora. A chave não será mostrada novamente."
}Depois, a interface pode exibir apenas o prefixo e os últimos caracteres não secretos.
Recebendo a chave
Prefira o header Authorization:
Authorization: Bearer cf_live_k7W2.aV9...Também é possível usar um header específico, como X-API-Key. Evite query string, pois URLs aparecem em logs, histórico, analytics e proxies.
Extraindo o header
function extractBearerToken(req) {
const header = req.headers.authorization;
if (!header?.startsWith('Bearer ')) {
return null;
}
return header.slice(7).trim();
}Validação
async function authenticateApiKey(rawKey) {
const parsed = parseApiKey(rawKey);
if (!parsed) return null;
const record = await repository.findByPrefix(parsed.prefix);
if (!record || record.status !== 'active') return null;
const providedHash = hashApiKey(parsed.secret);
if (!timingSafeHashEquals(record.secretHash, providedHash)) {
return null;
}
if (record.expiresAt && record.expiresAt < new Date()) {
return null;
}
return record;
}Comparação segura
import { timingSafeEqual } from 'node:crypto';
function timingSafeHashEquals(expectedHex, providedHex) {
const expected = Buffer.from(expectedHex, 'hex');
const provided = Buffer.from(providedHex, 'hex');
return expected.length === provided.length
&& timingSafeEqual(expected, provided);
}A diferença de tempo provavelmente não será a principal defesa, mas a comparação segura evita sinais desnecessários.
Escopos
Não conceda acesso total por padrão. Use permissões específicas:
orders.read
orders.write
customers.read
webhooks.manageUma chave de relatório não precisa modificar pedidos.
Verificando escopo
function requireScope(scope) {
return (req, res, next) => {
if (!req.apiKey.scopes.includes(scope)) {
return res.status(403).json({
code: 'INSUFFICIENT_SCOPE',
message: 'Permissão insuficiente'
});
}
next();
};
}RBAC e API keys
Chaves podem receber roles ou permissions, mas mantenha o modelo explícito. Consulte RBAC no Node.js e ABAC no Node.js.
Multi-tenancy
Uma chave deve estar ligada a um tenant. Todas as consultas precisam usar esse tenant, sem aceitar um valor arbitrário do cliente.
Veja Multi-Tenancy no Node.js e Row-Level Security no Node.js.
Expiração
Defina prazo para chaves temporárias e integrações de terceiros. Para chaves permanentes, exija revisão periódica e rotação.
Rotação sem indisponibilidade
- Crie uma nova chave.
- Mantenha a antiga ativa por curto período.
- Atualize o cliente.
- Confirme uso da nova.
- Revogue a antiga.
- Audite a troca.
Permitir duas chaves ativas por integração reduz interrupções.
Revogação
UPDATE api_keys
SET status = 'revoked',
revoked_at = now()
WHERE id = $1
AND status = 'active';A revogação deve produzir efeito imediato. Evite cache longo de credenciais.
Cache de validação
É possível cachear prefixo, status e escopos por poucos segundos, mas a revogação precisa invalidar o cache. Chaves de alto risco podem ser verificadas diretamente no store.
Rate limiting
Limite por chave, tenant e rota. Uma única integração não deve consumir toda a capacidade.
Consulte Rate Limiting no Node.js.
Quotas
Além de requisições por segundo, aplique quotas diárias, mensais ou por operação cara. Retorne headers ou dados que permitam ao cliente acompanhar o consumo.
Ambientes separados
Use chaves diferentes para desenvolvimento, homologação e produção. O prefixo pode indicar o ambiente:
cf_test_...
cf_live_...Uma chave de teste não deve funcionar em produção.
Restrições de rede
Para integrações estáveis, uma allowlist de IP pode ser uma camada adicional, mas não deve ser a única autenticação. IPs mudam e headers podem ser forjados se proxies não estiverem configurados.
mTLS
Integrações de alto risco podem combinar API key com mutual TLS. Assim, o cliente precisa da chave e do certificado.
Armazenamento no cliente
O consumidor deve manter a chave em Secret, cofre ou variável protegida. Não coloque a credencial em repositórios, bundles frontend ou aplicativos distribuídos.
Frontend público
Qualquer chave embutida no navegador pode ser copiada. Para chamadas privilegiadas, use um backend ou BFF. Consulte Backend for Frontend no Node.js.
Detecção de vazamentos
Prefixos exclusivos facilitam scanners de repositórios. Configure alertas para padrões como cf_live_ e revogue automaticamente quando houver confirmação.
Logs
Nunca registre a chave completa. Faça redaction do header Authorization e de X-API-Key.
logger.info({
apiKeyId: record.id,
keyPrefix: record.keyPrefix,
scope: requiredScope
}, 'Chave autenticada');Auditoria
Registre criação, visualização inicial, rotação, revogação, alteração de escopos e uso anormal. Consulte Logs de Auditoria no Node.js.
Métricas
Monitore requisições por chave de forma controlada, erros, limites, chaves expiradas e tentativas inválidas. Não use a chave como label.
Resposta de autenticação
- 401: chave ausente, inválida, expirada ou revogada.
- 403: chave válida sem escopo.
- 429: limite excedido.
Teste de criação
test('armazena apenas o hash', async () => {
const created = await createApiKey(input);
const row = await repository.findById(created.id);
assert.notEqual(row.secretHash, created.apiKey);
assert.equal(row.secretHash, hashSecret(created.secret));
});Teste de revogação
Autentique, revogue e confirme que a próxima requisição retorna 401.
Teste de escopo
Uma chave com orders.read não deve acessar uma rota que exige orders.write.
Teste de rotação
Durante a janela, ambas funcionam. Depois da revogação, apenas a nova permanece válida.
Teste de logs
Envie uma chave conhecida, gere erro e confirme que o segredo não aparece nos logs.
Erros comuns
- Guardar chave em texto claro: vazamento do banco concede acesso.
- Usar query string: credencial aparece em logs e histórico.
- Chave sem escopo: qualquer integração recebe acesso total.
- Sem expiração ou rotação: credenciais antigas permanecem ativas.
- Cache longo: revogação demora.
- Chave no frontend: qualquer usuário pode copiá-la.
- Registrar Authorization: logs viram fonte de vazamento.
Boas práticas
- Gere segredos aleatórios.
- Use prefixo identificável.
- Armazene somente o hash.
- Mostre a chave uma vez.
- Use escopos mínimos.
- Vincule ao tenant.
- Aplique rate limits e quotas.
- Permita rotação gradual.
- Revogue imediatamente.
- Audite sem registrar o segredo.
Conclusão
Implementar API Keys no Node.js exige tratar cada chave como uma credencial de alto valor. Um token aleatório, armazenado apenas por hash e limitado por escopos reduz muito o impacto de vazamentos.
Rotação, revogação, rate limiting, ambientes separados e auditoria completam a proteção. Com essas camadas, integrações ganham uma forma simples de autenticação sem depender de chaves permanentes, invisíveis e impossíveis de controlar.




