mod decode;
mod error;
mod http_utils;
mod issuer_index;
mod key_service;
mod loading_info;
mod loading_state;
mod log_entry;
mod status_list;
mod token;
mod token_cache;
mod trusted_issuers_loader;
mod validation;
pub(crate) mod test_utils;
pub(crate) use decode::*;
pub(crate) use error::*;
pub use loading_info::TrustedIssuerLoadingInfo;
pub(crate) use token::{Token, TokenClaims};
pub(crate) use token_cache::TokenCache;
pub(crate) use validation::{TrustedIssuerError, ValidateJwtError};
use crate::JwtConfig;
use crate::LogLevel;
use crate::LogWriter;
use crate::authz::MultiIssuerValidationError;
use crate::authz::metrics::MetricsCollector;
use crate::authz::request::TokenInput;
use crate::common::issuer_utils::IssClaim;
use crate::common::policy_store::TrustedIssuer;
use self::http_utils::{GetFromUrl, OpenIdConfig};
use crate::http::HttpClient;
use crate::log::Logger;
use chrono::Utc;
use issuer_index::IssuerIndex;
use key_service::KeyService;
use loading_state::TrustedIssuerLoadingState;
use log_entry::JwtLogEntry;
use smol_str::SmolStr;
use status_list::{JwtStatus, JwtStatusError, StatusListCache};
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex};
use tokio::sync::Notify;
use tokio_util::sync::CancellationToken;
use trusted_issuers_loader::TrustedIssuerLoader;
use validation::{
JwtValidator, JwtValidatorCache, OwnedValidatorInfo, TokenKind, TrustedIssuerValidator,
ValidatedJwt, ValidatorInfo, validate_required_claims,
};
pub(crate) struct JwtService {
validators: Arc<JwtValidatorCache>,
key_service: Arc<KeyService>,
issuer_configs: Arc<IssuerIndex>,
trusted_issuer_validator: TrustedIssuerValidator,
logger: Option<Logger>,
token_cache: TokenCache,
loading_state: Arc<TrustedIssuerLoadingState>,
jwks_refresh_notifiers: Arc<Mutex<HashMap<IssClaim, Arc<Notify>>>>,
jwks_cancel_token: CancellationToken,
metrics: Arc<MetricsCollector>,
}
struct IssuerConfig {
issuer_id: String,
policy: Arc<TrustedIssuer>,
openid_config: Option<OpenIdConfig>,
}
impl JwtService {
pub(crate) async fn new(
jwt_config: &JwtConfig,
trusted_issuers: Option<HashMap<String, TrustedIssuer>>,
logger: Option<Logger>,
metrics: Arc<MetricsCollector>,
http_client: HttpClient,
) -> Result<Self, JwtServiceInitError> {
if jwt_config.jwt_sig_validation && jwt_config.signature_algorithms_supported.is_empty() {
return Err(JwtServiceInitError::NoSupportedAlgorithms);
}
let mut jwt_config = jwt_config.clone();
jwt_config.normalize();
let jwt_config = &jwt_config;
warn_if_jwt_validation_disabled(jwt_config, logger.as_ref());
let status_lists = StatusListCache::default();
let issuer_configs = Arc::new(IssuerIndex::new());
let validators = Arc::new(JwtValidatorCache::default());
let key_service = Arc::new(KeyService::new());
let token_cache = TokenCache::new(
jwt_config.token_cache_max_ttl_secs,
jwt_config.token_cache_capacity,
jwt_config.token_cache_earliest_expiration_eviction,
logger.clone(),
metrics.clone(),
);
let trusted_issuers = trusted_issuers.unwrap_or_default();
let loading_state = Arc::new(TrustedIssuerLoadingState::new(trusted_issuers.len()));
let jwks_refresh_notifiers = Arc::new(Mutex::new(HashMap::new()));
let jwks_cancel_token = CancellationToken::new();
let loader = TrustedIssuerLoader {
jwt_config: jwt_config.clone(),
status_lists: status_lists.clone(),
issuer_configs: issuer_configs.clone(),
validators: validators.clone(),
key_service: key_service.clone(),
token_cache: token_cache.clone(),
logger: logger.clone(),
loading_state: loading_state.clone(),
http_client,
jwks_refresh_notifiers: jwks_refresh_notifiers.clone(),
jwks_cancel_token: jwks_cancel_token.clone(),
};
loader.load_trusted_issuers(trusted_issuers.clone()).await?;
let trusted_issuer_validator = TrustedIssuerValidator::new(trusted_issuers);
{
let cache = token_cache.clone();
let cancel = jwks_cancel_token.clone();
crate::http::spawn_task(async move {
loop {
tokio::select! {
() = crate::async_sleep::sleep(std::time::Duration::from_secs(30)) => {},
() = cancel.cancelled() => { break; },
}
cache.clear_expired();
}
});
}
Ok(Self {
validators,
key_service,
issuer_configs,
trusted_issuer_validator,
logger,
token_cache,
loading_state,
jwks_refresh_notifiers,
jwks_cancel_token,
metrics,
})
}
fn validate_single_token(
&self,
token_kind: &TokenKind,
jwt: &str,
) -> Result<ValidatedJwt, ValidateJwtError> {
let decoded_jwt = decode_jwt(jwt)?;
let decoding_key_info = decoded_jwt.decoding_key_info();
let decoding_key = self.key_service.get_key(&decoding_key_info);
if decoding_key.is_none()
&& let Some(iss) = &decoding_key_info.issuer
{
self.signal_jwks_refresh(iss);
}
let normalized_iss = decoded_jwt.iss();
let validator_key = ValidatorInfo {
iss: normalized_iss.as_ref(),
token_kind: token_kind.clone(),
algorithm: decoded_jwt.header.alg,
};
let validator: Arc<JwtValidator> = self
.validators
.get(&validator_key)
.ok_or(ValidateJwtError::MissingValidator(validator_key.owned()))?;
let mut validated_jwt = validator.validate_jwt(jwt, decoding_key)?;
let iss_claim = decoded_jwt.iss();
let trusted_iss = if let Some(iss) = iss_claim {
match self.trusted_issuer_validator.find_trusted_issuer(&iss) {
Ok(issuer) => Some(issuer),
Err(TrustedIssuerError::UntrustedIssuer(_)) => {
self.logger.log_any(JwtLogEntry::new(
format!("Untrusted issuer '{iss}', falling back to issuer_configs"),
Some(LogLevel::DEBUG),
));
self.get_issuer_ref(&iss)
},
Err(e) => {
self.logger.log_any(JwtLogEntry::new(
format!(
"Error finding trusted issuer '{iss}': {e}, falling back to issuer_configs"
),
Some(LogLevel::DEBUG),
));
self.get_issuer_ref(&iss)
},
}
} else {
None
};
validated_jwt.trusted_iss.clone_from(&trusted_iss);
if let Some(trusted_iss) = &trusted_iss {
let token_type: Option<&str> = match &token_kind {
TokenKind::AuthzRequestInput(name) => Some(*name),
TokenKind::AuthorizeMultiIssuer(name) => Some(name),
TokenKind::StatusList => None, };
if let Some(token_type) = token_type {
if let Some(token_metadata) = trusted_iss.token_metadata.get(token_type) {
if let Err(err) =
validate_required_claims(&validated_jwt.claims, token_type, token_metadata)
{
self.logger.log_any(JwtLogEntry::new(
format!(
"Token '{token_type}' failed required claims validation: {err}"
),
Some(LogLevel::ERROR),
));
match err {
TrustedIssuerError::MissingRequiredClaim { claim, .. } => {
return Err(ValidateJwtError::MissingClaims(vec![claim]));
},
_ => {
return Err(ValidateJwtError::TrustedIssuerValidation(err));
},
}
}
}
}
}
Ok(validated_jwt)
}
pub(crate) fn validate_multi_issuer_tokens(
&self,
tokens: &[TokenInput],
) -> Result<HashMap<String, Arc<Token>>, MultiIssuerValidationError> {
if tokens.is_empty() {
return Err(MultiIssuerValidationError::EmptyTokenArray);
}
let mut validated_tokens = HashMap::new();
let mut seen_combinations = HashSet::new();
let now = Utc::now();
for (index, token) in tokens.iter().enumerate() {
if let Err(err) = token.validate() {
if let Some(logger) = &self.logger {
logger.log_any(JwtLogEntry::new(
format!("Token validation failed at index {index}: {err}"),
Some(LogLevel::WARN),
));
}
continue;
}
let token_type = self.find_token_metadata_key(&token.mapping);
let token_kind = TokenKind::AuthorizeMultiIssuer(token_type);
let token_name = token.mapping.clone();
if let Some(cedar_token) = self.token_cache.find(&token_kind, &token.payload) {
validated_tokens.insert(token_name, cedar_token);
} else {
match self.validate_single_token(&token_kind, &token.payload) {
Ok(validated_jwt) => {
self.metrics.record_jwt_validation(true);
let issuer = validated_jwt
.claims
.get("iss")
.and_then(|iss| iss.as_str())
.ok_or(MultiIssuerValidationError::MissingIssuer)?;
let combination =
(SmolStr::from(issuer), SmolStr::from(token.mapping.as_str()));
if seen_combinations.insert(combination) {
let claims = TokenClaims::try_from(validated_jwt.claims)
.map_err(MultiIssuerValidationError::InvalidClaims)?;
let cedar_token = Arc::new(Token::new(
&token_name,
claims,
validated_jwt.trusted_iss,
));
self.token_cache.save(
&token_kind,
&token.payload,
cedar_token.clone(),
now,
);
validated_tokens.insert(token_name, cedar_token);
} else {
if let Some(logger) = &self.logger {
logger.log_any(JwtLogEntry::new(
format!(
"Non-deterministic token detected: type '{}' from issuer '{}' (duplicate found, skipping)",
token.mapping, issuer
),
Some(LogLevel::WARN),
));
}
}
},
Err(err) => {
self.metrics.record_jwt_validation(false);
if let Some(logger) = &self.logger {
logger.log_any(JwtLogEntry::new(
format!("Token validation failed at index {index}: {err}"),
Some(LogLevel::WARN),
));
}
self.metrics.record_error(&err);
},
}
}
}
if validated_tokens.is_empty() {
if let Some(logger) = &self.logger {
logger.log_any(JwtLogEntry::new(
"No valid tokens found in multi-issuer request".to_string(),
Some(LogLevel::ERROR),
));
}
return Err(MultiIssuerValidationError::TokenValidationFailed);
}
Ok(validated_tokens)
}
#[inline]
fn get_issuer_ref(&self, iss_claim: &IssClaim) -> Option<Arc<TrustedIssuer>> {
self.issuer_configs.get_trusted_issuer(iss_claim)
}
fn signal_jwks_refresh(&self, iss: &IssClaim) {
let notify = {
let notifiers = self
.jwks_refresh_notifiers
.lock()
.expect("acquire jwks_refresh_notifiers lock");
notifiers.get(iss).cloned()
};
if let Some(notify) = notify {
notify.notify_one();
self.logger.log_any(JwtLogEntry::new(
format!(
"signalled background JWKS refresh for issuer '{}'",
iss.as_str()
),
Some(LogLevel::INFO),
));
}
}
fn find_token_metadata_key<'a>(&'a self, entity_type_name: &'a str) -> Cow<'a, str> {
if let Some(token_key) = self
.issuer_configs
.find_token_metadata_key(entity_type_name)
{
return Cow::Owned(token_key);
}
Cow::Borrowed(entity_type_name)
}
}
impl Drop for JwtService {
fn drop(&mut self) {
self.jwks_cancel_token.cancel();
}
}
impl TrustedIssuerLoadingInfo for JwtService {
fn is_trusted_issuer_loaded_by_name(&self, issuer_id: &str) -> bool {
self.issuer_configs.is_issuer_id_present(issuer_id)
}
fn is_trusted_issuer_loaded_by_iss(&self, iss_claim: &str) -> bool {
let iss = IssClaim::new(iss_claim);
self.issuer_configs.contains_iss(&iss)
}
fn loaded_trusted_issuers_count(&self) -> usize {
self.issuer_configs.len()
}
fn total_issuers(&self) -> usize {
self.loading_state.total_issuers()
}
fn loaded_trusted_issuer_ids(&self) -> HashSet<String> {
self.issuer_configs.loaded_issuer_ids()
}
fn failed_trusted_issuer_ids(&self) -> HashSet<String> {
self.loading_state.failed_issuers()
}
}
fn warn_if_jwt_validation_disabled(jwt_config: &JwtConfig, logger: Option<&Logger>) {
if !jwt_config.jwt_sig_validation {
logger.log_any(JwtLogEntry::new(
"JWT signature validation is disabled (CEDARLING_JWT_SIG_VALIDATION=disabled); \
tokens are accepted without cryptographic verification"
.to_string(),
Some(LogLevel::WARN),
));
}
if !jwt_config.jwt_status_validation {
logger.log_any(JwtLogEntry::new(
"JWT status validation is disabled (CEDARLING_JWT_STATUS_VALIDATION=disabled); \
revoked tokens may continue to be accepted"
.to_string(),
Some(LogLevel::WARN),
));
}
}
#[cfg(test)]
mod test {
use super::JwtService;
use super::TrustedIssuerLoadingInfo;
use super::test_utils::*;
use crate::JwtConfig;
use crate::authz::MultiIssuerValidationError;
use crate::authz::metrics::MetricsCollector;
use crate::authz::request::TokenInput;
use crate::common::policy_store::TokenEntityMetadata;
use crate::http::HttpClient;
use crate::http::HttpClientConfig;
use jsonwebtoken::Algorithm;
use serde_json::json;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use std::sync::LazyLock;
use std::time::Duration;
use tokio::test;
static HTTP_CLIENT: LazyLock<HttpClient> = LazyLock::new(|| {
HttpClient::new(HttpClientConfig {
max_retries: 0,
retry_delay: Duration::from_millis(3),
request_timeout: Duration::from_millis(500),
max_response_size_bytes: None,
})
.expect("http client should be constructed")
});
#[test]
async fn test_validate_multi_issuer_tokens_success() {
let mut server = MockServer::new_with_defaults().await.unwrap();
let mut access_tkn_claims = json!({
"iss": server.issuer(),
"sub": "user123",
"jti": 1_231_231_231,
"exp": u64::MAX,
"client_id": "test123",
});
let access_tkn = server
.generate_token_with_hs256sig(&mut access_tkn_claims, None)
.unwrap();
let mut id_tkn_claims = json!({
"iss": server.issuer(),
"sub": "user123",
"exp": u64::MAX,
"aud": ["test123"],
});
let id_tkn = server
.generate_token_with_hs256sig(&mut id_tkn_claims, None)
.unwrap();
let mut iss = server.trusted_issuer();
iss.token_metadata.insert(
"Jans::Access_Token".to_string(),
TokenEntityMetadata {
trusted: true,
entity_type_name: "Jans::Access_Token".to_string(),
token_id: "jti".to_string(),
required_claims: HashSet::new(),
},
);
iss.token_metadata.insert(
"Jans::Id_Token".to_string(),
TokenEntityMetadata {
trusted: true,
entity_type_name: "Jans::Id_Token".to_string(),
token_id: "jti".to_string(),
required_claims: HashSet::new(),
},
);
let jwt_service = JwtService::new(
&JwtConfig {
jwks: None,
jwt_sig_validation: true,
jwt_status_validation: false,
signature_algorithms_supported: HashSet::from_iter([Algorithm::HS256]),
..Default::default()
},
Some(HashMap::from([(server.issuer().to_string(), iss)])),
None,
Arc::new(MetricsCollector::new(0)),
HTTP_CLIENT.clone(),
)
.await
.expect("Should create JwtService");
let tokens = vec![
TokenInput::new("Jans::Access_Token".to_string(), access_tkn),
TokenInput::new("Jans::Id_Token".to_string(), id_tkn),
];
let result = jwt_service.validate_multi_issuer_tokens(&tokens);
assert!(result.is_ok());
let validated_tokens = result.unwrap();
assert_eq!(validated_tokens.len(), 2);
assert!(validated_tokens.contains_key("Jans::Access_Token"));
assert!(validated_tokens.contains_key("Jans::Id_Token"));
}
#[test]
async fn test_validate_multi_issuer_tokens_empty_array() {
let server = MockServer::new_with_defaults().await.unwrap();
let iss = server.trusted_issuer();
let jwt_service = JwtService::new(
&JwtConfig {
jwks: None,
jwt_sig_validation: true,
jwt_status_validation: false,
signature_algorithms_supported: HashSet::from_iter([Algorithm::HS256]),
..Default::default()
},
Some(HashMap::from([(server.issuer().to_string(), iss)])),
None,
Arc::new(MetricsCollector::new(0)),
HTTP_CLIENT.clone(),
)
.await
.expect("Should create JwtService");
let result = jwt_service.validate_multi_issuer_tokens(&[]);
assert!(matches!(
result,
Err(MultiIssuerValidationError::EmptyTokenArray)
));
}
#[test]
async fn test_validate_multi_issuer_tokens_invalid_token_format() {
let server = MockServer::new_with_defaults().await.unwrap();
let iss = server.trusted_issuer();
let jwt_service = JwtService::new(
&JwtConfig {
jwks: None,
jwt_sig_validation: true,
jwt_status_validation: false,
signature_algorithms_supported: HashSet::from_iter([Algorithm::HS256]),
..Default::default()
},
Some(HashMap::from([(server.issuer().to_string(), iss)])),
None,
Arc::new(MetricsCollector::new(0)),
HTTP_CLIENT.clone(),
)
.await
.expect("Should create JwtService");
let tokens = vec![
TokenInput::new("Jans::Access_Token".to_string(), "invalid-jwt".to_string()),
TokenInput::new("Jans::Id_Token".to_string(), "also-invalid".to_string()),
];
let result = jwt_service.validate_multi_issuer_tokens(&tokens);
assert!(matches!(
result,
Err(MultiIssuerValidationError::TokenValidationFailed)
));
}
#[test]
async fn test_validate_multi_issuer_tokens_graceful_validation() {
let mut server = MockServer::new_with_defaults().await.unwrap();
let mut valid_claims = json!({
"iss": server.issuer(),
"sub": "user123",
"jti": 1_231_231_231,
"exp": u64::MAX,
});
let valid_token = server
.generate_token_with_hs256sig(&mut valid_claims, None)
.unwrap();
let mut iss = server.trusted_issuer();
iss.token_metadata.insert(
"Jans::Access_Token".to_string(),
TokenEntityMetadata {
trusted: true,
entity_type_name: "Jans::Access_Token".to_string(),
token_id: "jti".to_string(),
required_claims: HashSet::new(),
},
);
iss.token_metadata.insert(
"Jans::Id_Token".to_string(),
TokenEntityMetadata {
trusted: true,
entity_type_name: "Jans::Id_Token".to_string(),
token_id: "jti".to_string(),
required_claims: HashSet::new(),
},
);
let jwt_service = JwtService::new(
&JwtConfig {
jwks: None,
jwt_sig_validation: true,
jwt_status_validation: false,
signature_algorithms_supported: HashSet::from_iter([Algorithm::HS256]),
..Default::default()
},
Some(HashMap::from([(server.issuer().to_string(), iss)])),
None,
Arc::new(MetricsCollector::new(0)),
HTTP_CLIENT.clone(),
)
.await
.expect("Should create JwtService");
let tokens = vec![
TokenInput::new("Jans::Access_Token".to_string(), valid_token),
TokenInput::new("Jans::Id_Token".to_string(), "invalid-jwt".to_string()),
];
let result = jwt_service.validate_multi_issuer_tokens(&tokens);
assert!(result.is_ok());
let validated_tokens = result.unwrap();
assert_eq!(validated_tokens.len(), 1); }
#[test]
async fn test_validate_multi_issuer_tokens_non_deterministic_graceful() {
let mut server = MockServer::new_with_defaults().await.unwrap();
let mut claims1 = json!({
"iss": server.issuer(),
"sub": "user123",
"jti": 1_231_231_231,
"exp": u64::MAX,
});
let token_one = server
.generate_token_with_hs256sig(&mut claims1, None)
.unwrap();
let mut claims2 = json!({
"iss": server.issuer(),
"sub": "user456",
"jti": 1_231_231_232,
"exp": u64::MAX,
});
let token_two = server
.generate_token_with_hs256sig(&mut claims2, None)
.unwrap();
let mut iss = server.trusted_issuer();
iss.token_metadata.insert(
"Jans::Access_Token".to_string(),
TokenEntityMetadata {
trusted: true,
entity_type_name: "Jans::Access_Token".to_string(),
token_id: "jti".to_string(),
required_claims: HashSet::new(),
},
);
let jwt_service = JwtService::new(
&JwtConfig {
jwks: None,
jwt_sig_validation: true,
jwt_status_validation: false,
signature_algorithms_supported: HashSet::from_iter([Algorithm::HS256]),
..Default::default()
},
Some(HashMap::from([(server.issuer().to_string(), iss)])),
None,
Arc::new(MetricsCollector::new(0)),
HTTP_CLIENT.clone(),
)
.await
.expect("Should create JwtService");
let tokens = vec![
TokenInput::new("Jans::Access_Token".to_string(), token_one),
TokenInput::new("Jans::Access_Token".to_string(), token_two), ];
let result = jwt_service.validate_multi_issuer_tokens(&tokens);
assert!(result.is_ok());
let validated_tokens = result.unwrap();
assert_eq!(validated_tokens.len(), 1); }
#[test]
async fn test_validate_multi_issuer_tokens_missing_issuer() {
let mut server = MockServer::new_with_defaults().await.unwrap();
let mut claims = json!({
"sub": "user123",
"exp": u64::MAX,
});
let token = server
.generate_token_with_hs256sig(&mut claims, None)
.unwrap();
let iss = server.trusted_issuer();
let jwt_service = JwtService::new(
&JwtConfig {
jwks: None,
jwt_sig_validation: true,
jwt_status_validation: false,
signature_algorithms_supported: HashSet::from_iter([Algorithm::HS256]),
..Default::default()
},
Some(HashMap::from([(server.issuer().to_string(), iss)])),
None,
Arc::new(MetricsCollector::new(0)),
HTTP_CLIENT.clone(),
)
.await
.expect("Should create JwtService");
let tokens = vec![TokenInput::new("Jans::Access_Token".to_string(), token)];
let result = jwt_service.validate_multi_issuer_tokens(&tokens);
assert!(matches!(
result,
Err(MultiIssuerValidationError::TokenValidationFailed)
));
}
#[test]
async fn test_validate_multi_issuer_tokens_succeeds_after_key_rotation_without_reinit() {
let mut server = MockServer::new_with_defaults()
.await
.expect("Mock server with default OIDC/JWKS should initialize");
let mapping = "Jans::Access_Token".to_string();
let mut iss = server.trusted_issuer();
iss.token_metadata.insert(
mapping.clone(),
TokenEntityMetadata {
trusted: true,
entity_type_name: mapping.clone(),
token_id: "jti".to_string(),
required_claims: HashSet::new(),
},
);
let jwt_service = JwtService::new(
&JwtConfig {
jwks: None,
jwt_sig_validation: true,
jwt_status_validation: false,
signature_algorithms_supported: HashSet::from_iter([Algorithm::HS256]),
jwks_refresh_min_interval: 0,
..Default::default()
},
Some(HashMap::from([(server.issuer().to_string(), iss)])),
None,
Arc::new(MetricsCollector::new(0)),
HTTP_CLIENT.clone(),
)
.await
.expect("JwtService should initialize with trusted issuer metadata");
let mut claims_before_rotation = json!({
"iss": server.issuer(),
"sub": "user-before-rotation",
"jti": 1_111_111_111_u64,
"exp": u64::MAX,
});
let token_before_rotation = server
.generate_token_with_hs256sig(&mut claims_before_rotation, None)
.expect("Token signed with initial key should be generated");
jwt_service
.validate_multi_issuer_tokens(&[TokenInput::new(
mapping.clone(),
token_before_rotation,
)])
.expect("Token signed with initial key should validate before key rotation");
server
.rotate_signing_key_hs256("rotated_hs256_key_after_init")
.expect("Mock issuer should rotate signing key and JWKS response");
let mut claims_after_rotation = json!({
"iss": server.issuer(),
"sub": "user-after-rotation",
"jti": 2_222_222_222_u64,
"exp": u64::MAX,
});
let token_after_rotation = server
.generate_token_with_hs256sig(&mut claims_after_rotation, None)
.expect("Token signed with rotated key should be generated");
jwt_service
.validate_multi_issuer_tokens(&[TokenInput::new(
mapping.clone(),
token_after_rotation.clone(),
)])
.expect_err("First call after rotation should fail");
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
jwt_service
.validate_multi_issuer_tokens(&[TokenInput::new(mapping, token_after_rotation)])
.expect("Token signed with rotated key should validate after background JWKS refresh");
}
#[test]
async fn test_validate_multi_issuer_tokens_recovers_when_first_seen_kid_is_rotated() {
let mut server = MockServer::new_with_defaults()
.await
.expect("Mock server with default OIDC/JWKS should initialize");
let mapping = "Jans::Access_Token".to_string();
let mut iss = server.trusted_issuer();
iss.token_metadata.insert(
mapping.clone(),
TokenEntityMetadata {
trusted: true,
entity_type_name: mapping.clone(),
token_id: "jti".to_string(),
required_claims: HashSet::new(),
},
);
let jwt_service = JwtService::new(
&JwtConfig {
jwks: None,
jwt_sig_validation: true,
jwt_status_validation: false,
signature_algorithms_supported: HashSet::from_iter([Algorithm::HS256]),
jwks_refresh_min_interval: 0,
..Default::default()
},
Some(HashMap::from([(server.issuer().to_string(), iss)])),
None,
Arc::new(MetricsCollector::new(0)),
HTTP_CLIENT.clone(),
)
.await
.expect("JwtService should initialize with trusted issuer metadata");
server
.rotate_signing_key_hs256("rotated_hs256_key_before_first_validation")
.expect("Mock issuer should rotate signing key and JWKS response");
let mut claims_after_rotation = json!({
"iss": server.issuer(),
"sub": "user-after-rotation",
"jti": 3_333_333_333_u64,
"exp": u64::MAX,
});
let token_after_rotation = server
.generate_token_with_hs256sig(&mut claims_after_rotation, None)
.expect("Token signed with rotated key should be generated");
jwt_service
.validate_multi_issuer_tokens(&[TokenInput::new(
mapping.clone(),
token_after_rotation.clone(),
)])
.expect_err("First call with rotated kid should fail (stale key)");
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
jwt_service
.validate_multi_issuer_tokens(&[TokenInput::new(mapping, token_after_rotation)])
.expect(
"Validation should recover from unknown rotated kid after background JWKS refresh",
);
}
#[test]
async fn test_trusted_issuer_loading_info() {
let server = MockServer::new_with_defaults().await.unwrap();
let iss = server.trusted_issuer();
let jwt_service = JwtService::new(
&JwtConfig {
jwks: None,
jwt_sig_validation: true,
jwt_status_validation: false,
signature_algorithms_supported: HashSet::from_iter([Algorithm::HS256]),
..Default::default()
},
Some(HashMap::from([("Jans".into(), iss)])),
None,
Arc::new(MetricsCollector::new(0)),
HTTP_CLIENT.clone(),
)
.await
.expect("Should create JwtService");
assert!(jwt_service.is_trusted_issuer_loaded_by_name("Jans"));
assert!(!jwt_service.is_trusted_issuer_loaded_by_name("NonExistent"));
assert!(jwt_service.is_trusted_issuer_loaded_by_iss(server.issuer().as_str()));
assert!(!jwt_service.is_trusted_issuer_loaded_by_iss("https://nonexistent.com"));
assert_eq!(jwt_service.loaded_trusted_issuers_count(), 1);
let loaded_ids = jwt_service.loaded_trusted_issuer_ids();
assert_eq!(loaded_ids.len(), 1);
assert!(loaded_ids.contains("Jans"));
let failed_ids = jwt_service.failed_trusted_issuer_ids();
assert!(failed_ids.is_empty());
}
}