JWT Authentication
The jwtAuth middleware verifies JSON Web Tokens (JWT) signed with HMAC-SHA256. It extracts the token from the Authorization: Bearer header, validates the signature using a shared secret, and stores the decoded payload JSON in assigns.
Configuration
Section titled “Configuration”| Option | Type | Default | Description |
|---|---|---|---|
secret | []const u8 | (required) | The HMAC-SHA256 secret key used to verify token signatures |
assign_key | []const u8 | "jwt_payload" | Key used to store the decoded payload JSON in ctx.assigns |
required | bool | false | When true, returns 401 if the token is missing or invalid |
Basic usage
Section titled “Basic usage”-
Apply the
jwtAuthmiddleware to the routes you want to protect, providing your secret key. -
In your handler, read the decoded JWT payload from assigns.
-
Parse the payload JSON to extract claims as needed.
const zzz = @import("zzz");
const routes = zzz.Router.scope("/api", &.{ zzz.jwtAuth(.{ .secret = "my-hmac-secret", .required = true }),}, &.{ zzz.Router.get("/me", meHandler),});
fn meHandler(ctx: *zzz.Context) !void { const payload = ctx.getAssign("jwt_payload").?; // payload is the raw JSON string, e.g. {"sub":"1234","name":"Alice"} var buf: [512]u8 = undefined; const body = std.fmt.bufPrint(&buf, \\{{"auth":"jwt","payload":{s}}} , .{payload}) catch return; ctx.json(.ok, body);}Test with curl:
curl -H "Authorization: Bearer <your-jwt-token>" http://127.0.0.1:9000/api/meHow token verification works
Section titled “How token verification works”The middleware performs the following steps internally:
-
Extract the bearer token from the
Authorizationheader. -
Split the token into three base64url-encoded parts:
header.payload.signature. -
Decode the signature and compute HMAC-SHA256 over the
header.payloadstring using the configured secret. -
Compare the computed MAC with the decoded signature using constant-time comparison to prevent timing attacks.
-
If the signature is valid, base64url-decode the payload and store the resulting JSON string in assigns.
The middleware supports both standard base64 and base64url encoding, with or without padding, so tokens generated by most JWT libraries will work.
Token format
Section titled “Token format”zzz expects standard JWT format with three dot-separated segments:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0IiwibmFtZSI6IkFsaWNlIn0.<signature>- Header: must specify
"alg":"HS256". Other algorithms are not supported. - Payload: any valid JSON object. The entire decoded payload is stored as a string in assigns.
- Signature: HMAC-SHA256 of
header.payload, base64url-encoded.
Extracting claims from the payload
Section titled “Extracting claims from the payload”The JWT payload is stored as a raw JSON string. To work with individual claims, you can parse it using Zig’s standard JSON parser or do simple string matching:
fn handler(ctx: *zzz.Context) !void { const payload = ctx.getAssign("jwt_payload") orelse { ctx.text(.unauthorized, "No token"); return; };
// Simple check: does the payload contain a specific claim value? if (std.mem.indexOf(u8, payload, "\"role\":\"admin\"") != null) { ctx.json(.ok, "{\"access\":\"admin\"}"); } else { ctx.respond(.forbidden, "text/plain; charset=utf-8", "403 Forbidden"); }}const std = @import("std");
fn handler(ctx: *zzz.Context) !void { const payload = ctx.getAssign("jwt_payload") orelse { ctx.text(.unauthorized, "No token"); return; };
const parsed = std.json.parseFromSlice( struct { sub: []const u8, name: []const u8 }, ctx.allocator, payload, .{ .ignore_unknown_fields = true }, ) catch { ctx.respond(.bad_request, "text/plain; charset=utf-8", "Invalid JWT payload"); return; }; defer parsed.deinit();
var buf: [256]u8 = undefined; const body = std.fmt.bufPrint(&buf, \\{{"user":"{s}","subject":"{s}"}} , .{ parsed.value.name, parsed.value.sub }) catch return; ctx.json(.ok, body);}Generating tokens
Section titled “Generating tokens”zzz does not include a JWT generation function — it is a verification-only middleware. You can generate tokens in your login handler using the standard library’s HMAC implementation:
const std = @import("std");const zzz = @import("zzz");
fn loginHandler(ctx: *zzz.Context) !void { // After validating user credentials...
const header_b64 = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9";
// Build payload JSON var payload_buf: [256]u8 = undefined; const payload_json = std.fmt.bufPrint(&payload_buf, \\{{"sub":"user-123","name":"Alice"}} , .{}) catch return;
// Base64url-encode the payload const encoder = std.base64.url_safe_no_pad; var payload_b64_buf: [512]u8 = undefined; const payload_b64 = encoder.encode(&payload_b64_buf, payload_json);
// Compute HMAC-SHA256 signature var signed_buf: [1024]u8 = undefined; const signed_part = std.fmt.bufPrint(&signed_buf, "{s}.{s}", .{ header_b64, payload_b64, }) catch return;
const HmacSha256 = std.crypto.auth.hmac.sha2.HmacSha256; var mac: [HmacSha256.mac_length]u8 = undefined; HmacSha256.create(&mac, signed_part, "my-hmac-secret");
// Base64url-encode the signature var sig_b64_buf: [64]u8 = undefined; const sig_b64 = encoder.encode(&sig_b64_buf, &mac);
// Assemble the token var token_buf: [2048]u8 = undefined; const token = std.fmt.bufPrint(&token_buf, \\{{"token":"{s}.{s}"}} , .{ signed_part, sig_b64 }) catch return;
ctx.json(.ok, token);}Configuring the secret
Section titled “Configuring the secret”The secret field is a comptime value. For production applications, you can load it from a build-time configuration:
const app_config = @import("app_config");
const routes = zzz.Router.scope("/api", &.{ zzz.jwtAuth(.{ .secret = app_config.jwt_secret, .required = true, }),}, &.{ zzz.Router.get("/protected", protectedHandler),});Since the secret is embedded at compile time, make sure your build configuration keeps it out of version control. Use Zig build options or environment-based config files to inject it.
Error responses
Section titled “Error responses”When required = true and authentication fails, the middleware responds with:
HTTP/1.1 401 UnauthorizedWWW-Authenticate: BearerContent-Type: text/plain; charset=utf-8
401 UnauthorizedThis happens when:
- The
Authorizationheader is missing - The header does not start with
Bearer - The token does not have three dot-separated segments
- The HMAC-SHA256 signature does not match
- The signature or payload cannot be base64-decoded
Security considerations
Section titled “Security considerations”- HMAC-SHA256 only — the middleware exclusively supports the HS256 algorithm. RSA and ECDSA signatures are not supported.
- Constant-time comparison — signature verification uses constant-time byte comparison to prevent timing side-channel attacks.
- No expiration checking — the middleware does not validate
exp,nbf, oriatclaims. If you need expiration enforcement, check the claims in your handler after parsing the payload. - Payload size limit — the decoded payload buffer is 2048 bytes. Tokens with larger payloads will fail verification.
Next steps
Section titled “Next steps”- Bearer and Basic Auth — simpler token and credential extraction
- Sessions and CSRF — server-side session management
- Rate Limiting — throttle API requests