Add support for reading keys from a static JWKS

Allow creating authorizer from JWKS files similar to other static
certificates.

Signed-off-by: Sjoerd Simons <sjoerd@collabora.com>
This commit is contained in:
Sjoerd Simons 2023-09-26 21:33:06 +02:00 committed by cduvray
parent 6e19f31c77
commit ef8ac07271
7 changed files with 175 additions and 26 deletions

View file

@ -18,3 +18,7 @@ curve name: prime256v1 (secp256r1, secp384r1)
(Ed25519 - EdDSA signature scheme using SHA-512 (SHA-2) and Curve25519) (Ed25519 - EdDSA signature scheme using SHA-512 (SHA-2) and Curve25519)
> openssl genpkey -algorithm ed25519 > 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

26
config/public1.jwks Normal file
View file

@ -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"
}
]
}

View file

@ -41,6 +41,7 @@ pub enum KeySourceType {
EDString(String), EDString(String),
Secret(String), Secret(String),
Jwks(String), Jwks(String),
JwksPath(String),
JwksString(String), // TODO: expose JwksString in JwtAuthorizer or remove it JwksString(String), // TODO: expose JwksString in JwtAuthorizer or remove it
Discovery(String), Discovery(String),
} }
@ -148,13 +149,36 @@ where
jwt_source, 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::<Result<Vec<_>, _>>()?;
Authorizer {
key_source: KeySource::MultiKeySource(keys.into()),
claims_checker,
validation,
jwt_source,
}
}
KeySourceType::JwksString(jwks_str) => { KeySourceType::JwksString(jwks_str) => {
// TODO: expose it in JwtAuthorizer or remove // TODO: expose it in JwtAuthorizer or remove
let set: JwkSet = serde_json::from_str(jwks_str.as_str())?; let set: JwkSet = serde_json::from_str(jwks_str.as_str())?;
// TODO: replace [0] by kid/alg search let keys = set
let k = KeyData::from_jwk(&set.keys[0]).map_err(InitError::KeyDecodingError)?; .keys
.iter()
.map(|k| match KeyData::from_jwk(k) {
Ok(kdata) => Ok(Arc::new(kdata)),
Err(err) => Err(InitError::KeyDecodingError(err)),
})
.collect::<Result<Vec<_>, _>>()?;
Authorizer { Authorizer {
key_source: KeySource::SingleKeySource(Arc::new(k)), key_source: KeySource::MultiKeySource(keys.into()),
claims_checker, claims_checker,
validation, validation,
jwt_source, jwt_source,
@ -363,6 +387,28 @@ mod tests {
.unwrap(); .unwrap();
let k = a.key_source.get_key(Header::new(Algorithm::EdDSA)); let k = a.key_source.get_key(Header::new(Algorithm::EdDSA));
assert!(k.await.is_ok()); assert!(k.await.is_ok());
let a = Authorizer::<Value>::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] #[tokio::test]

View file

@ -54,6 +54,26 @@ where
} }
} }
pub fn from_jwks(path: &str) -> AuthorizerBuilder<C> {
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<C> {
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 /// Builds Authorizer Layer from a RSA PEM file
pub fn from_rsa_pem(path: &str) -> AuthorizerBuilder<C> { pub fn from_rsa_pem(path: &str) -> AuthorizerBuilder<C> {
AuthorizerBuilder { AuthorizerBuilder {

View file

@ -8,7 +8,7 @@ use tokio::sync::Mutex;
use crate::error::AuthError; use crate::error::AuthError;
use super::KeyData; use super::{KeyData, KeySet};
/// Defines the strategy for the JWKS refresh. /// Defines the strategy for the JWKS refresh.
#[derive(Clone)] #[derive(Clone)]
@ -59,7 +59,7 @@ pub struct KeyStoreManager {
pub struct KeyStore { pub struct KeyStore {
/// key set /// key set
keys: Vec<Arc<KeyData>>, keys: KeySet,
/// time of the last successfully loaded jwkset /// time of the last successfully loaded jwkset
load_time: Option<Instant>, load_time: Option<Instant>,
/// time of the last failed load /// time of the last failed load
@ -72,7 +72,7 @@ impl KeyStoreManager {
key_url, key_url,
refresh, refresh,
keystore: Arc::new(Mutex::new(KeyStore { keystore: Arc::new(Mutex::new(KeyStore {
keys: vec![], keys: KeySet::default(),
load_time: None, load_time: None,
fail_time: None, fail_time: None,
})), })),
@ -87,11 +87,7 @@ impl KeyStoreManager {
if ks_gard.can_refresh(self.refresh.refresh_interval, self.refresh.retry_interval) { if ks_gard.can_refresh(self.refresh.refresh_interval, self.refresh.retry_interval) {
ks_gard.refresh(&self.key_url, &[]).await?; ks_gard.refresh(&self.key_url, &[]).await?;
} }
if let Some(ref kid) = header.kid { ks_gard.get_key(header)?
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))?
}
} }
RefreshStrategy::KeyNotFound => { RefreshStrategy::KeyNotFound => {
if let Some(ref kid) = header.kid { if let Some(ref kid) = header.kid {
@ -133,11 +129,7 @@ impl KeyStoreManager {
{ {
ks_gard.refresh(&self.key_url, &[]).await?; ks_gard.refresh(&self.key_url, &[]).await?;
} }
if let Some(ref kid) = header.kid { ks_gard.get_key(header)?
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))?
}
} }
}; };
Ok(key.clone()) Ok(key.clone())
@ -186,7 +178,7 @@ impl KeyStore {
if keys.is_empty() { if keys.is_empty() {
Err(AuthError::JwksRefreshError("No valid keys in the Jwk Set!".to_owned())) Err(AuthError::JwksRefreshError("No valid keys in the Jwk Set!".to_owned()))
} else { } else {
self.keys = keys; self.keys = keys.into();
self.fail_time = None; self.fail_time = None;
Ok(()) Ok(())
} }
@ -199,17 +191,21 @@ impl KeyStore {
/// Find the key in the set that matches the given key id, if any. /// Find the key in the set that matches the given key id, if any.
pub fn find_kid(&self, kid: &str) -> Option<&Arc<KeyData>> { pub fn find_kid(&self, kid: &str) -> Option<&Arc<KeyData>> {
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. /// Find the key in the set that matches the given key id, if any.
pub fn find_alg(&self, alg: &Algorithm) -> Option<&Arc<KeyData>> { pub fn find_alg(&self, alg: &Algorithm) -> Option<&Arc<KeyData>> {
self.keys.iter().find(|k| k.alg.contains(alg)) self.keys.find_alg(alg)
}
fn get_key(&self, header: &jsonwebtoken::Header) -> Result<&Arc<KeyData>, AuthError> {
self.keys.get_key(header)
} }
/// Find first key. /// Find first key.
pub fn find_first(&self) -> Option<&Arc<KeyData>> { pub fn find_first(&self) -> Option<&Arc<KeyData>> {
self.keys.get(0) self.keys.first()
} }
} }
@ -227,7 +223,7 @@ mod tests {
}; };
use crate::jwks::key_store_manager::{KeyStore, KeyStoreManager}; use crate::jwks::key_store_manager::{KeyStore, KeyStoreManager};
use crate::jwks::KeyData; use crate::jwks::{KeyData, KeySet};
use crate::{Refresh, RefreshStrategy}; use crate::{Refresh, RefreshStrategy};
const JWK_RSA01: &str = r#"{ const JWK_RSA01: &str = r#"{
@ -281,7 +277,7 @@ mod tests {
fn keystore_can_refresh() { fn keystore_can_refresh() {
// FAIL, NO LOAD // FAIL, NO LOAD
let ks = KeyStore { let ks = KeyStore {
keys: vec![], keys: KeySet::default(),
fail_time: Instant::now().checked_sub(Duration::from_secs(5)), fail_time: Instant::now().checked_sub(Duration::from_secs(5)),
load_time: None, load_time: None,
}; };
@ -290,7 +286,7 @@ mod tests {
// NO FAIL, LOAD // NO FAIL, LOAD
let ks = KeyStore { let ks = KeyStore {
keys: vec![], keys: KeySet::default(),
fail_time: None, fail_time: None,
load_time: Instant::now().checked_sub(Duration::from_secs(5)), load_time: Instant::now().checked_sub(Duration::from_secs(5)),
}; };
@ -299,7 +295,7 @@ mod tests {
// FAIL, LOAD // FAIL, LOAD
let ks = KeyStore { let ks = KeyStore {
keys: vec![], keys: KeySet::default(),
fail_time: Instant::now().checked_sub(Duration::from_secs(5)), fail_time: Instant::now().checked_sub(Duration::from_secs(5)),
load_time: Instant::now().checked_sub(Duration::from_secs(10)), load_time: Instant::now().checked_sub(Duration::from_secs(10)),
}; };
@ -318,7 +314,8 @@ mod tests {
keys: vec![ keys: vec![
Arc::new(KeyData::from_jwk(&jwk0).unwrap()), Arc::new(KeyData::from_jwk(&jwk0).unwrap()),
Arc::new(KeyData::from_jwk(&jwk1).unwrap()), Arc::new(KeyData::from_jwk(&jwk1).unwrap()),
], ]
.into(),
}; };
assert!(ks.find_kid("rsa01").is_some()); assert!(ks.find_kid("rsa01").is_some());
assert!(ks.find_kid("ec01").is_some()); assert!(ks.find_kid("ec01").is_some());
@ -331,7 +328,7 @@ mod tests {
let ks = KeyStore { let ks = KeyStore {
load_time: None, load_time: None,
fail_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::RS256).is_some());
assert!(ks.find_alg(&Algorithm::EdDSA).is_none()); assert!(ks.find_alg(&Algorithm::EdDSA).is_none());

View file

@ -12,6 +12,8 @@ pub mod key_store_manager;
pub enum KeySource { pub enum KeySource {
/// KeyDataSource managing a refreshable key sets /// KeyDataSource managing a refreshable key sets
KeyStoreSource(KeyStoreManager), KeyStoreSource(KeyStoreManager),
/// Manages public key sets, initialized on startup
MultiKeySource(KeySet),
/// Manages one public key, initialized on startup /// Manages one public key, initialized on startup
SingleKeySource(Arc<KeyData>), SingleKeySource(Arc<KeyData>),
} }
@ -33,10 +35,49 @@ impl KeyData {
} }
} }
#[derive(Clone, Default)]
pub struct KeySet(Vec<Arc<KeyData>>);
impl From<Vec<Arc<KeyData>>> for KeySet {
fn from(value: Vec<Arc<KeyData>>) -> 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<KeyData>> {
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<KeyData>> {
self.0.iter().find(|k| k.alg.contains(alg))
}
/// Find first key.
pub fn first(&self) -> Option<&Arc<KeyData>> {
self.0.first()
}
pub(crate) fn get_key(&self, header: &Header) -> Result<&Arc<KeyData>, 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 { impl KeySource {
pub async fn get_key(&self, header: Header) -> Result<Arc<KeyData>, AuthError> { pub async fn get_key(&self, header: Header) -> Result<Arc<KeyData>, AuthError> {
match self { match self {
KeySource::KeyStoreSource(kstore) => kstore.get_key(&header).await, KeySource::KeyStoreSource(kstore) => kstore.get_key(&header).await,
KeySource::MultiKeySource(keys) => keys.get_key(&header).cloned(),
KeySource::SingleKeySource(key) => Ok(key.clone()), KeySource::SingleKeySource(key) => Ok(key.clone()),
} }
} }

View file

@ -114,6 +114,21 @@ mod tests {
assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.status(), StatusCode::OK);
let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
assert_eq!(&body[..], b"hello: b@b.com"); 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] #[tokio::test]