JWT
JSON Web Tokens: sign and verify HS256 tokens for an API, check a token's audience and issuer, verify RS256, ES256 and EdDSA tokens against an identity provider's JWKS, peek at claims.
Version 2.0.0 · 6 functions · 2 crates
Functions
What use jwt.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use jwt_sign, jwt_verify from jwt (Quillo 0.6).
jwt_sign(claims: any, secret: str, expires_in: int) → stror an errorA token for claims, signed with HMAC-SHA256: iat is added, and exp when expires_in (seconds) is above 0.
jwt_verify(token: str, secret: str) → anyor an errorThe claims of an HS256 token signed with secret, or an error: a bad signature, an expired token, one with no exp, another algorithm.
jwt_check(token: str, secret: str, options: any) → anyor an errorjwt_verify, with options: audience (aud must name it), issuer (iss must be it), require (the claims it must have, ["exp"] unless given) and leeway (seconds).
jwt_verify_rs256(token: str, public_key_pem: str, audience: str) → anyor an errorThe claims of an RS256 token, checked with the issuer's public key (PEM) and its audience (aud, empty for none).
jwt_verify_jwks(token: str, options: any) → anyor an errorThe claims of an access token from an identity provider, checked against its JWKS: options has jwks_url, and audience, issuer, require, leeway as jwt_check takes them, and algorithms (RS256, ES256 and EdDSA by default; never HS256 or none). A kid the keys do not have fetches them again.
jwt_peek(token: str) → anyor an errorA token's claims without checking anything: to show or log them, never to trust them.
Get it
$ quillo install jwt # jwt.qwer into this folder$ quillo install jwt --example # and its example, jwt.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/jwt.qwer$ curl -O https://quillolang.org/plugins/files/jwt.qweThen, in your program, all of it or only what you call:
use jwt.qweruse jwt_sign, jwt_verify from jwt // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
jsonwebtoken = "9"ureq = "2"Example
// An API that hands out tokens and checks them.//// quillo run jwt.qwe then open http://localhost:8080/token// curl -H "authorization: Bearer <token>" localhost:8080/meuse "jwt.qwer"let SECRET = env("JWT_SECRET") ?? "change me: at least 32 random characters"route GET /token = { token: jwt_sign({ sub: "user-7", role: "admin" }, SECRET, 3600)? }route GET /me { let token = replace(request.headers.authorization ?? "", "Bearer ", "") let claims = jwt_verify(token, SECRET) if is_err(claims) { reply 401 { error: claims.error } } { user: claims.sub, role: claims.role }}// A token for another service, as an identity provider issues one: its audience and issuer// checked, and one with no `exp` refused (it would be good for ever).route GET /audience { let t = jwt_sign({ sub: "service:shop", aud: "orders", iss: "idp" }, SECRET, 60)? let forever = jwt_sign({ sub: "service:shop" }, SECRET, 0)? { orders: jwt_check(t, SECRET, { audience: "orders", issuer: "idp" })?.sub, not_billing: error(jwt_check(t, SECRET, { audience: "billing" })), no_exp: error(jwt_verify(forever, SECRET)), plain: jwt_verify(t, SECRET)?.aud, }}route GET /check { let t = jwt_sign({ sub: "user-7" }, SECRET, 60)? { verified: jwt_verify(t, SECRET)?.sub, wrong_secret: is_err(jwt_verify(t, "another secret")), peek: jwt_peek(t)?.sub }}$ quillo run jwt.qwe$ curl 'localhost:8080/token'$ curl 'localhost:8080/check'$ curl 'localhost:8080/audience'Source
//! JSON Web Tokens: sign and verify HS256 tokens, check a token's audience and issuer, and verify RS256, ES256 and EdDSA tokens against a key or an identity provider's JWKS.//! version: 2.0.0//!//! [dependencies]//! jsonwebtoken = "9"//! ureq = "2"use jsonwebtoken::{decode, decode_header, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};use serde_json::Value;use std::collections::HashMap;use std::sync::Mutex;use std::time::{Duration, Instant};fn now() -> i64 { std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_secs() as i64)}/// The checks every verifier makes: the signature, `exp` (which a token must have: one without/// is good for ever), `nbf`, and 30 s of leeway for clocks that differ. `aud` is checked only when/// one is asked for, so a token that names one is not refused for it.fn validation(alg: Algorithm) -> Validation { let mut v = Validation::new(alg); v.required_spec_claims = ["exp".to_string()].into_iter().collect(); v.validate_nbf = true; v.validate_aud = false; v.leeway = 30; v}/// `options` of `jwt_check` and `jwt_verify_jwks`: `audience`, `issuer`, `require` (claims a token/// must have, `exp` by default; `[]` for none) and `leeway`.fn apply(v: &mut Validation, options: &Value) -> Result<(), String> { if let Value::Object(o) = options { for k in o.keys() { if !["audience", "issuer", "require", "leeway", "jwks_url", "algorithms"].contains(&k.as_str()) { return Err(format!("no option `{}` (audience, issuer, require, leeway)", k)); } } } if let Some(a) = options["audience"].as_str() { v.set_audience(&[a]); v.validate_aud = true; } if let Some(i) = options["issuer"].as_str() { v.set_issuer(&[i]); } if let Some(r) = options["require"].as_array() { v.required_spec_claims = r.iter().filter_map(|c| c.as_str().map(str::to_string)).collect(); } if let Some(l) = options["leeway"].as_u64() { v.leeway = l; } Ok(())}/// A token for `claims`, signed with HMAC-SHA256: `iat` is added, and `exp` when `expires_in` (seconds) is above 0.pub fn jwt_sign(claims: serde_json::Value, secret: &str, expires_in: i64) -> Result<String, String> { let Value::Object(mut c) = claims else { return Err("the claims are a map".into()) }; c.insert("iat".into(), now().into()); if expires_in > 0 { c.insert("exp".into(), (now() + expires_in).into()); } encode(&Header::default(), &Value::Object(c), &EncodingKey::from_secret(secret.as_bytes())).map_err(|e| e.to_string())}/// The claims of an HS256 token signed with `secret`, or an error: a bad signature, an expired token, one with no `exp`, another algorithm.pub fn jwt_verify(token: &str, secret: &str) -> Result<Value, String> { decode::<Value>(token, &DecodingKey::from_secret(secret.as_bytes()), &validation(Algorithm::HS256)).map(|d| d.claims).map_err(|e| e.to_string())}/// `jwt_verify`, with `options`: `audience` (`aud` must name it), `issuer` (`iss` must be it), `require` (the claims it must have, `["exp"]` unless given) and `leeway` (seconds).pub fn jwt_check(token: &str, secret: &str, options: serde_json::Value) -> Result<Value, String> { let mut v = validation(Algorithm::HS256); apply(&mut v, &options)?; decode::<Value>(token, &DecodingKey::from_secret(secret.as_bytes()), &v).map(|d| d.claims).map_err(|e| e.to_string())}/// The claims of an RS256 token, checked with the issuer's public key (PEM) and its audience (`aud`, empty for none).pub fn jwt_verify_rs256(token: &str, public_key_pem: &str, audience: &str) -> Result<Value, String> { let key = DecodingKey::from_rsa_pem(public_key_pem.as_bytes()).map_err(|e| e.to_string())?; let mut v = validation(Algorithm::RS256); if !audience.is_empty() { v.set_audience(&[audience]); v.validate_aud = true; } decode::<Value>(token, &key, &v).map(|d| d.claims).map_err(|e| e.to_string())}/// Each JWKS fetched: its keys, when, and when a token with a new `kid` last fetched it again.static JWKS: Mutex<Option<HashMap<String, (jsonwebtoken::jwk::JwkSet, Instant, Option<Instant>)>>> = Mutex::new(None);fn jwks(url: &str, again: bool) -> Result<jsonwebtoken::jwk::JwkSet, String> { let held = JWKS.lock().unwrap().as_ref().and_then(|c| c.get(url).cloned()); if let Some((set, at, forced)) = &held { // A `kid` nobody knows fetches the keys again, at most every 30 s, so a forged token cannot make us flood the provider. let keep = if again { forced.is_some_and(|f| f.elapsed() < Duration::from_secs(30)) } else { at.elapsed() < Duration::from_secs(600) }; if keep { return Ok(set.clone()); } } let local = ["http://localhost", "http://127.0.0.1", "http://[::1]"].iter().any(|p| url.starts_with(p)); if !url.starts_with("https://") && !local { return Err(format!("the JWKS `{}` is not https", url)); } let text = ureq::AgentBuilder::new().timeout(Duration::from_secs(10)).build().get(url).call().map_err(|e| format!("{}: {}", url, e))?.into_string().map_err(|e| e.to_string())?; let set: jsonwebtoken::jwk::JwkSet = serde_json::from_str(&text).map_err(|e| format!("{} is not a JWKS: {}", url, e))?; let forced = if again { Some(Instant::now()) } else { held.and_then(|h| h.2) }; JWKS.lock().unwrap().get_or_insert_with(HashMap::new).insert(url.to_string(), (set.clone(), Instant::now(), forced)); Ok(set)}/// The claims of an access token from an identity provider, checked against its JWKS: `options` has `jwks_url`, and `audience`, `issuer`, `require`, `leeway` as `jwt_check` takes them, and `algorithms` (RS256, ES256 and EdDSA by default; never HS256 or `none`). A `kid` the keys do not have fetches them again.pub fn jwt_verify_jwks(token: &str, options: serde_json::Value) -> Result<Value, String> { let url = options["jwks_url"].as_str().ok_or("options: jwks_url, the identity provider's keys")?; let header = decode_header(token).map_err(|e| e.to_string())?; let allowed: Vec<String> = match options["algorithms"].as_array() { Some(l) => l.iter().filter_map(|a| a.as_str().map(str::to_string)).collect(), None => vec!["RS256".into(), "ES256".into(), "EdDSA".into()], }; let name = format!("{:?}", header.alg); if name.starts_with("HS") || !allowed.iter().any(|a| *a == name) { return Err(format!("algorithm `{}` is refused: {} are taken", name, allowed.join(", "))); } let kid = header.kid.clone(); let find = |set: &jsonwebtoken::jwk::JwkSet| match &kid { Some(k) => set.find(k).cloned(), None if set.keys.len() == 1 => set.keys.first().cloned(), None => None, }; let key = match find(&jwks(url, false)?) { Some(k) => k, None => find(&jwks(url, true)?).ok_or_else(|| format!("no key in {} for kid {}", url, kid.as_deref().unwrap_or("(none)")))?, }; let mut v = validation(header.alg); apply(&mut v, &options)?; let key = DecodingKey::from_jwk(&key).map_err(|e| e.to_string())?; decode::<Value>(token, &key, &v).map(|d| d.claims).map_err(|e| e.to_string())}/// A token's claims without checking anything: to show or log them, never to trust them.pub fn jwt_peek(token: &str) -> Result<Value, String> { let mut v = Validation::new(Algorithm::HS256); v.insecure_disable_signature_validation(); v.required_spec_claims.clear(); v.validate_exp = false; v.validate_aud = false; v.algorithms = vec![Algorithm::HS256, Algorithm::HS384, Algorithm::HS512, Algorithm::RS256, Algorithm::RS384, Algorithm::RS512, Algorithm::ES256, Algorithm::ES384, Algorithm::PS256, Algorithm::EdDSA]; decode::<Value>(token, &DecodingKey::from_secret(&[]), &v).map(|d| d.claims).map_err(|e| e.to_string())}Related
jwks.qwer · 1.0.0Verify sign-in tokens from Auth0, Okta, Entra ID, Cognito or Keycloak against the provider's JWKS keys, picked by kid.8 views · 6 downloadsBiscuit tokens biscuit.qwer · 1.0.0Biscuit authorization tokens: issued with Datalog facts and checks signed by your key, narrowed by their holder without asking the issuer, and authorized offline against each request.7 views · 8 downloadsPASETO paseto.qwer · 1.0.0PASETO v4 tokens, the safer JWT: v4.local encrypted with a shared key and v4.public signed with Ed25519, expiry checked.7 views · 9 downloadsOAuth PKCE oauth_pkce.qwer · 1.0.0Sign in with Google, GitHub or Microsoft made safe: PKCE verifier and challenge, state, the authorization link and the token request.6 views · 6 downloadsRate limits rate_limit.qwer · 1.0.0Rate limits per key kept in memory (GCRA): allowed or retry after so many milliseconds, for sign-ins and APIs.7 views · 6 downloadsArgon2 passwords argon2.qwer · 1.0.0Argon2id password hashes with the memory and passes you choose, any Argon2 hash verified, its parameters read, old ones marked for a rehash, and the cost timed on this server.7 views · 6 downloadsThread 0 posts
On JWT: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.