mirror of
https://github.com/TECHNOFAB11/jwt-authorizer.git
synced 2025-12-12 08:00:07 +01:00
feat: refresh configuration
This commit is contained in:
parent
7b6f8fb4c5
commit
1203163b0c
9 changed files with 189 additions and 58 deletions
|
|
@ -10,8 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- JwtAuthorizer creation simplified:
|
- JwtAuthorizer creation simplified:
|
||||||
|
|
||||||
- JwtAuthorizer::from_* creates an instance, new() is not necessary anymore
|
- JwtAuthorizer::from_* creates an instance, new() is not necessary anymore
|
||||||
|
- with_check() renamed to check()
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- jwks store refresh configuration
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,11 @@ JWT authorizer Layer for Axum.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- JWT token verification (Bearer)
|
- JWT token verification (Bearer)
|
||||||
|
- Algoritms: ECDSA, RSA, EdDSA, HS
|
||||||
|
- JWKS endpoint support
|
||||||
|
- Configurable refresh
|
||||||
- Claims extraction
|
- Claims extraction
|
||||||
- JWKS endpoint support (with refresh)
|
|
||||||
- Algoritms: ECDSA, RSA, EdDSA, HS
|
|
||||||
- Claims checker
|
- Claims checker
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use axum::{
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
use jwt_authorizer::{AuthError, JwtAuthorizer, JwtClaims};
|
use jwt_authorizer::{AuthError, JwtAuthorizer, JwtClaims, Refresh, RefreshStrategy};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::{fmt::Display, net::SocketAddr};
|
use std::{fmt::Display, net::SocketAddr};
|
||||||
use tower_http::trace::TraceLayer;
|
use tower_http::trace::TraceLayer;
|
||||||
|
|
@ -30,7 +30,9 @@ async fn main() {
|
||||||
// User is a struct deserializable from JWT claims representing the authorized user
|
// User is a struct deserializable from JWT claims representing the authorized user
|
||||||
let jwt_auth: JwtAuthorizer<User> = JwtAuthorizer::
|
let jwt_auth: JwtAuthorizer<User> = JwtAuthorizer::
|
||||||
from_jwks_url("http://localhost:3000/oidc/jwks")
|
from_jwks_url("http://localhost:3000/oidc/jwks")
|
||||||
.with_check(claim_checker);
|
// .no_refresh()
|
||||||
|
.refresh(Refresh {strategy: RefreshStrategy::Interval, ..Default::default()})
|
||||||
|
.check(claim_checker);
|
||||||
|
|
||||||
let oidc = Router::new()
|
let oidc = Router::new()
|
||||||
.route("/authorize", post(oidc_provider::authorize))
|
.route("/authorize", post(oidc_provider::authorize))
|
||||||
|
|
|
||||||
|
|
@ -29,13 +29,6 @@ pub struct Keys {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Keys {
|
impl Keys {
|
||||||
fn new(secret: &[u8]) -> Self {
|
|
||||||
Self {
|
|
||||||
alg: Algorithm::HS256,
|
|
||||||
encoding: EncodingKey::from_secret(secret),
|
|
||||||
decoding: DecodingKey::from_secret(secret),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn load_rsa() -> Self {
|
fn load_rsa() -> Self {
|
||||||
Self {
|
Self {
|
||||||
alg: Algorithm::RS256,
|
alg: Algorithm::RS256,
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,17 @@
|
||||||
|
|
||||||
JWT authoriser Layer for Axum.
|
JWT authoriser Layer for Axum.
|
||||||
|
|
||||||
Example:
|
## Features
|
||||||
|
|
||||||
|
- JWT token verification (Bearer)
|
||||||
|
- Algoritms: ECDSA, RSA, EdDSA, HS
|
||||||
|
- JWKS endpoint support
|
||||||
|
- Configurable refresh
|
||||||
|
- Claims extraction
|
||||||
|
- Claims checker
|
||||||
|
|
||||||
|
|
||||||
|
## Usage Example
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use jwt_authorizer::{AuthError, JwtAuthorizer, JwtClaims};
|
use jwt_authorizer::{AuthError, JwtAuthorizer, JwtClaims};
|
||||||
|
|
@ -55,8 +65,14 @@ Example:
|
||||||
}
|
}
|
||||||
|
|
||||||
let authorizer = JwtAuthorizer::from_rsa_pem("../config/jwtRS256.key.pub")
|
let authorizer = JwtAuthorizer::from_rsa_pem("../config/jwtRS256.key.pub")
|
||||||
.with_check(
|
.check(
|
||||||
|claims: &User| claims.sub.contains('@') // must be an email
|
|claims: &User| claims.sub.contains('@') // must be an email
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## JWKS Refresh
|
||||||
|
|
||||||
|
By default the jwks keys are reloaded when a request token is signed with a key (`kid` jwt header) that is not present in the store (a minimal intervale between 2 reloads is 10s by default, can be configured).
|
||||||
|
|
||||||
|
- `JwtAuthorizer::no_refresh()` configures one and unique reload of jwks keys
|
||||||
|
- `JwtAuthorizer::refresh(refresh_configuration)` allows to define a finer configuration for jwks refreshing, for more details see the documentation of `Refresh` struct.
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
use std::{io::Read, time::Duration};
|
use std::io::Read;
|
||||||
|
|
||||||
use jsonwebtoken::{decode, decode_header, jwk::JwkSet, DecodingKey, TokenData, Validation};
|
use jsonwebtoken::{decode, decode_header, jwk::JwkSet, DecodingKey, TokenData, Validation};
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::{AuthError, InitError},
|
error::{AuthError, InitError},
|
||||||
jwks::{key_store_manager::KeyStoreManager, KeySource},
|
jwks::{key_store_manager::KeyStoreManager, KeySource}, Refresh,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub trait ClaimsChecker<C> {
|
pub trait ClaimsChecker<C> {
|
||||||
|
|
@ -66,7 +66,7 @@ where
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from(key_source_type: &KeySourceType, claims_checker: Option<FnClaimsChecker<C>>) -> Result<Authorizer<C>, InitError> {
|
pub(crate) fn from(key_source_type: &KeySourceType, claims_checker: Option<FnClaimsChecker<C>>) -> Result<Authorizer<C>, InitError> {
|
||||||
let key = match key_source_type {
|
let key = match key_source_type {
|
||||||
KeySourceType::RSA(path) => DecodingKey::from_rsa_pem(&read_data(path.as_str())?)?,
|
KeySourceType::RSA(path) => DecodingKey::from_rsa_pem(&read_data(path.as_str())?)?,
|
||||||
KeySourceType::EC(path) => DecodingKey::from_ec_der(&read_data(path.as_str())?),
|
KeySourceType::EC(path) => DecodingKey::from_ec_der(&read_data(path.as_str())?),
|
||||||
|
|
@ -81,8 +81,8 @@ where
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_jwks_url(url: &str, claims_checker: Option<FnClaimsChecker<C>>) -> Result<Authorizer<C>, InitError> {
|
pub(crate) fn from_jwks_url(url: &str, claims_checker: Option<FnClaimsChecker<C>>, refresh: Refresh) -> Result<Authorizer<C>, InitError> {
|
||||||
let key_store_manager = KeyStoreManager::with_refresh_interval(url, Duration::from_secs(60));
|
let key_store_manager = KeyStoreManager::new(url, refresh);
|
||||||
Ok(Authorizer {
|
Ok(Authorizer {
|
||||||
key_source: KeySource::KeyStoreSource(key_store_manager),
|
key_source: KeySource::KeyStoreSource(key_store_manager),
|
||||||
claims_checker,
|
claims_checker,
|
||||||
|
|
|
||||||
|
|
@ -10,21 +10,47 @@ use tokio::sync::Mutex;
|
||||||
|
|
||||||
use crate::error::AuthError;
|
use crate::error::AuthError;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Copy)]
|
||||||
pub enum RefreshStrategy {
|
pub enum RefreshStrategy {
|
||||||
|
|
||||||
/// refresh periodicaly
|
/// refresh periodicaly
|
||||||
Interval(Duration),
|
Interval,
|
||||||
|
|
||||||
/// when kid not found in the store
|
/// when kid not found in the store
|
||||||
KeyNotFound,
|
KeyNotFound,
|
||||||
// other strategies? KeyNotFoundOrInterval(Duration), Once,
|
|
||||||
|
/// load once triggered by the first use
|
||||||
|
NoRefresh,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// JWKS Refresh configuration
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct Refresh {
|
||||||
|
pub strategy: RefreshStrategy,
|
||||||
|
// after the interval the store will be refreshed (before getting a new key - lazy behaviour)
|
||||||
|
pub refresh_interval: Duration,
|
||||||
|
// don't refresh before (counting from the last refresh, when the kid not found)
|
||||||
|
pub minimal_refresh_interval: Duration,
|
||||||
|
// don't refresh before (after an error or jwks unawailable)
|
||||||
|
pub retry_interval: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Refresh {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
strategy: RefreshStrategy::KeyNotFound,
|
||||||
|
refresh_interval: Duration::from_secs(600),
|
||||||
|
minimal_refresh_interval: Duration::from_secs(30),
|
||||||
|
retry_interval: Duration::from_secs(10),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct KeyStoreManager {
|
pub struct KeyStoreManager {
|
||||||
key_url: String,
|
key_url: String,
|
||||||
refresh: RefreshStrategy,
|
|
||||||
/// in case of fail loading (error or key not found), minimal interval
|
/// in case of fail loading (error or key not found), minimal interval
|
||||||
minimal_refresh_interval: Duration,
|
refresh: Refresh,
|
||||||
keystore: Arc<Mutex<KeyStore>>,
|
keystore: Arc<Mutex<KeyStore>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,11 +64,10 @@ pub struct KeyStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KeyStoreManager {
|
impl KeyStoreManager {
|
||||||
pub(crate) fn new(url: &str, refresh: RefreshStrategy) -> KeyStoreManager {
|
pub(crate) fn new(url: &str, refresh: Refresh) -> KeyStoreManager {
|
||||||
KeyStoreManager {
|
KeyStoreManager {
|
||||||
key_url: url.to_owned(),
|
key_url: url.to_owned(),
|
||||||
refresh,
|
refresh,
|
||||||
minimal_refresh_interval: Duration::from_secs(5), // TODO: make configurable
|
|
||||||
keystore: Arc::new(Mutex::new(KeyStore {
|
keystore: Arc::new(Mutex::new(KeyStore {
|
||||||
jwks: JwkSet { keys: vec![] },
|
jwks: JwkSet { keys: vec![] },
|
||||||
load_time: None,
|
load_time: None,
|
||||||
|
|
@ -51,20 +76,12 @@ impl KeyStoreManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn with_refresh(url: &str) -> KeyStoreManager {
|
|
||||||
KeyStoreManager::new(url, RefreshStrategy::KeyNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn with_refresh_interval(url: &str, interval: Duration) -> KeyStoreManager {
|
|
||||||
KeyStoreManager::new(url, RefreshStrategy::Interval(interval))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn get_key(&self, header: &jsonwebtoken::Header) -> Result<jsonwebtoken::DecodingKey, AuthError> {
|
pub(crate) async fn get_key(&self, header: &jsonwebtoken::Header) -> Result<jsonwebtoken::DecodingKey, AuthError> {
|
||||||
let kstore = self.keystore.clone();
|
let kstore = self.keystore.clone();
|
||||||
let mut ks_gard = kstore.lock().await;
|
let mut ks_gard = kstore.lock().await;
|
||||||
let key = match self.refresh {
|
let key = match self.refresh.strategy {
|
||||||
RefreshStrategy::Interval(refresh_interval) => {
|
RefreshStrategy::Interval => {
|
||||||
if ks_gard.should_refresh(refresh_interval) && ks_gard.can_refresh(self.minimal_refresh_interval) {
|
if ks_gard.should_refresh(self.refresh.refresh_interval) && ks_gard.can_refresh(self.refresh.minimal_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 {
|
if let Some(ref kid) = header.kid {
|
||||||
|
|
@ -82,12 +99,13 @@ impl KeyStoreManager {
|
||||||
let jwk_opt = ks_gard.find_kid(kid);
|
let jwk_opt = ks_gard.find_kid(kid);
|
||||||
if let Some(jwk) = jwk_opt {
|
if let Some(jwk) = jwk_opt {
|
||||||
jwk
|
jwk
|
||||||
} else if ks_gard.can_refresh(self.minimal_refresh_interval) {
|
} else if ks_gard.can_refresh(self.refresh.minimal_refresh_interval, self.refresh.retry_interval) {
|
||||||
ks_gard.refresh(&self.key_url, &[("kid", kid)]).await?;
|
ks_gard.refresh(&self.key_url, &[("kid", kid)]).await?;
|
||||||
ks_gard
|
ks_gard
|
||||||
.find_kid(kid)
|
.find_kid(kid)
|
||||||
.ok_or_else(|| AuthError::InvalidKid(kid.to_owned()))?
|
.ok_or_else(|| AuthError::InvalidKid(kid.to_owned()))?
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
return Err(AuthError::InvalidKid(kid.to_owned()));
|
return Err(AuthError::InvalidKid(kid.to_owned()));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -95,7 +113,7 @@ impl KeyStoreManager {
|
||||||
// .ok_or(AuthError::InvalidKeyAlg(header.alg))?
|
// .ok_or(AuthError::InvalidKeyAlg(header.alg))?
|
||||||
if let Some(jwk) = jwk_opt {
|
if let Some(jwk) = jwk_opt {
|
||||||
jwk
|
jwk
|
||||||
} else if ks_gard.can_refresh(self.minimal_refresh_interval) {
|
} else if ks_gard.can_refresh(self.refresh.minimal_refresh_interval, self.refresh.retry_interval) {
|
||||||
ks_gard
|
ks_gard
|
||||||
.refresh(
|
.refresh(
|
||||||
&self.key_url,
|
&self.key_url,
|
||||||
|
|
@ -113,6 +131,20 @@ impl KeyStoreManager {
|
||||||
return Err(AuthError::InvalidKeyAlg(header.alg));
|
return Err(AuthError::InvalidKeyAlg(header.alg));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
RefreshStrategy::NoRefresh => {
|
||||||
|
if ks_gard.load_time.is_none() {
|
||||||
|
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))?
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -129,15 +161,15 @@ impl KeyStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn can_refresh(&self, minimal_refresh_interval: Duration) -> bool {
|
fn can_refresh(&self, minimal_refresh_interval: Duration, minimal_retry: Duration) -> bool {
|
||||||
if let Some(ft) = self.fail_time {
|
if let Some(fail_tm) = self.fail_time {
|
||||||
if let Some(lt) = self.load_time {
|
if let Some(load_tm) = self.load_time {
|
||||||
ft.elapsed() > minimal_refresh_interval && lt.elapsed() > minimal_refresh_interval
|
fail_tm.elapsed() > minimal_retry && load_tm.elapsed() > minimal_refresh_interval
|
||||||
} else {
|
} else {
|
||||||
ft.elapsed() > minimal_refresh_interval
|
fail_tm.elapsed() > minimal_retry
|
||||||
}
|
}
|
||||||
} else if let Some(lt) = self.load_time {
|
} else if let Some(load_tm) = self.load_time {
|
||||||
lt.elapsed() > minimal_refresh_interval
|
load_tm.elapsed() > minimal_refresh_interval
|
||||||
} else {
|
} else {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
@ -196,6 +228,7 @@ mod tests {
|
||||||
Mock, MockServer, ResponseTemplate,
|
Mock, MockServer, ResponseTemplate,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::{RefreshStrategy, Refresh};
|
||||||
use crate::jwks::key_store_manager::{KeyStore, KeyStoreManager};
|
use crate::jwks::key_store_manager::{KeyStore, KeyStoreManager};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -219,21 +252,34 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn keystore_can_refresh() {
|
fn keystore_can_refresh() {
|
||||||
|
|
||||||
|
// FAIL, NO LOAD
|
||||||
let ks = KeyStore {
|
let ks = KeyStore {
|
||||||
jwks: jsonwebtoken::jwk::JwkSet { keys: vec![] },
|
jwks: jsonwebtoken::jwk::JwkSet { keys: vec![] },
|
||||||
fail_time: Some(Instant::now() - Duration::from_secs(5)),
|
fail_time: Some(Instant::now() - Duration::from_secs(5)),
|
||||||
load_time: None,
|
load_time: None,
|
||||||
};
|
};
|
||||||
assert!(ks.can_refresh(Duration::from_secs(4)));
|
assert!(ks.can_refresh(Duration::from_secs(4), Duration::from_secs(4)));
|
||||||
assert!(!ks.can_refresh(Duration::from_secs(6)));
|
assert!(ks.can_refresh(Duration::from_secs(6), Duration::from_secs(4)));
|
||||||
|
assert!(!ks.can_refresh(Duration::from_secs(6), Duration::from_secs(6)));
|
||||||
|
|
||||||
|
// NO FAIL, LOAD
|
||||||
let ks = KeyStore {
|
let ks = KeyStore {
|
||||||
jwks: jsonwebtoken::jwk::JwkSet { keys: vec![] },
|
jwks: jsonwebtoken::jwk::JwkSet { keys: vec![] },
|
||||||
fail_time: None,
|
fail_time: None,
|
||||||
load_time: Some(Instant::now() - Duration::from_secs(5)),
|
load_time: Some(Instant::now() - Duration::from_secs(5)),
|
||||||
};
|
};
|
||||||
assert!(ks.can_refresh(Duration::from_secs(4)));
|
assert!(ks.can_refresh(Duration::from_secs(4), Duration::from_secs(4)));
|
||||||
assert!(!ks.can_refresh(Duration::from_secs(6)));
|
assert!(!ks.can_refresh(Duration::from_secs(6), Duration::from_secs(6)));
|
||||||
|
|
||||||
|
// FAIL, LOAD
|
||||||
|
let ks = KeyStore {
|
||||||
|
jwks: jsonwebtoken::jwk::JwkSet { keys: vec![] },
|
||||||
|
fail_time: Some(Instant::now() - Duration::from_secs(5)),
|
||||||
|
load_time: Some(Instant::now() - Duration::from_secs(10)),
|
||||||
|
};
|
||||||
|
assert!(ks.can_refresh(Duration::from_secs(6), Duration::from_secs(4)));
|
||||||
|
assert!(!ks.can_refresh(Duration::from_secs(6), Duration::from_secs(6)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -295,7 +341,10 @@ mod tests {
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let ksm = KeyStoreManager::with_refresh_interval(&mock_server.uri(), Duration::from_secs(3000));
|
let ksm = KeyStoreManager::new(
|
||||||
|
&mock_server.uri(),
|
||||||
|
Refresh {strategy: RefreshStrategy::Interval, refresh_interval: Duration::from_secs(3000), ..Default::default()}
|
||||||
|
);
|
||||||
let r = ksm.get_key(&Header::new(Algorithm::EdDSA)).await;
|
let r = ksm.get_key(&Header::new(Algorithm::EdDSA)).await;
|
||||||
assert!(r.is_ok());
|
assert!(r.is_ok());
|
||||||
mock_server.verify().await;
|
mock_server.verify().await;
|
||||||
|
|
@ -317,7 +366,9 @@ mod tests {
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let mut ksm = KeyStoreManager::with_refresh(&mock_server.uri());
|
let mut ksm = KeyStoreManager::new(
|
||||||
|
&mock_server.uri(),
|
||||||
|
Refresh {strategy: RefreshStrategy::KeyNotFound, ..Default::default()});
|
||||||
|
|
||||||
// STEP 1: initial (lazy) reloading
|
// STEP 1: initial (lazy) reloading
|
||||||
let r = ksm.get_key(&build_header("key-ed", Algorithm::EdDSA)).await;
|
let r = ksm.get_key(&build_header("key-ed", Algorithm::EdDSA)).await;
|
||||||
|
|
@ -341,7 +392,7 @@ mod tests {
|
||||||
let h = build_header("key-ed02", Algorithm::EdDSA);
|
let h = build_header("key-ed02", Algorithm::EdDSA);
|
||||||
assert!(ksm.get_key(&h).await.is_err());
|
assert!(ksm.get_key(&h).await.is_err());
|
||||||
|
|
||||||
ksm.minimal_refresh_interval = Duration::from_millis(100);
|
ksm.refresh.minimal_refresh_interval = Duration::from_millis(100);
|
||||||
tokio::time::sleep(Duration::from_millis(101)).await;
|
tokio::time::sleep(Duration::from_millis(101)).await;
|
||||||
assert!(ksm.get_key(&h).await.is_ok());
|
assert!(ksm.get_key(&h).await.is_ok());
|
||||||
|
|
||||||
|
|
@ -370,4 +421,36 @@ mod tests {
|
||||||
|
|
||||||
mock_server.verify().await;
|
mock_server.verify().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn keystore_manager_find_key_with_no_refresh() {
|
||||||
|
let mock_server = MockServer::start().await;
|
||||||
|
mock_jwks_response_once(
|
||||||
|
&mock_server,
|
||||||
|
r#"{
|
||||||
|
"kty": "OKP",
|
||||||
|
"use": "sig",
|
||||||
|
"crv": "Ed25519",
|
||||||
|
"x": "uWtSkE-I9aTMYTTvuTE1rtu0rNdxp3DU33cJ_ksL1Gk",
|
||||||
|
"kid": "key-ed",
|
||||||
|
"alg": "EdDSA"
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let ksm = KeyStoreManager::new(
|
||||||
|
&mock_server.uri(),
|
||||||
|
Refresh {strategy: RefreshStrategy::NoRefresh, ..Default::default()});
|
||||||
|
|
||||||
|
// STEP 1: initial (lazy) reloading
|
||||||
|
let r = ksm.get_key(&build_header("key-ed", Algorithm::EdDSA)).await;
|
||||||
|
assert!(r.is_ok());
|
||||||
|
mock_server.verify().await;
|
||||||
|
|
||||||
|
// STEP2: new kid -> reloading ksm
|
||||||
|
let h = build_header("key-ed02", Algorithm::EdDSA);
|
||||||
|
assert!(ksm.get_key(&h).await.is_err());
|
||||||
|
|
||||||
|
mock_server.verify().await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,10 @@ use std::task::{Context, Poll};
|
||||||
use tower_layer::Layer;
|
use tower_layer::Layer;
|
||||||
use tower_service::Service;
|
use tower_service::Service;
|
||||||
|
|
||||||
use crate::AuthError;
|
use crate::{AuthError, RefreshStrategy};
|
||||||
use crate::authorizer::{Authorizer, FnClaimsChecker, KeySourceType};
|
use crate::authorizer::{Authorizer, FnClaimsChecker, KeySourceType};
|
||||||
use crate::error::InitError;
|
use crate::error::InitError;
|
||||||
|
use crate::jwks::key_store_manager::Refresh;
|
||||||
|
|
||||||
/// Authorizer Layer builder
|
/// Authorizer Layer builder
|
||||||
///
|
///
|
||||||
|
|
@ -27,6 +28,7 @@ where
|
||||||
C: Clone + DeserializeOwned,
|
C: Clone + DeserializeOwned,
|
||||||
{
|
{
|
||||||
key_source_type: Option<KeySourceType>,
|
key_source_type: Option<KeySourceType>,
|
||||||
|
refresh: Option<Refresh>,
|
||||||
claims_checker: Option<FnClaimsChecker<C>>,
|
claims_checker: Option<FnClaimsChecker<C>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,6 +41,7 @@ where
|
||||||
pub fn from_jwks_url(url: &'static str) -> JwtAuthorizer<C> {
|
pub fn from_jwks_url(url: &'static str) -> JwtAuthorizer<C> {
|
||||||
JwtAuthorizer {
|
JwtAuthorizer {
|
||||||
key_source_type: Some(KeySourceType::Jwks(url.to_owned())),
|
key_source_type: Some(KeySourceType::Jwks(url.to_owned())),
|
||||||
|
refresh: Default::default(),
|
||||||
claims_checker: None,
|
claims_checker: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -47,6 +50,7 @@ where
|
||||||
pub fn from_rsa_pem(path: &'static str) -> JwtAuthorizer<C> {
|
pub fn from_rsa_pem(path: &'static str) -> JwtAuthorizer<C> {
|
||||||
JwtAuthorizer {
|
JwtAuthorizer {
|
||||||
key_source_type: Some(KeySourceType::RSA(path.to_owned())),
|
key_source_type: Some(KeySourceType::RSA(path.to_owned())),
|
||||||
|
refresh: Default::default(),
|
||||||
claims_checker: None,
|
claims_checker: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -55,6 +59,7 @@ where
|
||||||
pub fn from_ec_pem(path: &'static str) -> JwtAuthorizer<C> {
|
pub fn from_ec_pem(path: &'static str) -> JwtAuthorizer<C> {
|
||||||
JwtAuthorizer {
|
JwtAuthorizer {
|
||||||
key_source_type: Some(KeySourceType::EC(path.to_owned())),
|
key_source_type: Some(KeySourceType::EC(path.to_owned())),
|
||||||
|
refresh: Default::default(),
|
||||||
claims_checker: None,
|
claims_checker: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -63,6 +68,7 @@ where
|
||||||
pub fn from_ed_pem(path: &'static str) -> JwtAuthorizer<C> {
|
pub fn from_ed_pem(path: &'static str) -> JwtAuthorizer<C> {
|
||||||
JwtAuthorizer {
|
JwtAuthorizer {
|
||||||
key_source_type: Some(KeySourceType::ED(path.to_owned())),
|
key_source_type: Some(KeySourceType::ED(path.to_owned())),
|
||||||
|
refresh: Default::default(),
|
||||||
claims_checker: None,
|
claims_checker: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -71,12 +77,35 @@ where
|
||||||
pub fn from_secret(secret: &'static str) -> JwtAuthorizer<C> {
|
pub fn from_secret(secret: &'static str) -> JwtAuthorizer<C> {
|
||||||
JwtAuthorizer {
|
JwtAuthorizer {
|
||||||
key_source_type: Some(KeySourceType::Secret(secret)),
|
key_source_type: Some(KeySourceType::Secret(secret)),
|
||||||
|
refresh: Default::default(),
|
||||||
claims_checker: None,
|
claims_checker: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// layer that checks token validity and claim constraints (custom function)
|
/// refresh configuration for jwk store
|
||||||
pub fn with_check(mut self, checker_fn: fn(&C) -> bool) -> JwtAuthorizer<C> {
|
pub fn refresh(mut self, refresh: Refresh) -> JwtAuthorizer<C> {
|
||||||
|
if self.refresh.is_some() {
|
||||||
|
tracing::warn!("More than one refresh configuration found!");
|
||||||
|
}
|
||||||
|
self.refresh = Some(refresh);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// no refresh, jwks will be loaded juste once
|
||||||
|
pub fn no_refresh(mut self) -> JwtAuthorizer<C> {
|
||||||
|
if self.refresh.is_some() {
|
||||||
|
tracing::warn!("More than one refresh configuration found!");
|
||||||
|
}
|
||||||
|
self.refresh = Some(Refresh {
|
||||||
|
strategy: RefreshStrategy::NoRefresh,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// configures token content check (custom function), if false a 403 will be sent.
|
||||||
|
/// (AuthError::InvalidClaims())
|
||||||
|
pub fn check(mut self, checker_fn: fn(&C) -> bool) -> JwtAuthorizer<C> {
|
||||||
self.claims_checker = Some(FnClaimsChecker { checker_fn });
|
self.claims_checker = Some(FnClaimsChecker { checker_fn });
|
||||||
|
|
||||||
self
|
self
|
||||||
|
|
@ -90,7 +119,9 @@ where
|
||||||
Arc::new(Authorizer::from(key_source_type, self.claims_checker.clone())?)
|
Arc::new(Authorizer::from(key_source_type, self.claims_checker.clone())?)
|
||||||
}
|
}
|
||||||
KeySourceType::Jwks(url) => {
|
KeySourceType::Jwks(url) => {
|
||||||
Arc::new(Authorizer::from_jwks_url(url.as_str(), self.claims_checker.clone())?)
|
Arc::new(
|
||||||
|
Authorizer::from_jwks_url(
|
||||||
|
url.as_str(), self.claims_checker.clone(), self.refresh.unwrap_or_default())?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ use serde::de::DeserializeOwned;
|
||||||
|
|
||||||
pub use self::error::AuthError;
|
pub use self::error::AuthError;
|
||||||
pub use layer::JwtAuthorizer;
|
pub use layer::JwtAuthorizer;
|
||||||
|
pub use jwks::key_store_manager::{Refresh, RefreshStrategy};
|
||||||
|
|
||||||
pub mod authorizer;
|
pub mod authorizer;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue