JWTs in 150 Lines
Most JWT libraries bundle six algorithms, three encoders, two JSON parsers, and a framework integration layer. Most applications need HS256 and two operations: issue and verify. So that's what we wrote.
examples/security/jwt_auth.ltl — the whole thing.
◉ Issuing
let secret = "rotate-me-every-90-days" |> as_bytes()
let token = jwt::issue(secret, "alice", 3600,
Map::empty() |> set("role", json::Str("admin")))
issue base64url-encodes the header and claims, HMACs the header.claims signing input, concatenates with dots. No surprises.
◉ Verifying
match jwt::verify(secret, token, clock_skew_secs = 5) {
Ok(claims) => {
let role = claims |> get("role") |> unwrap()
handle_request_as(role)
},
Err(JwtError::Expired) => response_401("expired"),
Err(JwtError::InvalidSignature) => response_401("bad sig"),
Err(JwtError::WrongAlgorithm(a))=> response_400("alg: {a}"),
Err(_) => response_400("malformed"),
}
Errors are a closed enum. You handle the cases the compiler enumerates for you. You can't forget to check a signature — verify never returns claims without having done it.
◉ The constant-time compare
fn constant_time_eq(a: [Byte], b: [Byte]) -> Bool {
if len(a) != len(b) { return false }
let mut diff: Int = 0
for i in 0..len(a) {
diff = diff | ((a[i] as Int) ^ (b[i] as Int))
}
diff == 0
}
This is the one place shortcuts are dangerous. A naive == short-circuits on the first mismatching byte and leaks signature bytes via timing. Our version walks the full input every time. Three lines, one footgun closed.
◉ What about `alg: none`?
Rejected. The verify path pattern-matches on Some(json::Str("HS256")) and returns WrongAlgorithm for anything else. A token with "alg": "none" doesn't deserialize into that branch, so it errors out. No "attack surface" — just exhaustive match.
◉ What's missing on purpose
- RS256 / ES256 — bring std::crypto::rsa or std::crypto::ed25519, pattern is identical - JWK rotation — out of scope; key management belongs in your auth service - Encrypted JWTs (JWE) — different protocol, different file
◉ The takeaway
Crypto code is where "no dependencies" matters most. Every library you add is code you didn't audit. This file is small enough you *can* audit it. Read it, vendor it, change the secret-rotation policy, move on.
Try it in the playground
Every snippet on this page runs in your browser. No install, no signup.
▶ Open Playground Star on GitHub