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);
}
}