From d62cfcd1d90239ac0c69b0b3dd9315516f7ede46 Mon Sep 17 00:00:00 2001 From: Philippe Loctaux Date: Sun, 12 Mar 2023 18:46:58 +0100 Subject: [PATCH] ezidam: added jwks route in well-known --- Cargo.lock | 16 +++++++++ crates/ezidam/Cargo.toml | 1 + crates/ezidam/src/error/conversion.rs | 6 ++++ crates/ezidam/src/routes/well_known.rs | 47 +++++++++++++++++++++++++- crates/jwt/src/error.rs | 3 ++ crates/jwt/src/key/public.rs | 28 +++++++++++---- 6 files changed, 94 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e655a40..4358886 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -771,6 +771,7 @@ version = "0.1.0" dependencies = [ "database_pool", "erased-serde", + "futures", "hash", "id", "identicon-rs", @@ -911,6 +912,7 @@ checksum = "13e2792b0ff0340399d58445b88fd9770e3489eff258a4cbc1523418f12abf84" dependencies = [ "futures-channel", "futures-core", + "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -961,6 +963,17 @@ version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfb8371b6fb2aeb2d280374607aeabfc99d95c72edfe51692e42d3d7f0d08531" +[[package]] +name = "futures-macro" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures-sink" version = "0.3.26" @@ -982,6 +995,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -1483,6 +1497,8 @@ dependencies = [ name = "jwt" version = "0.0.0" dependencies = [ + "chrono", + "database", "id", "jwt-compact", "rand", diff --git a/crates/ezidam/Cargo.toml b/crates/ezidam/Cargo.toml index e01f889..c358745 100644 --- a/crates/ezidam/Cargo.toml +++ b/crates/ezidam/Cargo.toml @@ -11,6 +11,7 @@ infer = { version = "0.12.0", default-features = false } erased-serde = "0.3" url = { workspace = true } identicon-rs = "4.0.1" +futures = "0.3.26" # local crates database_pool = { path = "../database_pool" } diff --git a/crates/ezidam/src/error/conversion.rs b/crates/ezidam/src/error/conversion.rs index 277df79..b56fc24 100644 --- a/crates/ezidam/src/error/conversion.rs +++ b/crates/ezidam/src/error/conversion.rs @@ -35,3 +35,9 @@ impl From for Error { Error::internal_server_error(e) } } + +impl From for Error { + fn from(e: jwt::Error) -> Self { + Error::internal_server_error(e) + } +} diff --git a/crates/ezidam/src/routes/well_known.rs b/crates/ezidam/src/routes/well_known.rs index d3fd0ee..7cb17f7 100644 --- a/crates/ezidam/src/routes/well_known.rs +++ b/crates/ezidam/src/routes/well_known.rs @@ -1,10 +1,13 @@ use super::prelude::*; +use futures::future::join_all; +use jwt::database::Key; +use jwt::PublicKey; use rocket::get; use rocket::serde::json::{Json, Value}; use settings::Settings; pub fn routes() -> Vec { - routes![openid_configuration] + routes![openid_configuration, json_web_keys] } #[get("/openid-configuration")] @@ -24,6 +27,35 @@ async fn openid_configuration(mut db: Connection) -> Result) -> Result>> { + // Get keys + let keys = Key::get_all(&mut *db, Some(false)).await?; + + // For each key, import as public key and extract the jwk + let json_web_keys = join_all(keys.into_iter().map(|key| { + task::spawn_blocking(move || { + // Import public key + PublicKey::try_from(key) + }) + })) + .await + // Handle JoinError if one is found + .into_iter() + .collect::, _>>()? + // Collect all values, return error immediately if one is found + .into_iter() + .collect::, _>>()? + // Extract jwk + .into_iter() + .map(|key| key.jwk()) + // Collect all values, return error immediately if one is found + .collect::, _>>()?; + + // HTTP response + Ok(Json(json_web_keys)) +} + #[cfg(test)] mod test { use crate::tests::*; @@ -58,4 +90,17 @@ mod test { .dispatch(); assert_ne!(response.status(), Status::Ok); } + + #[test] + fn jwks() { + // Setup http server + let client = setup_rocket_testing(); + + // Make request + let response = client.get(uri!("/.well-known/jwks.json")).dispatch(); + assert_eq!(response.status(), Status::Ok); + + // Make sure it is valid json + assert!(response.into_json::().is_some()); + } } diff --git a/crates/jwt/src/error.rs b/crates/jwt/src/error.rs index 32a2aea..92b79cb 100644 --- a/crates/jwt/src/error.rs +++ b/crates/jwt/src/error.rs @@ -17,4 +17,7 @@ pub enum Error { #[error("Failed to parse JWT: `{0}`")] JwtParsing(#[from] jwt_compact::ParseError), + + #[error("Failed to serialize JWK: `{0}`")] + JwkSerialization(#[from] serde_json::Error), } diff --git a/crates/jwt/src/key/public.rs b/crates/jwt/src/key/public.rs index 1cf928e..b7930f0 100644 --- a/crates/jwt/src/key/public.rs +++ b/crates/jwt/src/key/public.rs @@ -1,3 +1,4 @@ +use crate::database::Key; use crate::jwk::JsonWebKey; use crate::Error; use id::KeyID; @@ -5,12 +6,21 @@ use jwt_compact::alg::{Rsa, RsaPublicKey, StrongKey}; use jwt_compact::jwk::JsonWebKey as JsonWebKeyBase; use jwt_compact::Algorithm; use rsa::pkcs1::{DecodeRsaPublicKey, EncodeRsaPublicKey}; +use serde_json::Value; pub struct PublicKey { id: KeyID, key: RsaPublicKey, } +impl TryFrom for PublicKey { + type Error = Error; + + fn try_from(key: Key) -> Result { + PublicKey::from_der(key.public_der(), key.key_id()) + } +} + impl PublicKey { pub fn new(id: &KeyID, key: StrongKey) -> Self { Self { @@ -30,12 +40,16 @@ impl PublicKey { }) } - pub fn jwk(&self) -> JsonWebKey { - JsonWebKey { + pub fn jwk(&self) -> Result { + Ok(serde_json::to_value(JsonWebKey { base: JsonWebKeyBase::from(&self.key), key_id: self.id.as_ref(), algorithm: Rsa::ps256().name(), - } + })?) + } + + pub fn key_id(&self) -> &KeyID { + &self.id } } @@ -82,11 +96,13 @@ mod tests { let premade_der = include_bytes!("../../tests/public_key.der"); let public_key = PublicKey::from_der(premade_der, &key_id).unwrap(); - let generated_jwk = serde_json::to_string(&public_key.jwk()); + // Generate jwk + let generated_jwk = public_key.jwk(); assert!(generated_jwk.is_ok()); - let jwk = r#"{"kty":"RSA","n":"3os0j_kNfdTHJVQ-eMYXyRBWIqsrJDdELxLAh3_WlOZtsBwiGVNnpHQm9cRB63Un9UJpYGbWz38emglXc8bHPrUArDl-K_5ioDlbh7hAaz3rZ6b8LDIPUO-jYICdxBdv1THXSWbTEistZF1TYsXg7G4xrxiFKnZLBNaSgJrKOAY8AUNWuby-vZKr5X9e3SG7kvPsUITyqSmDz4ZTCj4QScx4O9gyqz1_UEBxTRSKcpS82YzAo2Byo5avRWesiGoaxs8lNv0QJ22IY1KVoROv3hHFeFEcg3D4NTfFG2Cd8d1OMXfILhtFnQZbt5ZxIG9SCOfirn32-9OtoLemKlgSq0gbLf6t1OK12LK6mIJ78pphlnhHdvHeJ75PV6c2lS2Wwd75NYBJzhIojG4U4Lbpe7T_NDFaxExry_7V5oxX8tbb-OzJnuPOQRR0H5uOBjdVo7i5vjnDKOTDpro3XPQjBbIBkABhDdU2FcXkEbl8_ByyYZZni7ekzGrVSJB_vxvv","e":"AQAB","kid":"SgTG8ulMHAp5UsGWuCclw36zWsdEo5","alg":"PS256"}"#; - let generated_jwk = generated_jwk.unwrap(); + // Convert to string to verify easily + let generated_jwk = serde_json::to_string(&generated_jwk.unwrap()).unwrap(); + let jwk = r#"{"alg":"PS256","e":"AQAB","kid":"SgTG8ulMHAp5UsGWuCclw36zWsdEo5","kty":"RSA","n":"3os0j_kNfdTHJVQ-eMYXyRBWIqsrJDdELxLAh3_WlOZtsBwiGVNnpHQm9cRB63Un9UJpYGbWz38emglXc8bHPrUArDl-K_5ioDlbh7hAaz3rZ6b8LDIPUO-jYICdxBdv1THXSWbTEistZF1TYsXg7G4xrxiFKnZLBNaSgJrKOAY8AUNWuby-vZKr5X9e3SG7kvPsUITyqSmDz4ZTCj4QScx4O9gyqz1_UEBxTRSKcpS82YzAo2Byo5avRWesiGoaxs8lNv0QJ22IY1KVoROv3hHFeFEcg3D4NTfFG2Cd8d1OMXfILhtFnQZbt5ZxIG9SCOfirn32-9OtoLemKlgSq0gbLf6t1OK12LK6mIJ78pphlnhHdvHeJ75PV6c2lS2Wwd75NYBJzhIojG4U4Lbpe7T_NDFaxExry_7V5oxX8tbb-OzJnuPOQRR0H5uOBjdVo7i5vjnDKOTDpro3XPQjBbIBkABhDdU2FcXkEbl8_ByyYZZni7ekzGrVSJB_vxvv"}"#; assert_eq!(jwk, generated_jwk); } }