Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 39 additions & 22 deletions crates/oidc-provider/src/jwks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,31 @@ use base64::Engine;
use rsa::traits::PublicKeyParts;
use rsa::RsaPublicKey;

/// Generate a JWKS JSON response containing the proxy's RSA public key.
/// Generate a JWKS JSON response containing one or more RSA public keys.
///
/// This is served at the JWKS URI referenced by the OIDC discovery document,
/// allowing relying parties (cloud providers) to verify JWTs signed by the proxy.
pub fn jwks_json(public_key: &RsaPublicKey, kid: &str) -> String {
/// Multiple keys support key rotation.
pub fn jwks_json(keys: &[(&RsaPublicKey, &str)]) -> String {
let b64 = &base64::engine::general_purpose::URL_SAFE_NO_PAD;

let n = public_key.n();
let e = public_key.e();

let n_b64 = b64.encode(n.to_bytes_be());
let e_b64 = b64.encode(e.to_bytes_be());

let jwks = serde_json::json!({
"keys": [{
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"kid": kid,
"n": n_b64,
"e": e_b64,
}]
});

jwks.to_string()
let jwk_entries: Vec<serde_json::Value> = keys
.iter()
.map(|(public_key, kid)| {
let n_b64 = b64.encode(public_key.n().to_bytes_be());
let e_b64 = b64.encode(public_key.e().to_bytes_be());
serde_json::json!({
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"kid": kid,
"n": n_b64,
"e": e_b64,
})
})
.collect();

serde_json::json!({ "keys": jwk_entries }).to_string()
}

#[cfg(test)]
Expand All @@ -42,7 +42,7 @@ mod tests {
let private_key = RsaPrivateKey::new(&mut rng, 2048).unwrap();
let public_key: &RsaPublicKey = private_key.as_ref();

let json_str = jwks_json(public_key, "my-kid");
let json_str = jwks_json(&[(public_key, "my-kid")]);
let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();

let keys = parsed["keys"].as_array().unwrap();
Expand All @@ -57,6 +57,23 @@ mod tests {
assert!(key["e"].as_str().unwrap().len() > 0);
}

#[test]
fn jwks_contains_multiple_keys() {
let mut rng = rand::rngs::OsRng;
let key1 = RsaPrivateKey::new(&mut rng, 2048).unwrap();
let pub1: &RsaPublicKey = key1.as_ref();
let key2 = RsaPrivateKey::new(&mut rng, 2048).unwrap();
let pub2: &RsaPublicKey = key2.as_ref();

let json_str = jwks_json(&[(pub1, "kid-1"), (pub2, "kid-2")]);
let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();

let keys = parsed["keys"].as_array().unwrap();
assert_eq!(keys.len(), 2);
assert_eq!(keys[0]["kid"], "kid-1");
assert_eq!(keys[1]["kid"], "kid-2");
}

#[test]
fn jwks_roundtrips_through_rsa() {
use rsa::BigUint;
Expand All @@ -65,7 +82,7 @@ mod tests {
let private_key = RsaPrivateKey::new(&mut rng, 2048).unwrap();
let public_key: &RsaPublicKey = private_key.as_ref();

let json_str = jwks_json(public_key, "k1");
let json_str = jwks_json(&[(public_key, "k1")]);
let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();

let key = &parsed["keys"][0];
Expand Down
2 changes: 1 addition & 1 deletion crates/oidc-provider/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ mod tests {
let token = signer.sign("sub", "iss", "aud", &[]).unwrap();

// Generate JWKS from the same signer
let jwks_str = jwks::jwks_json(signer.public_key(), signer.kid());
let jwks_str = jwks::jwks_json(&[(signer.public_key(), signer.kid())]);
let jwks: serde_json::Value = serde_json::from_str(&jwks_str).unwrap();

// Extract public key from JWKS
Expand Down
18 changes: 13 additions & 5 deletions crates/oidc-provider/src/route_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,20 @@ impl RouteHandler for OidcConfigHandler {

/// Handler that serves the JWKS (JSON Web Key Set) document.
struct OidcJwksHandler {
signer: JwtSigner,
signers: Vec<JwtSigner>,
}

impl RouteHandler for OidcJwksHandler {
fn handle<'a>(&'a self, req: &'a RequestInfo<'a>) -> RouteHandlerFuture<'a> {
if req.method.as_str() != "GET" {
return Box::pin(async { None });
}
let json = jwks_json(self.signer.public_key(), self.signer.kid());
let keys: Vec<_> = self
.signers
.iter()
.map(|s| (s.public_key(), s.kid()))
.collect();
let json = jwks_json(&keys);
Box::pin(async move { Some(ProxyResult::json(200, json)) })
}
}
Expand All @@ -44,17 +49,20 @@ impl RouteHandler for OidcJwksHandler {
pub trait OidcRouterExt {
/// Register `/.well-known/openid-configuration` and `/.well-known/jwks.json`
/// routes backed by the given issuer and signer.
fn with_oidc_discovery(self, issuer: String, signer: JwtSigner) -> Self;
///
/// `additional_signers` contains previous keys that should still appear in
/// the JWKS for key rotation (they are not used for signing new tokens).
fn with_oidc_discovery(self, issuer: String, signers: Vec<JwtSigner>) -> Self;
}

impl OidcRouterExt for Router {
fn with_oidc_discovery(self, issuer: String, signer: JwtSigner) -> Self {
fn with_oidc_discovery(self, issuer: String, signers: Vec<JwtSigner>) -> Self {
let jwks_uri = format!("{}/.well-known/jwks.json", issuer);

self.route(
"/.well-known/openid-configuration",
OidcConfigHandler { issuer, jwks_uri },
)
.route("/.well-known/jwks.json", OidcJwksHandler { signer })
.route("/.well-known/jwks.json", OidcJwksHandler { signers })
}
}
2 changes: 1 addition & 1 deletion examples/cf-workers/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ async fn fetch(req: web_sys::Request, env: Env, _ctx: Context) -> Result<web_sys
// Build router with OIDC discovery (if configured) and STS.
let mut router = Router::new();
if let (Some(signer), Some(issuer)) = (oidc_signer, oidc_issuer) {
router = router.with_oidc_discovery(issuer, signer);
router = router.with_oidc_discovery(issuer, vec![signer]);
}
router = router.with_sts(sts_creds, jwks_cache, token_key.clone());

Expand Down
2 changes: 1 addition & 1 deletion examples/lambda/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ async fn main() -> Result<(), Error> {
// Build router with OIDC discovery (if configured) and STS.
let mut router = Router::new();
if let (Some(signer), Some(issuer)) = (oidc_signer, oidc_issuer) {
router = router.with_oidc_discovery(issuer, signer);
router = router.with_oidc_discovery(issuer, vec![signer]);
}
router = router.with_sts(sts_creds, jwks_cache, token_key.clone());

Expand Down
2 changes: 1 addition & 1 deletion examples/server/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ where
// Build router with OIDC discovery (if configured) and STS.
let mut proxy_router = ProxyRouter::new();
if let (Some(signer), Some(issuer)) = (oidc_signer, oidc_issuer) {
proxy_router = proxy_router.with_oidc_discovery(issuer, signer);
proxy_router = proxy_router.with_oidc_discovery(issuer, vec![signer]);
}
proxy_router = proxy_router.with_sts(sts_creds, jwks_cache, token_key.clone());

Expand Down
Loading