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