API Keys em Node.js: autenticação de serviços
API Keys são o padrão para autenticação de serviços machine-to-machine — integrações B2B, SDKs de terceiros, webhooks e chamadas server-to-server. Diferente do JWT, a API key é um segredo opaco que o servidor valida comparando com o hash armazenado no banco.
Gerar e armazenar API Keys com segurança
Nunca armazenar a chave em texto puro — apenas o hash SHA-256:
typescript
// lib/api-keys.ts
import crypto from 'crypto'
import { db } from './db'
// Formato: rsk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
// Prefixo identificável + ambiente + chave aleatória
export function gerarApiKey(ambiente: 'live' | 'test' = 'live'): string {
const randomBytes = crypto.randomBytes(24).toString('base64url')
return `rsk_${ambiente}_${randomBytes}`
}
// Hash SHA-256 para armazenar no banco (nunca a chave em si):
export function hashApiKey(chave: string): string {
return crypto.createHash('sha256').update(chave).digest('hex')
}
// Criar nova API Key para um usuário/organização:
export async function criarApiKey(params: {
usuarioId: string
nome: string // "Integração ERP", "Webhook Shopify"
permissoes: string[]
expiresAt?: Date
}) {
const chave = gerarApiKey()
const hash = hashApiKey(chave)
const { rows: [apiKey] } = await db.query(
`INSERT INTO api_keys (usuario_id, nome, hash, permissoes, expires_at)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, nome, criado_em`,
[params.usuarioId, params.nome, hash, params.permissoes, params.expiresAt ?? null]
)
// Retornar a chave completa APENAS neste momento — nunca mais será exibida:
return { ...apiKey, chave }
}
// Validar API Key recebida na requisição:
export async function validarApiKey(chave: string) {
const hash = hashApiKey(chave)
const { rows: [apiKey] } = await db.query(
`SELECT ak.*, u.id AS usuario_id, u.email
FROM api_keys ak
JOIN usuarios u ON u.id = ak.usuario_id
WHERE ak.hash = $1
AND ak.ativo = true
AND (ak.expires_at IS NULL OR ak.expires_at > NOW())`,
[hash]
)
if (!apiKey) return null
// Atualizar last_used_at:
await db.query('UPDATE api_keys SET last_used_at = NOW() WHERE id = $1', [apiKey.id])
return apiKey
}Middleware de autenticação por API Key
Aceitar chave no header X-API-Key ou Authorization Bearer:
typescript
// middleware/api-key-auth.ts
import { validarApiKey } from '../lib/api-keys'
export async function autenticarApiKey(req: Request, res: Response, next: NextFunction) {
// Aceitar nos dois formatos comuns:
const chave =
req.headers['x-api-key'] as string ||
(req.headers.authorization?.startsWith('Bearer ')
? req.headers.authorization.slice(7)
: null)
if (!chave) {
return res.status(401).json({ error: 'API Key não fornecida' })
}
// Validar formato antes de consultar o banco:
if (!/^rsk_(live|test)_[A-Za-z0-9_-]{32}$/.test(chave)) {
return res.status(401).json({ error: 'Formato de API Key inválido' })
}
const apiKey = await validarApiKey(chave)
if (!apiKey) {
return res.status(401).json({ error: 'API Key inválida ou expirada' })
}
// Adicionar contexto ao request:
req.apiKey = apiKey
req.usuario = { id: apiKey.usuario_id, email: apiKey.email }
next()
}
// Verificar permissão específica:
export function exigirPermissao(permissao: string) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.apiKey?.permissoes?.includes(permissao)) {
return res.status(403).json({
error: 'Permissão negada',
required: permissao,
available: req.apiKey?.permissoes,
})
}
next()
}
}
// Usar nas rotas:
app.post('/api/v1/pedidos',
autenticarApiKey,
exigirPermissao('pedidos:write'),
criarPedidoHandler
)Cache de validação no Redis
Evitar query ao banco em cada requisição com cache de 5 minutos:
typescript
// lib/api-keys.ts — adicionar cache Redis:
import { redis } from './redis'
const CACHE_TTL = 300 // 5 minutos
export async function validarApiKey(chave: string) {
const hash = hashApiKey(chave)
const cacheKey = `api_key:${hash}`
// Verificar cache primeiro:
const cached = await redis.get(cacheKey)
if (cached === 'invalid') return null // chave inválida conhecida
if (cached) return JSON.parse(cached) // chave válida em cache
// Consultar banco:
const { rows: [apiKey] } = await db.query(
`SELECT ak.id, ak.permissoes, ak.expires_at, ak.ativo,
u.id AS usuario_id, u.email
FROM api_keys ak JOIN usuarios u ON u.id = ak.usuario_id
WHERE ak.hash = $1 AND ak.ativo = true
AND (ak.expires_at IS NULL OR ak.expires_at > NOW())`,
[hash]
)
if (!apiKey) {
// Cachear resultado negativo (evitar brute force de banco):
await redis.setex(cacheKey, 60, 'invalid') // TTL menor para inválidas
return null
}
// Cachear resultado positivo:
await redis.setex(cacheKey, CACHE_TTL, JSON.stringify(apiKey))
// Atualizar last_used (fire-and-forget, não bloquear a resposta):
db.query('UPDATE api_keys SET last_used_at = NOW() WHERE id = $1', [apiKey.id]).catch(() => {})
return apiKey
}
// Ao revogar uma chave: limpar o cache imediatamente:
export async function revogarApiKey(id: string) {
const { rows: [key] } = await db.query(
'UPDATE api_keys SET ativo = false WHERE id = $1 RETURNING hash',
[id]
)
if (key) await redis.del(`api_key:${key.hash}`)
}Dashboard de gerenciamento de API Keys
Endpoints para criar, listar e revogar chaves via API:
typescript
// Listar API Keys do usuário (sem mostrar o hash):
app.get('/api/api-keys', autenticar, async (req, res) => {
const { rows } = await db.query(
`SELECT id, nome, permissoes, criado_em, last_used_at,
expires_at, ativo,
-- Mostrar apenas prefixo para identificação:
'rsk_****' AS chave_preview
FROM api_keys
WHERE usuario_id = $1
ORDER BY criado_em DESC`,
[req.usuario!.userId]
)
res.json(rows)
})
// Criar nova API Key:
app.post('/api/api-keys', autenticar, async (req, res) => {
const { nome, permissoes, expiresIn } = req.body as {
nome: string
permissoes: string[]
expiresIn?: number // dias
}
const expiresAt = expiresIn
? new Date(Date.now() + expiresIn * 86_400_000)
: undefined
const apiKey = await criarApiKey({
usuarioId: req.usuario!.userId,
nome,
permissoes,
expiresAt,
})
// chave é retornada APENAS aqui — armazenar do lado do cliente
res.status(201).json({
...apiKey,
aviso: 'Salve a chave agora — ela não será exibida novamente',
})
})
// Revogar:
app.delete('/api/api-keys/:id', autenticar, async (req, res) => {
await revogarApiKey(req.params.id)
res.json({ message: 'API Key revogada' })
})Rate limiting por API Key
Limitar requisições por chave usando Redis:
typescript
// middleware/rate-limit-api-key.ts
import { redis } from '../lib/redis'
interface RateLimitConfig {
windowMs: number // janela em ms
max: number // max requisições por janela
}
export function rateLimitPorApiKey(config: RateLimitConfig) {
return async (req: Request, res: Response, next: NextFunction) => {
const keyId = req.apiKey?.id
if (!keyId) return next()
const window = Math.floor(Date.now() / config.windowMs)
const redisKey = `rl:apikey:${keyId}:${window}`
const count = await redis.incr(redisKey)
if (count === 1) {
// Definir expiração na primeira requisição da janela:
await redis.pexpire(redisKey, config.windowMs)
}
// Adicionar headers informativos:
res.setHeader('X-RateLimit-Limit', config.max)
res.setHeader('X-RateLimit-Remaining', Math.max(0, config.max - count))
res.setHeader('X-RateLimit-Reset', Math.ceil((window + 1) * config.windowMs / 1000))
if (count > config.max) {
return res.status(429).json({
error: 'Rate limit excedido',
retryAfter: Math.ceil(config.windowMs / 1000),
})
}
next()
}
}
// Usar nas rotas:
app.use('/api/v1',
autenticarApiKey,
rateLimitPorApiKey({ windowMs: 60_000, max: 100 }) // 100 req/min
)$ runstack deploy --plan starter
Não quer configurar manualmente?
Não quer configurar manualmente? Implante o VPS para APIs em menos de 3 minutos com a Runstack. Infraestrutura da OPEN DATACENTER, com servidores no Brasil.
Perguntas frequentes
Conteúdos relacionados