JWT em Zig: Autenticação para APIs sem Framework Pesado
JWT aparece cedo em quase toda API moderna. Um serviço começa com uma rota pública, depois precisa identificar usuário, separar permissões, proteger endpoints internos e conversar com frontend, CLI ou outro serviço. Em Node.js, Go ou Java, normalmente existe um pacote pronto que esconde boa parte da mecânica. Em Zig, a decisão costuma ser mais explícita: você precisa entender o formato do token, validar assinatura, impor expiração, limitar algoritmo aceito e desenhar a fronteira de autenticação da sua aplicação.
Essa explicitude é boa para sistemas críticos, mas perigosa se virar improviso. JWT não é apenas “decodificar Base64 e ler JSON”. Um token só é confiável depois de validar assinatura, alg, exp, emissor, audiência e regras locais. Também não é uma boa ideia espalhar parsing de token por handlers HTTP diferentes. O caminho saudável em Zig é tratar autenticação como uma borda: uma camada pequena recebe o header Authorization, valida tudo, transforma claims confiáveis em uma struct tipada e entrega essa struct para o restante da API.
Este guia mostra como pensar em JWT em Zig para APIs reais: quando faz sentido, quais erros evitar, como montar validação HMAC com std.crypto, onde encaixar isso em um servidor HTTP e como operar rotação de chaves sem vazar segredo. Ele complementa os guias de API REST completa em Zig, servidor HTTP em produção, configuração segura, env vars e segredos e observabilidade em Zig.
Quando JWT faz sentido em uma API Zig
JWT é útil quando a API precisa receber uma credencial curta, verificável localmente e compatível com clientes diversos. Ele funciona bem para:
- APIs chamadas por frontend web, mobile ou CLI;
- autenticação entre serviços quando existe um emissor central confiável;
- sessões curtas em sistemas stateless;
- autorização baseada em claims simples, como
sub,role,tenantescope; - gateways que validam token antes de encaminhar tráfego para serviços menores.
JWT faz menos sentido quando você precisa revogar sessão imediatamente, manter estado complexo de sessão, auditar cada permissão no banco em tempo real ou esconder dados sensíveis dentro do token. O payload de um JWT comum é apenas Base64URL, não criptografia. Qualquer cliente consegue ler os claims. Coloque ali identificadores e permissões necessárias, não segredo, senha, CPF, token de terceiro ou dado privado desnecessário.
Para produtos com autenticação social, painel administrativo e dezenas de integrações, talvez uma stack mais madura entregue mais rápido. Uma estratégia comum é usar Go, Kotlin, Node.js ou Python no plano de produto e Zig em componentes de performance. Se você está comparando arquiteturas de API, vale olhar também o material irmão de API REST em Go, especialmente quando o time precisa de ecossistema web pronto.
O formato de um JWT
Um JWT assinado tem três partes separadas por ponto:
header.payload.signature
O header informa o tipo e o algoritmo, por exemplo HS256. O payload contém claims JSON, como usuário, expiração e escopos. A signature prova que header e payload foram assinados com a chave esperada. Para HS256, a assinatura é HMAC-SHA256 sobre a string header.payload.
O erro clássico é ler o payload antes de confiar no token. Você pode até parsear o header para descobrir o algoritmo declarado, mas nunca deve aceitar qualquer algoritmo vindo do cliente. Defina no servidor uma lista curta, de preferência um único algoritmo. Se sua API espera HS256, rejeite none, RS256, HS512 e qualquer valor inesperado. O algoritmo do token é dado não confiável até a política local confirmar que ele é permitido.
Claims mínimos para uma API costumam ser:
| Claim | Uso | Validação recomendada |
|---|---|---|
sub | Identificador do usuário ou serviço | obrigatório, formato esperado |
exp | Expiração em Unix time | obrigatório, maior que agora |
iat | Emissão | opcional, não muito no futuro |
iss | Emissor | igual ao emissor configurado |
aud | Audiência | igual à API esperada |
scope | Permissões | conjunto conhecido, sem aceitar string arbitrária |
Modelo de dados em Zig
Comece com structs pequenas. Uma struct para configuração, uma para claims confiáveis e uma para erro de autenticação já evita muita ambiguidade.
const std = @import("std");
const AuthConfig = struct {
issuer: []const u8,
audience: []const u8,
hmac_secret: []const u8,
clock_skew_seconds: i64 = 30,
};
const Claims = struct {
subject: []const u8,
issuer: []const u8,
audience: []const u8,
expires_at: i64,
issued_at: ?i64,
scope: []const u8,
};
const AuthError = error{
MissingAuthorization,
InvalidBearerFormat,
MalformedToken,
UnsupportedAlgorithm,
InvalidSignature,
ExpiredToken,
InvalidIssuer,
InvalidAudience,
InvalidClaims,
};
Essa estrutura força a aplicação a separar três coisas: configuração sensível, token recebido do cliente e identidade validada. hmac_secret deve vir de variável de ambiente, secret manager ou arquivo montado pelo orquestrador, como discutido no guia de configuração segura. Não grave a chave no build.zig, no build.zig.zon, em fixture pública nem em log de boot.
Extraindo o Bearer token
No handler HTTP, a primeira etapa é extrair o header. O handler não deve validar claims diretamente; ele só chama uma função de autenticação.
fn bearerToken(authorization: ?[]const u8) AuthError![]const u8 {
const value = authorization orelse return AuthError.MissingAuthorization;
const prefix = "Bearer ";
if (!std.mem.startsWith(u8, value, prefix)) return AuthError.InvalidBearerFormat;
const token = value[prefix.len..];
if (token.len == 0) return AuthError.InvalidBearerFormat;
return token;
}
Em produção, limite o tamanho do header antes de copiar qualquer coisa. Um JWT de sessão normal costuma caber em poucos kilobytes. Um header Authorization enorme pode ser tentativa de abuso ou bug de cliente. Combine limite no proxy, limite no parser HTTP e métrica de rejeição.
Validando assinatura HS256
Para HS256, a assinatura esperada é HMAC-SHA256 de header.payload usando o segredo compartilhado. O esqueleto abaixo mostra a ideia principal. Em um projeto real, você também precisa implementar Base64URL estrito, JSON parsing com limites e comparação em tempo constante.
const HmacSha256 = std.crypto.auth.hmac.sha2.HmacSha256;
fn verifyHs256(signing_input: []const u8, signature: []const u8, secret: []const u8) AuthError!void {
var expected: [HmacSha256.mac_length]u8 = undefined;
HmacSha256.create(&expected, signing_input, secret);
if (signature.len != expected.len) return AuthError.InvalidSignature;
if (!std.crypto.utils.timingSafeEql([expected.len]u8, expected, signature[0..expected.len].*)) {
return AuthError.InvalidSignature;
}
}
O ponto mais importante é a ordem: primeiro separe exatamente as três partes, valide o algoritmo permitido no header, gere a assinatura esperada sobre a string original header.payload, compare com a assinatura decodificada e só então trate os claims como confiáveis. Se você normalizar JSON, reserializar payload ou montar outra string antes de assinar, a validação deixa de representar o token recebido.
Também evite mensagens de erro muito específicas para o cliente. Para log interno, é útil saber InvalidAudience ou ExpiredToken. Para resposta HTTP, normalmente basta 401 Unauthorized. Não diga ao atacante se o usuário existe, se a assinatura estava quase certa ou se a audiência foi o único problema.
Validando claims obrigatórios
Depois da assinatura, valide claims como regra de produto:
fn validateClaims(claims: Claims, cfg: AuthConfig, now: i64) AuthError!void {
if (!std.mem.eql(u8, claims.issuer, cfg.issuer)) return AuthError.InvalidIssuer;
if (!std.mem.eql(u8, claims.audience, cfg.audience)) return AuthError.InvalidAudience;
if (claims.subject.len == 0) return AuthError.InvalidClaims;
if (claims.expires_at + cfg.clock_skew_seconds < now) return AuthError.ExpiredToken;
if (claims.issued_at) |iat| {
if (iat - cfg.clock_skew_seconds > now) return AuthError.InvalidClaims;
}
}
Não confunda autenticação com autorização completa. Validar sub responde “quem é”. Validar scope, role, tenant e políticas locais responde “pode fazer isso agora”. Para endpoints sensíveis, a checagem precisa acontecer perto da ação, não apenas em um middleware genérico. Uma rota de pagamento, por exemplo, deve confirmar o tenant, o escopo necessário e o estado atual do recurso.
Encaixando no servidor HTTP
Um desenho simples para APIs Zig é:
HTTP parser -> auth middleware -> router -> handler -> store
O middleware recebe request e configuração, tenta autenticar, adiciona Claims ao contexto e chama o próximo handler. Rotas públicas pulam essa etapa. Rotas privadas recebem um contexto já autenticado.
const RequestContext = struct {
allocator: std.mem.Allocator,
request_id: []const u8,
claims: ?Claims = null,
};
Essa abordagem combina com a arquitetura sugerida em microserviços em Zig e servidores HTTP em produção: cada borda externa traduz detalhe técnico em tipos pequenos do domínio. O handler que cria um recurso não precisa saber como HMAC funciona; ele precisa saber que existe um subject validado e quais escopos foram concedidos.
Rotação de chaves e kid
Quando a API cresce, uma única chave eterna vira risco. JWT costuma usar kid no header para identificar qual chave assinou o token. A validação lê kid, procura a chave em um mapa configurado e valida a assinatura com ela. Isso permite rotação gradual:
- adicione uma chave nova como ativa para emissão;
- mantenha a chave antiga apenas para validação;
- espere expirar o maior TTL de token;
- remova a chave antiga.
Não busque chave remota a cada request sem cache, timeout e fallback. Autenticação está no caminho crítico. Se você usa JWKS com chaves públicas, faça refresh em background, limite tamanho da resposta e mantenha a última versão válida por uma janela curta. Para HS256, prefira segredo local vindo do orquestrador e rotação coordenada pelo deploy.
Checklist de produção
Antes de colocar JWT em uma API Zig, revise:
algé fixo ou explicitamente allowlisted;noneé sempre rejeitado;- assinatura é validada antes de confiar no payload;
expé obrigatório e curto;isseaudsão validados;- segredo não aparece em Git, Dockerfile, log, métrica ou erro;
- comparação de assinatura não vaza timing óbvio;
- header e payload têm limite de tamanho;
- respostas externas usam
401/403sem detalhe sensível; - logs registram causa agregada, request id e rota, sem token bruto;
- rotação de chaves está documentada;
- testes cobrem token expirado, assinatura inválida, algoritmo errado, audiência errada e claim ausente.
Conclusão
JWT em Zig é viável, mas deve ser tratado como infraestrutura de segurança, não como parsing casual de JSON. A vantagem de Zig é deixar custos e fronteiras visíveis: segredo entra pela configuração, token entra pelo header, assinatura é validada com std.crypto, claims viram uma struct tipada e handlers recebem apenas identidade já validada.
Se a sua API precisa de autenticação simples e previsível, esse modelo funciona bem. Se o produto exige login social complexo, autorização dinâmica, revogação instantânea e integrações corporativas, talvez faça sentido delegar autenticação a um gateway, serviço especializado ou stack com ecossistema mais maduro. Em todos os casos, mantenha a regra central: token recebido é dado não confiável até assinatura, algoritmo e claims passarem por validação explícita.
Próximos passos
- Zig API REST completa — desenho de rotas, handlers e integração com banco.
- Zig HTTP Server em produção — limites, health checks, logs e operação.
- Configuração segura em Zig — env vars, segredos e validação no boot.
- Observabilidade em Zig — logs e métricas sem vazar dados sensíveis.