Security

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

#auth#tokens#sso#api#oidc

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 error

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.

jwks_keys(jwks: str) → listor an error

The 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 error

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.

Get it

$ quillo install jwks               # jwks.qwer into this folder$ quillo install jwks --example     # and its example, jwks.qwe

Or 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.pem

Then, in your program, all of it or only what you call:

main.qwe
use jwks.qweruse jwks_verify, jwks_keys from jwks      // Quillo 0.6: only these

It uses this crate, which cargo fetches the first time the plugin is built:

[dependencies]
jsonwebtoken = "9"

Example

jwks.qwe
// 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

jwks.qwer
//! 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())}
Thread
86Useful? Like it, or add to its thread below.

Thread 0 posts

On JWKS: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.