DevGuideTK
← Trilha Backend

Chaves de API

Autenticação entre sistemas, com geração, armazenamento, escopo e rotação das chaves.

Os servidores desta página foram executados nas cinco linguagens e receberam os mesmos sete pedidos. A medição dos hashes, o log do Nginx e a varredura com o gitleaks estão no nível Conceito.

Autenticação por chave, em cada linguagem

Cada versão tem as mesmas peças:

  1. criarChave: gera o prefixo e 32 bytes aleatórios, guarda só o SHA-256 e devolve a chave, que o cliente vê uma única vez.
  2. autenticar: lê a chave do cabeçalho Authorization, calcula o hash e busca o registro, recusando chaves revogadas e expiradas.
  3. A rota: 401 sem uma chave válida, 403 quando a chave não tem o escopo da ação.
import crypto from "node:crypto";
import http from "node:http";

type Registro = { cliente: string; escopos: string[]; expiraEm: Date | null; revogada: boolean };
const PREFIXO = "loja_live_";

// O banco guarda só o hash; a chave aparece para o cliente uma única vez
const hash = (chave: string) => crypto.createHash("sha256").update(chave).digest("hex");
const chaves = new Map<string, Registro>(); // hash -> registro (em produção, uma tabela)

function criarChave(cliente: string, escopos: string[], expiraEm: Date | null = null): string {
  const chave = PREFIXO + crypto.randomBytes(32).toString("base64url"); // 256 bits aleatórios
  chaves.set(hash(chave), { cliente, escopos, expiraEm, revogada: false });
  return chave;
}

function autenticar(req: http.IncomingMessage): Registro | null {
  const [tipo, chave] = (req.headers.authorization ?? "").split(" ");
  if (tipo !== "Bearer" || !chave?.startsWith(PREFIXO)) return null; // nunca na URL
  const registro = chaves.get(hash(chave)); // busca pelo hash, como num índice único
  if (!registro || registro.revogada) return null;
  if (registro.expiraEm && registro.expiraEm < new Date()) return null;
  return registro;
}

const escopoDaRota: Record<string, string> = { GET: "pedidos:ler", POST: "pedidos:escrever" };

http.createServer((req, res) => {
  const registro = autenticar(req);
  if (!registro) {
    res.writeHead(401, { "WWW-Authenticate": "Bearer" });
    return res.end();
  }
  if (!registro.escopos.includes(escopoDaRota[req.method ?? ""])) {
    res.statusCode = 403;
    return res.end();
  }
  res.end(JSON.stringify({ cliente: registro.cliente }));
}).listen(3000);

// Chaves de exemplo, impressas só para o teste
const a = criarChave("parceiro-a", ["pedidos:ler"]);
const b = criarChave("parceiro-b", ["pedidos:ler"]);
chaves.get(hash(b))!.revogada = true;
const c = criarChave("parceiro-c", ["pedidos:ler"], new Date(Date.now() - 1000));
console.log(JSON.stringify({ a, b, c }));

O mapa em memória faz o papel da tabela, para o exemplo rodar sozinho. As três chaves impressas na saída também só existem para o teste.

O resultado, nas cinco linguagens

PedidoEsperadoNas cinco
GET /pedidos sem chave401401
Chave inventada, com o prefixo certo401401
Chave do parceiro A, com o escopo pedidos:ler200200
POST /pedidos com a chave do parceiro A, sem pedidos:escrever403403
Chave revogada do parceiro B401401
Chave expirada do parceiro C401401
Chave válida do parceiro A, enviada em ?api_key=401401

O que muda de uma linguagem para outra

Bytes aleatóriosBase64 para URLSHA-256 em hexadecimal
TypeScriptcrypto.randomBytestoString("base64url")createHash("sha256")
Pythonsecrets.token_urlsafejá incluídohashlib.sha256(...).hexdigest()
Gocrypto/randbase64.RawURLEncodingsha256.Sum256 e hex
JavaSecureRandomBase64.getUrlEncoder().withoutPadding()MessageDigest e HexFormat
C#RandomNumberGenerator.GetBytesToBase64String com troca de caracteresSHA256.HashData e ToHexStringLower

Repare que a busca é feita pelo hash, num mapa, e não comparando a chave recebida com cada chave guardada. Por isso a comparação em tempo constante não é necessária aqui: o atacante não controla o hash que está sendo procurado, e o tempo da busca não revela nada útil sobre as chaves.

A tabela

CREATE TABLE chaves_api (
  id             bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  cliente_id     bigint NOT NULL REFERENCES clientes (id),
  nome           text NOT NULL,                -- "ERP da filial Sul"
  hash           text NOT NULL UNIQUE,         -- SHA-256 da chave inteira
  final          text NOT NULL,                -- últimos 4 caracteres, para a tela
  escopos        text[] NOT NULL,
  expira_em      timestamptz,
  revogada_em    timestamptz,
  ultimo_uso_em  timestamptz,
  ultimo_uso_ip  inet,
  criada_em      timestamptz NOT NULL DEFAULT now()
);

A busca de cada pedido usa o índice criado pelo UNIQUE em hash. Atualizar ultimo_uso_em em todo pedido gera uma escrita por leitura; uma saída comum é atualizar só quando o valor guardado tiver mais de alguns minutos.

A regra na varredura de segredos

A configuração do gitleaks usada no teste, que soma a regra da chave às regras padrão:

[extend]
useDefault = true

[[rules]]
id = "loja-api-key"
description = "Chave de API da Loja"
regex = '''loja_(?:live|test)_[A-Za-z0-9_-]{43}'''

Rode a varredura na integração contínua e num gancho antes do commit (gitleaks git no repositório, ou gitleaks dir numa pasta). No teste, só essa regra encontrou a chave com prefixo no meio de um texto. A primeira versão, com (live|test) em vez de (?:live|test), registrou só a palavra live como segredo.

Lista de revisão

Geração

  • A chave usa o gerador criptográfico e tem pelo menos 128 bits aleatórios (de preferência 256)?
  • Tem um prefixo do produto e do ambiente?
  • É mostrada ao cliente uma única vez?

Armazenamento

  • O banco guarda só o hash, com índice único, e os últimos caracteres para a tela?
  • O hash é rápido (SHA-256), e não bcrypt ou Argon2?

Uso

  • A chave só é aceita no cabeçalho, nunca na URL?
  • Chaves revogadas e expiradas são recusadas?
  • Cada chave tem escopos, e as rotas conferem o escopo?
  • Há limite de requisições por chave?
  • O último uso é registrado?

Ciclo de vida

  • O cliente pode ter várias chaves ativas, para fazer a rotação sem parar?
  • Há uma regra na varredura de segredos, e um plano para quando uma chave vazar?
  • Chaves sem uso há muito tempo são removidas?

Prompts para aprofundar

Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.

  • Implementar autenticação por chave de API

    Quero implementar autenticação por chave de API na minha aplicação.
    
    Pergunte qual linguagem, framework e banco eu uso.
    
    Vamos em etapas:
    1. A geração da chave com prefixo e 256 bits aleatórios.
    2. A tabela com o hash SHA-256 em índice único, escopos, validade e revogação.
    3. O middleware que lê o cabeçalho, busca pelo hash e recusa chaves revogadas ou expiradas.
    4. A conferência do escopo em cada rota.
    5. Testes com chave válida, inventada, revogada, expirada, sem escopo e enviada na URL.
    
    Em cada etapa, me passe os comandos para testar com curl.
  • Configurar a varredura de segredos

    Quero configurar a varredura de segredos no meu repositório e na integração contínua.
    
    Pergunte qual serviço de repositório e de integração contínua eu uso e quais formatos de chave o meu sistema e os meus fornecedores usam.
    
    Explique, com a configuração completa:
    1. O gitleaks ou outra ferramenta, com as regras padrão.
    2. Uma regra própria para o prefixo das minhas chaves, com um teste que prove que ela encontra a chave.
    3. O gancho antes do commit e a etapa na integração contínua.
    4. A varredura do histórico inteiro, e o que fazer quando ela encontrar algo antigo.
    5. Como tratar falsos positivos sem desligar a regra.
  • Revisar uma implementação de chaves de API

    Vou colar abaixo o código de geração, armazenamento e verificação de chaves de API de uma aplicação. Revise para produção.
    
    Verifique, e para cada item diga se está correto, incorreto ou ausente, citando o trecho:
    1. A chave usa um gerador criptográfico e tem pelo menos 128 bits aleatórios?
    2. O banco guarda só o hash, com um hash rápido e índice único?
    3. A chave é aceita só no cabeçalho?
    4. Chaves revogadas e expiradas são recusadas?
    5. Há escopos conferidos em cada rota?
    6. O cliente pode ter várias chaves, para fazer a rotação?
    7. O último uso é registrado, e há limite de requisições por chave?
    
    Para cada problema, descreva o risco e mostre a correção.
    
    Código:
    [cole aqui]