← Back to Blog

JWTs in 150 Lines

April 2026 securitycrypto

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