diff --git a/config/README.md b/config/README.md index e21e48c..4f3f8d6 100644 --- a/config/README.md +++ b/config/README.md @@ -18,3 +18,7 @@ curve name: prime256v1 (secp256r1, secp384r1) (Ed25519 - EdDSA signature scheme using SHA-512 (SHA-2) and Curve25519) > openssl genpkey -algorithm ed25519 + +## JWK - combined file of above keys + +> rnbyc -j -f rsa-public1.pem -k rsa01 -a RS256 -f ecdsa-public1.pem -k ec01 -a ES256 -f ed25519-public1.pem -k ed01 -a EdDSA -o public1.jw diff --git a/config/public1.jwks b/config/public1.jwks new file mode 100644 index 0000000..0c67ebe --- /dev/null +++ b/config/public1.jwks @@ -0,0 +1,26 @@ +{ + "keys": [ + { + "alg": "RS256", + "e": "AQAB", + "kid": "rsa01", + "kty": "RSA", + "n": "2pQeZdxa7q093K7bj5h6-leIpxfTnuAxzXdhjfGEJHxmt2ekHyCBWWWXCBiDn2RTcEBcy6gZqOW45Uy_tw-5e-Px1xFj1PykGEkRlOpYSAeWsNaAWvvpGB9m4zQ0PgZeMDDXE5IIBrY6YAzmGQxV-fcGGLhJnXl0-5_z7tKC7RvBoT3SGwlc_AmJqpFtTpEBn_fDnyqiZbpcjXYLExFpExm41xDitRKHWIwfc3dV8_vlNntlxCPGy_THkjdXJoHv2IJmlhvmr5_h03iGMLWDKSywxOol_4Wc1BT7Hb6byMxW40GKwSJJ4p7W8eI5mqggRHc8jlwSsTN9LZ2VOvO-XiVShZRVg7JeraGAfWwaIgIJ1D8C1h5Pi0iFpp2suxpHAXHfyLMJXuVotpXbDh4NDX-A4KRMgaxcfAcui_x6gybksq6gF90-9nfQfmVMVJctZ6M-FvRr-itd1Nef5WAtwUp1qyZygAXU3cH3rarscajmurOsP6dE1OHl3grY_eZhQxk33VBK9lavqNKPg6Q_PLiq1ojbYBj3bcYifJrsNeQwxldQP83aWt5rGtgZTehKVJwa40Uy_Grae1iRnsDtdSy5sTJIJ6EiShnWAdMoGejdiI8vpkjrdU8SWH8lv1KXI54DsbyAuke2cYz02zPWc6JEotQqI0HwhzU0KHyoY4s" + }, + { + "alg": "ES256", + "crv": "P-256", + "kid": "ec01", + "kty": "EC", + "x": "MZiwc5EVP_E3vkd2oKedr4lWVMN9vgdyBBpBIVFJjwY", + "y": "1npLU75B6M0mb01zUAVoeYJSDOlQJmvjBdqLPjJvy3Y" + }, + { + "alg": "EdDSA", + "crv": "Ed25519", + "kid": "ed01", + "kty": "OKP", + "x": "uWtSkE-I9aTMYTTvuTE1rtu0rNdxp3DU33cJ_ksL1Gk" + } + ] +} diff --git a/jwt-authorizer/src/authorizer.rs b/jwt-authorizer/src/authorizer.rs index 825c5b3..0b3248b 100644 --- a/jwt-authorizer/src/authorizer.rs +++ b/jwt-authorizer/src/authorizer.rs @@ -41,6 +41,7 @@ pub enum KeySourceType { EDString(String), Secret(String), Jwks(String), + JwksPath(String), JwksString(String), // TODO: expose JwksString in JwtAuthorizer or remove it Discovery(String), } @@ -148,13 +149,36 @@ where jwt_source, } } + KeySourceType::JwksPath(path) => { + let set: JwkSet = serde_json::from_slice(&read_data(path.as_str())?)?; + let keys = set + .keys + .iter() + .map(|k| match KeyData::from_jwk(k) { + Ok(kdata) => Ok(Arc::new(kdata)), + Err(err) => Err(InitError::KeyDecodingError(err)), + }) + .collect::, _>>()?; + Authorizer { + key_source: KeySource::MultiKeySource(keys.into()), + claims_checker, + validation, + jwt_source, + } + } KeySourceType::JwksString(jwks_str) => { // TODO: expose it in JwtAuthorizer or remove let set: JwkSet = serde_json::from_str(jwks_str.as_str())?; - // TODO: replace [0] by kid/alg search - let k = KeyData::from_jwk(&set.keys[0]).map_err(InitError::KeyDecodingError)?; + let keys = set + .keys + .iter() + .map(|k| match KeyData::from_jwk(k) { + Ok(kdata) => Ok(Arc::new(kdata)), + Err(err) => Err(InitError::KeyDecodingError(err)), + }) + .collect::, _>>()?; Authorizer { - key_source: KeySource::SingleKeySource(Arc::new(k)), + key_source: KeySource::MultiKeySource(keys.into()), claims_checker, validation, jwt_source, @@ -363,6 +387,28 @@ mod tests { .unwrap(); let k = a.key_source.get_key(Header::new(Algorithm::EdDSA)); assert!(k.await.is_ok()); + + let a = Authorizer::::build( + KeySourceType::JwksPath("../config/public1.jwks".to_owned()), + None, + None, + Validation::new(), + JwtSource::AuthorizationHeader, + ) + .await + .unwrap(); + a.key_source + .get_key(Header::new(Algorithm::RS256)) + .await + .expect("Couldn't get RS256 key from jwk"); + a.key_source + .get_key(Header::new(Algorithm::ES256)) + .await + .expect("Couldn't get ES256 key from jwk"); + a.key_source + .get_key(Header::new(Algorithm::EdDSA)) + .await + .expect("Couldn't get EdDSA key from jwk"); } #[tokio::test] diff --git a/jwt-authorizer/src/builder.rs b/jwt-authorizer/src/builder.rs index 925d532..46dece9 100644 --- a/jwt-authorizer/src/builder.rs +++ b/jwt-authorizer/src/builder.rs @@ -54,6 +54,26 @@ where } } + pub fn from_jwks(path: &str) -> AuthorizerBuilder { + AuthorizerBuilder { + key_source_type: KeySourceType::JwksPath(path.to_owned()), + refresh: Default::default(), + claims_checker: None, + validation: None, + jwt_source: JwtSource::AuthorizationHeader, + } + } + + pub fn from_jwks_text(text: &str) -> AuthorizerBuilder { + AuthorizerBuilder { + key_source_type: KeySourceType::JwksString(text.to_owned()), + refresh: Default::default(), + claims_checker: None, + validation: None, + jwt_source: JwtSource::AuthorizationHeader, + } + } + /// Builds Authorizer Layer from a RSA PEM file pub fn from_rsa_pem(path: &str) -> AuthorizerBuilder { AuthorizerBuilder { diff --git a/jwt-authorizer/src/jwks/key_store_manager.rs b/jwt-authorizer/src/jwks/key_store_manager.rs index d44f9e7..2ff3edf 100644 --- a/jwt-authorizer/src/jwks/key_store_manager.rs +++ b/jwt-authorizer/src/jwks/key_store_manager.rs @@ -8,7 +8,7 @@ use tokio::sync::Mutex; use crate::error::AuthError; -use super::KeyData; +use super::{KeyData, KeySet}; /// Defines the strategy for the JWKS refresh. #[derive(Clone)] @@ -59,7 +59,7 @@ pub struct KeyStoreManager { pub struct KeyStore { /// key set - keys: Vec>, + keys: KeySet, /// time of the last successfully loaded jwkset load_time: Option, /// time of the last failed load @@ -72,7 +72,7 @@ impl KeyStoreManager { key_url, refresh, keystore: Arc::new(Mutex::new(KeyStore { - keys: vec![], + keys: KeySet::default(), load_time: None, fail_time: None, })), @@ -87,11 +87,7 @@ impl KeyStoreManager { if ks_gard.can_refresh(self.refresh.refresh_interval, self.refresh.retry_interval) { ks_gard.refresh(&self.key_url, &[]).await?; } - if let Some(ref kid) = header.kid { - ks_gard.find_kid(kid).ok_or_else(|| AuthError::InvalidKid(kid.to_owned()))? - } else { - ks_gard.find_alg(&header.alg).ok_or(AuthError::InvalidKeyAlg(header.alg))? - } + ks_gard.get_key(header)? } RefreshStrategy::KeyNotFound => { if let Some(ref kid) = header.kid { @@ -133,11 +129,7 @@ impl KeyStoreManager { { ks_gard.refresh(&self.key_url, &[]).await?; } - if let Some(ref kid) = header.kid { - ks_gard.find_kid(kid).ok_or_else(|| AuthError::InvalidKid(kid.to_owned()))? - } else { - ks_gard.find_alg(&header.alg).ok_or(AuthError::InvalidKeyAlg(header.alg))? - } + ks_gard.get_key(header)? } }; Ok(key.clone()) @@ -186,7 +178,7 @@ impl KeyStore { if keys.is_empty() { Err(AuthError::JwksRefreshError("No valid keys in the Jwk Set!".to_owned())) } else { - self.keys = keys; + self.keys = keys.into(); self.fail_time = None; Ok(()) } @@ -199,17 +191,21 @@ impl KeyStore { /// Find the key in the set that matches the given key id, if any. pub fn find_kid(&self, kid: &str) -> Option<&Arc> { - self.keys.iter().find(|k| k.kid.is_some() && k.kid.as_ref().unwrap() == kid) + self.keys.find_kid(kid) } /// Find the key in the set that matches the given key id, if any. pub fn find_alg(&self, alg: &Algorithm) -> Option<&Arc> { - self.keys.iter().find(|k| k.alg.contains(alg)) + self.keys.find_alg(alg) + } + + fn get_key(&self, header: &jsonwebtoken::Header) -> Result<&Arc, AuthError> { + self.keys.get_key(header) } /// Find first key. pub fn find_first(&self) -> Option<&Arc> { - self.keys.get(0) + self.keys.first() } } @@ -227,7 +223,7 @@ mod tests { }; use crate::jwks::key_store_manager::{KeyStore, KeyStoreManager}; - use crate::jwks::KeyData; + use crate::jwks::{KeyData, KeySet}; use crate::{Refresh, RefreshStrategy}; const JWK_RSA01: &str = r#"{ @@ -281,7 +277,7 @@ mod tests { fn keystore_can_refresh() { // FAIL, NO LOAD let ks = KeyStore { - keys: vec![], + keys: KeySet::default(), fail_time: Instant::now().checked_sub(Duration::from_secs(5)), load_time: None, }; @@ -290,7 +286,7 @@ mod tests { // NO FAIL, LOAD let ks = KeyStore { - keys: vec![], + keys: KeySet::default(), fail_time: None, load_time: Instant::now().checked_sub(Duration::from_secs(5)), }; @@ -299,7 +295,7 @@ mod tests { // FAIL, LOAD let ks = KeyStore { - keys: vec![], + keys: KeySet::default(), fail_time: Instant::now().checked_sub(Duration::from_secs(5)), load_time: Instant::now().checked_sub(Duration::from_secs(10)), }; @@ -318,7 +314,8 @@ mod tests { keys: vec![ Arc::new(KeyData::from_jwk(&jwk0).unwrap()), Arc::new(KeyData::from_jwk(&jwk1).unwrap()), - ], + ] + .into(), }; assert!(ks.find_kid("rsa01").is_some()); assert!(ks.find_kid("ec01").is_some()); @@ -331,7 +328,7 @@ mod tests { let ks = KeyStore { load_time: None, fail_time: None, - keys: vec![Arc::new(KeyData::from_jwk(&jwk0).unwrap())], + keys: vec![Arc::new(KeyData::from_jwk(&jwk0).unwrap())].into(), }; assert!(ks.find_alg(&Algorithm::RS256).is_some()); assert!(ks.find_alg(&Algorithm::EdDSA).is_none()); diff --git a/jwt-authorizer/src/jwks/mod.rs b/jwt-authorizer/src/jwks/mod.rs index 28d86e3..eac2b11 100644 --- a/jwt-authorizer/src/jwks/mod.rs +++ b/jwt-authorizer/src/jwks/mod.rs @@ -12,6 +12,8 @@ pub mod key_store_manager; pub enum KeySource { /// KeyDataSource managing a refreshable key sets KeyStoreSource(KeyStoreManager), + /// Manages public key sets, initialized on startup + MultiKeySource(KeySet), /// Manages one public key, initialized on startup SingleKeySource(Arc), } @@ -33,10 +35,49 @@ impl KeyData { } } +#[derive(Clone, Default)] +pub struct KeySet(Vec>); + +impl From>> for KeySet { + fn from(value: Vec>) -> Self { + KeySet(value) + } +} + +impl KeySet { + /// Find the key in the set that matches the given key id, if any. + pub fn find_kid(&self, kid: &str) -> Option<&Arc> { + self.0.iter().find(|k| match &k.kid { + Some(k) => k == kid, + None => false, + }) + } + + /// Find the key in the set that matches the given key id, if any. + pub fn find_alg(&self, alg: &Algorithm) -> Option<&Arc> { + self.0.iter().find(|k| k.alg.contains(alg)) + } + + /// Find first key. + pub fn first(&self) -> Option<&Arc> { + self.0.first() + } + + pub(crate) fn get_key(&self, header: &Header) -> Result<&Arc, AuthError> { + let key = if let Some(ref kid) = header.kid { + self.find_kid(kid).ok_or_else(|| AuthError::InvalidKid(kid.to_owned()))? + } else { + self.find_alg(&header.alg).ok_or(AuthError::InvalidKeyAlg(header.alg))? + }; + Ok(key) + } +} + impl KeySource { pub async fn get_key(&self, header: Header) -> Result, AuthError> { match self { KeySource::KeyStoreSource(kstore) => kstore.get_key(&header).await, + KeySource::MultiKeySource(keys) => keys.get_key(&header).cloned(), KeySource::SingleKeySource(key) => Ok(key.clone()), } } diff --git a/jwt-authorizer/tests/tests.rs b/jwt-authorizer/tests/tests.rs index f5128e4..298dbdc 100644 --- a/jwt-authorizer/tests/tests.rs +++ b/jwt-authorizer/tests/tests.rs @@ -114,6 +114,21 @@ mod tests { assert_eq!(response.status(), StatusCode::OK); let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); assert_eq!(&body[..], b"hello: b@b.com"); + + let response = make_proteced_request(JwtAuthorizer::from_jwks("../config/public1.jwks"), common::JWT_RSA1_OK).await; + assert_eq!(response.status(), StatusCode::OK); + let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); + assert_eq!(&body[..], b"hello: b@b.com"); + + let response = make_proteced_request(JwtAuthorizer::from_jwks("../config/public1.jwks"), common::JWT_EC1_OK).await; + assert_eq!(response.status(), StatusCode::OK); + let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); + assert_eq!(&body[..], b"hello: b@b.com"); + + let response = make_proteced_request(JwtAuthorizer::from_jwks("../config/public1.jwks"), common::JWT_ED1_OK).await; + assert_eq!(response.status(), StatusCode::OK); + let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); + assert_eq!(&body[..], b"hello: b@b.com"); } #[tokio::test]