JWKS
Verify sign-in tokens from Auth0, Okta, Entra ID, Cognito or Keycloak against the provider's JWKS keys, picked by kid.
Version 1.0.0 · 3 functions · 1 crate
Functions
What use jwks.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use jwks_verify, jwks_keys from jwks (Quillo 0.6).
jwks_verify(token: str, jwks: str, audience: str, issuer: str) → anyor an errorThe claims of token, checked against the keys of jwks (the text of the provider's /.well-known/jwks.json, fetched with http.get and kept a while). The key is the one whose kid the token names (the only one when the token names none). audience and issuer are checked when not empty; exp and nbf with 30 s of leeway. Only RS*, PS*, ES* and EdDSA are taken, never HS*, so a token cannot pass by being signed with the public key as a secret.
jwks_keys(jwks: str) → listor an errorThe keys of a JWKS document: { kid, kty, alg, use } each, to see what a provider publishes.
jwks_sign(claims: any, private_key_pem: str, kid: str, expires_in: int) → stror an errorA token for claims signed RS256 with an RSA private key (PEM) and kid in its header: for an identity service of your own that publishes its JWKS, and for tests. iat is added, and exp when expires_in (seconds) is above 0.
Get it
$ quillo install jwks # jwks.qwer into this folder$ quillo install jwks --example # and its example, jwks.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/jwks.qwer$ curl -O https://quillolang.org/plugins/files/jwks.qwe$ curl -O https://quillolang.org/plugins/files/jwks_sample.json$ curl -O https://quillolang.org/plugins/files/jwks_sample_key.pemThen, in your program, all of it or only what you call:
use jwks.qweruse jwks_verify, jwks_keys from jwks // Quillo 0.6: only theseIt uses this crate, which cargo fetches the first time the plugin is built:
jsonwebtoken = "9"Example
// An API that takes the identity provider's tokens: the key is picked from its JWKS by `kid`.//// quillo run jwks.qwe then open http://localhost:8080/check//// In a real app the JWKS comes from the provider, fetched now and then:// let JWKS = http.get("https://YOUR_TENANT.auth0.com/.well-known/jwks.json")?.body// Here it is a sample file, and the sample private key stands in for the provider.use "jwks.qwer"let JWKS = read_file("jwks_sample.json")?let ISSUER = "https://id.example.com/"let AUDIENCE = "orders-api"fn signed_in(req) { let token = replace(req.headers.authorization ?? "", "Bearer ", "") let claims = jwks_verify(token, JWKS, AUDIENCE, ISSUER) if is_err(claims) { reply 401 { error: claims.error } } claims}route GET /keys = jwks_keys(JWKS)?route GET /check { let key = read_file("jwks_sample_key.pem")? let token = jwks_sign({ sub: "user-42", aud: AUDIENCE, iss: ISSUER }, key, "2026-11-key-1", 300)? let wrong_audience = jwks_sign({ sub: "user-42", aud: "billing", iss: ISSUER }, key, "2026-11-key-1", 300)? let unknown_kid = jwks_sign({ sub: "user-42", aud: AUDIENCE, iss: ISSUER }, key, "old-key", 300)? { user: jwks_verify(token, JWKS, AUDIENCE, ISSUER)?.sub, wrong_audience: jwks_verify(wrong_audience, JWKS, AUDIENCE, ISSUER).error, unknown_kid: jwks_verify(unknown_kid, JWKS, AUDIENCE, ISSUER).error, }}route GET /me = request | signed_in | ((c) => { user: c.sub })$ quillo run jwks.qwe$ curl 'localhost:8080/check'$ curl 'localhost:8080/keys'Source
//! Verify sign-in tokens from an identity provider (Auth0, Okta, Entra ID, Cognito, Keycloak) against its JWKS keys, picking the key by `kid`.//! version: 1.0.0//!//! [dependencies]//! jsonwebtoken = "9"use jsonwebtoken::jwk::{AlgorithmParameters, JwkSet};use jsonwebtoken::{decode, decode_header, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};use serde_json::{json, Value};fn now() -> i64 { std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_secs() as i64)}fn keys(jwks: &str) -> Result<JwkSet, String> { if jwks.len() > 1_000_000 { return Err("a JWKS document is at most 1 MB".into()); } serde_json::from_str(jwks).map_err(|e| format!("not a JWKS document: {}", e))}/// The claims of `token`, checked against the keys of `jwks` (the text of the provider's `/.well-known/jwks.json`, fetched with/// `http.get` and kept a while). The key is the one whose `kid` the token names (the only one when the token names none)./// `audience` and `issuer` are checked when not empty; `exp` and `nbf` with 30 s of leeway. Only RS*, PS*, ES* and EdDSA are taken,/// never HS*, so a token cannot pass by being signed with the public key as a secret.pub fn jwks_verify(token: &str, jwks: &str, audience: &str, issuer: &str) -> Result<Value, String> { let set = keys(jwks)?; let header = decode_header(token).map_err(|e| format!("not a token: {}", e))?; let jwk = match &header.kid { Some(kid) => set.find(kid).ok_or_else(|| format!("no key with kid `{}` in the JWKS: fetch it again, the provider may have rotated its keys", kid))?, None if set.keys.len() == 1 => &set.keys[0], None => return Err("the token names no kid, and the JWKS has several keys".into()), }; let fits = match (&jwk.algorithm, header.alg) { (AlgorithmParameters::RSA(_), Algorithm::RS256 | Algorithm::RS384 | Algorithm::RS512 | Algorithm::PS256 | Algorithm::PS384 | Algorithm::PS512) => true, (AlgorithmParameters::EllipticCurve(_), Algorithm::ES256 | Algorithm::ES384) => true, (AlgorithmParameters::OctetKeyPair(_), Algorithm::EdDSA) => true, _ => false, }; if !fits { return Err(format!("the token's algorithm {:?} does not go with the key's type", header.alg)); } let key = DecodingKey::from_jwk(jwk).map_err(|e| e.to_string())?; let mut v = Validation::new(header.alg); v.required_spec_claims.clear(); v.leeway = 30; if !audience.is_empty() { v.set_audience(&[audience]); } else { v.validate_aud = false; } if !issuer.is_empty() { v.set_issuer(&[issuer]); } decode::<Value>(token, &key, &v).map(|d| d.claims).map_err(|e| e.to_string())}/// The keys of a JWKS document: `{ kid, kty, alg, use }` each, to see what a provider publishes.pub fn jwks_keys(jwks: &str) -> Result<Vec<Value>, String> { let set = keys(jwks)?; Ok(set.keys.iter().map(|k| { let kty = match &k.algorithm { AlgorithmParameters::RSA(_) => "RSA", AlgorithmParameters::EllipticCurve(_) => "EC", AlgorithmParameters::OctetKeyPair(_) => "OKP", AlgorithmParameters::OctetKey(_) => "oct", }; json!({ "kid": k.common.key_id, "kty": kty, "alg": k.common.key_algorithm.map(|a| a.to_string()), "use": k.common.public_key_use.as_ref().map(|u| format!("{:?}", u).to_lowercase()), }) }).collect())}/// A token for `claims` signed RS256 with an RSA private key (PEM) and `kid` in its header: for an identity service of your own/// that publishes its JWKS, and for tests. `iat` is added, and `exp` when `expires_in` (seconds) is above 0.pub fn jwks_sign(claims: serde_json::Value, private_key_pem: &str, kid: &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()); } let mut h = Header::new(Algorithm::RS256); if !kid.is_empty() { h.kid = Some(kid.to_string()); } let key = EncodingKey::from_rsa_pem(private_key_pem.as_bytes()).map_err(|e| e.to_string())?; encode(&h, &Value::Object(c), &key).map_err(|e| e.to_string())}Related
jwt.qwer · 2.0.0JSON 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.7 views · 8 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 JWKS: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.