std/jwt
JSON Web Tokens (RFC 7519 / 7515) over HMAC-SHA2, pure Milo on std/hmac, std/base64, std/json and std/subtle. HS256/384/512 are deterministic, so a token produced here is byte-identical to PyJWT / jsonwebtoken for the same header, payload, and secret.
from "std/jwt" import { Jwt, JwtAlg, JwtVerifier, JwtError, jwtErrorMessage }Verification returns the claims, not a bool. A yes/no verifier invites if verify(token, secret) { ... }, which happily accepts a token that expired three years ago or was minted for a different audience — the signature is only half the check. Result<JwtClaims, JwtError> makes "the signature is good" and "this token is valid right now" the same line of code.
Three attacks are handled explicitly:
alg: none— the header algorithm must equal the one the caller asked for, so an unsigned token isUnsupportedAlg("none").- Algorithm confusion — the algorithm comes from the verifier, never from the token.
- Signature timing — the MAC comparison is
constantTimeEqover raw bytes, and a non-canonical base64url signature is rejected rather than silently accepted.
HMAC algorithms only. RS*/ES*/PS* need public-key crypto the standard library does not have; such a token is rejected as unsupported rather than downgraded.
Signing and verifying
Jwt.signHS256 signs a JSON payload string and returns a compact JWS: base64url(header).base64url(payload).base64url(HMAC). Jwt.verifyHS256 checks the algorithm, the signature, and exp/nbf/iat against the system clock with JWT_DEFAULT_LEEWAY (60 s) of tolerance, then returns the claims.
match Jwt.verifyHS256(token, secret) {
Ok(claims) => {
match claims.subject() {
Some(sub) => { print("hello " + sub) }
None => { print("anonymous token") }
}
}
Err(e) => { print(jwtErrorMessage(e)) }
}One policy, reused
JwtVerifier holds the policy in one place: which algorithm, how much clock skew, which audience and issuer, and whether a token without exp is acceptable. at pins the clock to a fixed instant, which is what makes a claim test reproducible.
let verifier = JwtVerifier.new(JwtAlg.HS256, secret)
.audience("api.example")
.issuer("auth.example")
.requireExpiry()
let claims = verifier.verify(token)?A rejection is a JwtError variant, not a false: the caller recovers differently from Expired than from BadSignature. jwtErrorMessage(err) renders any of them as one log-safe line.
API reference
Jwt
pub struct JwtJwt namespace — Jwt.signHS256(payload, secret) and Jwt.verifyHS256(token, secret) for the common case; JwtVerifier when you need audience, issuer, or a fixed clock.
Jwt.sign
fn Jwt.sign(alg: JwtAlg, payload: &string, secret: &string): stringSign a JSON payload string with alg, returning a compact JWS.
Jwt.signHS256
fn Jwt.signHS256(payload: &string, secret: &string): stringSign with HMAC-SHA256 — the JWT default.
Jwt.signHS384
fn Jwt.signHS384(payload: &string, secret: &string): stringSign with HMAC-SHA384.
Jwt.signHS512
fn Jwt.signHS512(payload: &string, secret: &string): stringSign with HMAC-SHA512.
Jwt.verifyHS256
fn Jwt.verifyHS256(token: &string, secret: &string): Result<JwtClaims, JwtError>Verify an HS256 token against secret and return its claims. Rejects any other algorithm (including none), a bad signature, and a token that is expired or not yet valid, allowing JWT_DEFAULT_LEEWAY of clock skew. Use JwtVerifier to require an audience or issuer, or to fix the clock.
Jwt.verifyHS384
fn Jwt.verifyHS384(token: &string, secret: &string): Result<JwtClaims, JwtError>Verify an HS384 token. Same checks as verifyHS256.
Jwt.verifyHS512
fn Jwt.verifyHS512(token: &string, secret: &string): Result<JwtClaims, JwtError>Verify an HS512 token. Same checks as verifyHS256.
JwtAlg
pub enum JwtAlgThe JOSE alg values this module can verify. Named, not stringly-typed, so a caller cannot accidentally ask for "none".
Variants:
HS256HS384HS512
JwtClaims
pub struct JwtClaimsThe verified payload of a token. Only ever produced by a successful verify, so holding one is the proof that the signature and the time window checked out.
Fields: payload: string, doc: Json.
JwtClaims.bool
fn JwtClaims.bool(self: &JwtClaims, key: &string): Option<bool>A boolean-valued claim, or None if absent or not a boolean.
JwtClaims.expiresAt
fn JwtClaims.expiresAt(self: &JwtClaims): Option<i64>exp — expiry, seconds since the Unix epoch (§4.1.4).
JwtClaims.hasAudience
fn JwtClaims.hasAudience(self: &JwtClaims, want: &string): boolTrue iff the aud claim is want, or is an array containing it. RFC 7519 §4.1.3 allows either shape, and a verifier that only understands the string form silently accepts every multi-audience token.
JwtClaims.i64
fn JwtClaims.i64(self: &JwtClaims, key: &string): Option<i64>An integer-valued claim, or None if absent or not a number.
JwtClaims.issuedAt
fn JwtClaims.issuedAt(self: &JwtClaims): Option<i64>iat — issued-at, seconds since the Unix epoch (§4.1.6).
JwtClaims.issuer
fn JwtClaims.issuer(self: &JwtClaims): Option<string>iss — who minted the token (§4.1.1).
JwtClaims.notBefore
fn JwtClaims.notBefore(self: &JwtClaims): Option<i64>nbf — not-before, seconds since the Unix epoch (§4.1.5).
JwtClaims.raw
fn JwtClaims.raw(self: &JwtClaims): stringThe payload segment as its raw JSON text.
JwtClaims.str
fn JwtClaims.str(self: &JwtClaims, key: &string): Option<string>A string-valued claim, or None if absent or not a string.
JwtClaims.subject
fn JwtClaims.subject(self: &JwtClaims): Option<string>sub — the subject the token is about (RFC 7519 §4.1.2).
JwtClaims.tokenId
fn JwtClaims.tokenId(self: &JwtClaims): Option<string>jti — the token's unique id, for replay tracking (§4.1.7).
JwtError
pub enum JwtErrorWhy a token was rejected. Callers genuinely recover differently: Expired means "refresh and retry", BadSignature means "this is an attack or the wrong key", WrongAudience means "this token is for another service" — collapsing them into false throws away the only information the caller needs.
Variants:
Malformed(string): Not three base64url segments, or a segment is not valid base64url / JSON.UnsupportedAlg(string): The header'salgis not the one the verifier requires. Carries the value found in the token, so "none" shows up in the log verbatim.UnsupportedCritical: The header carries acritextension this implementation does not understand (RFC 7515 §4.1.11 requires rejection rather than ignoring it).BadSignature: The MAC did not match. No detail: there is nothing safe to report.Expired(i64):exphas passed. Carries the expiry, in seconds since the Unix epoch.NotYetValid(i64):nbfis in the future. Carries the not-before time.IssuedInFuture(i64):iatis in the future — the issuer's clock is wrong or the token is forged.MissingClaim(string): A required claim (exp, or theaud/issthe verifier demands) is absent.WrongAudience(string):audis present but does not contain the audience the verifier requires.WrongIssuer(string):issis present but is not the issuer the verifier requires.
JwtVerifier
pub struct JwtVerifierA configured verifier: the algorithm and secret to require, plus which claims to insist on. Build one with JwtVerifier.new, chain the options you need, and call verify. Reusing a verifier is the point — the policy lives in one place instead of being re-spelled at every call site.
Fields:
secret: stringalg: JwtAlgleewaySecs: i64nowSecs: i64: Seconds since the epoch, or -1 to read the system clock at verify time. A fixed value makes claim validation deterministic in tests.audience: string: "" means "do not check". An empty audience or issuer is not meaningful, so there is no ambiguity between "unset" and "must equal the empty string".issuer: stringrequireExp: bool
JwtVerifier.at
fn JwtVerifier.at(self: JwtVerifier, unixSecs: i64): JwtVerifierEvaluate the time claims against a fixed instant instead of the system clock. Seconds since the Unix epoch. This is what makes a claim test reproducible.
JwtVerifier.audience
fn JwtVerifier.audience(self: JwtVerifier, want: string): JwtVerifierRequire an aud claim naming want (string or array form).
JwtVerifier.issuer
fn JwtVerifier.issuer(self: JwtVerifier, want: string): JwtVerifierRequire an iss claim equal to want.
JwtVerifier.leeway
fn JwtVerifier.leeway(self: JwtVerifier, secs: i64): JwtVerifierTolerate secs of clock skew in both directions on exp/nbf/iat.
JwtVerifier.new
fn JwtVerifier.new(alg: JwtAlg, secret: &string): JwtVerifierA verifier that requires alg and secret, allows JWT_DEFAULT_LEEWAY of clock skew, and validates whatever of exp/nbf/iat the token carries.
The secret is borrowed and copied in: a verifier is usually built per request from one long-lived secret, and taking ownership would move that secret out of the caller's variable on first use.
JwtVerifier.requireExpiry
fn JwtVerifier.requireExpiry(self: JwtVerifier): JwtVerifierReject a token that carries no exp at all. Off by default because the claim is optional in RFC 7519; turn it on for anything session-shaped, where a token that never expires is a permanent credential.
JwtVerifier.verify
fn JwtVerifier.verify(self: &JwtVerifier, token: &string): Result<JwtClaims, JwtError>Check the signature and the claims, and hand back the payload.
Functions
jwtAlgName
pub fn jwtAlgName(alg: &JwtAlg): stringThe JOSE registry name, as it appears in the header.
jwtErrorMessage
pub fn jwtErrorMessage(err: &JwtError): stringOne-line, log-safe description of a rejection.