use std::collections::HashMap;
use std::sync::Arc;
use serde_json::Value as JsonValue;
use thiserror::Error;
use crate::common::{
issuer_utils::IssClaim,
policy_store::{TokenEntityMetadata, TrustedIssuer},
};
#[derive(Debug, Error)]
pub enum TrustedIssuerError {
#[error("Untrusted issuer: '{0}'")]
UntrustedIssuer(String),
#[error("Missing required claim: '{claim}' for token type '{token_type}'")]
MissingRequiredClaim {
claim: String,
token_type: String,
},
#[error(
"Invalid token metadata configuration: entity_type_name is empty for token type '{token_type}'"
)]
EmptyEntityTypeName {
token_type: String,
},
}
type Result<T> = std::result::Result<T, TrustedIssuerError>;
pub(crate) struct TrustedIssuerValidator {
url_to_issuer: HashMap<IssClaim, Arc<TrustedIssuer>>,
}
impl TrustedIssuerValidator {
pub(crate) fn new(trusted_issuers: HashMap<String, TrustedIssuer>) -> Self {
let mut url_to_issuer = HashMap::with_capacity(trusted_issuers.len());
for issuer in trusted_issuers.into_values().map(Arc::new) {
url_to_issuer.insert(issuer.iss_claim(), issuer);
}
Self { url_to_issuer }
}
pub(crate) fn find_trusted_issuer(
&self,
issuer_claim: &IssClaim,
) -> Result<Arc<TrustedIssuer>> {
if let Some(issuer) = self.url_to_issuer.get(issuer_claim) {
return Ok(issuer.clone());
}
Err(TrustedIssuerError::UntrustedIssuer(
issuer_claim.to_string(),
))
}
}
pub(crate) fn validate_required_claims(
claims: &JsonValue,
token_type: &str,
token_metadata: &TokenEntityMetadata,
) -> Result<()> {
if token_metadata.entity_type_name.is_empty() {
return Err(TrustedIssuerError::EmptyEntityTypeName {
token_type: token_type.to_string(),
});
}
for claim in &token_metadata.required_claims {
if claims.get(claim).is_none() {
return Err(TrustedIssuerError::MissingRequiredClaim {
claim: claim.clone(),
token_type: token_type.to_string(),
});
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use url::Url;
use super::*;
use crate::common::policy_store::TokenEntityMetadata;
use std::collections::HashSet;
fn create_test_issuer(id: &str, endpoint: &str) -> TrustedIssuer {
let mut token_metadata = HashMap::new();
token_metadata.insert(
"access_token".to_string(),
TokenEntityMetadata::access_token(),
);
token_metadata.insert("id_token".to_string(), TokenEntityMetadata::id_token());
TrustedIssuer::new(
format!("Test Issuer {id}"),
"Test issuer for validation".to_string(),
Url::parse(endpoint).unwrap(),
token_metadata,
)
}
#[test]
fn test_find_trusted_issuer_by_url() {
let issuers = HashMap::from([(
"issuer1".to_string(),
create_test_issuer("1", "https://issuer1.com/.well-known/openid-configuration"),
)]);
let validator = TrustedIssuerValidator::new(issuers);
let result = validator.find_trusted_issuer(&IssClaim::new("https://issuer1.com"));
assert!(result.is_ok());
assert_eq!(result.unwrap().name, "Test Issuer 1");
}
#[test]
fn test_untrusted_issuer() {
let issuers = HashMap::from([(
"issuer1".to_string(),
create_test_issuer("1", "https://issuer1.com/.well-known/openid-configuration"),
)]);
let validator = TrustedIssuerValidator::new(issuers);
let result = validator.find_trusted_issuer(&IssClaim::new("https://evil.com"));
assert!(
matches!(result.unwrap_err(), TrustedIssuerError::UntrustedIssuer(_)),
"expected UntrustedIssuer error"
);
}
#[test]
fn test_validate_required_claims_success() {
let claims = serde_json::json!({
"sub": "user123",
"jti": "token123",
"role": "admin"
});
let metadata = TokenEntityMetadata::builder()
.entity_type_name("Jans::Access_token".to_string())
.token_id("jti".to_string())
.build();
let result = validate_required_claims(&claims, "access_token", &metadata);
assert!(result.is_ok());
}
#[test]
fn test_validate_required_claims_missing_sub() {
let claims = serde_json::json!({
"jti": "token123",
"role": "admin"
});
let metadata = TokenEntityMetadata::builder()
.entity_type_name("Jans::Access_token".to_string())
.token_id("jti".to_string())
.required_claims(HashSet::from(["sub".to_string()])) .build();
let result = validate_required_claims(&claims, "access_token", &metadata);
assert!(
matches!(
result.unwrap_err(),
TrustedIssuerError::MissingRequiredClaim { claim, .. } if claim == "sub"
),
"expected MissingRequiredClaim error for 'sub'"
);
}
#[test]
fn test_validate_required_claims_missing_role() {
let claims = serde_json::json!({
"sub": "user123",
"jti": "token123"
});
let metadata = TokenEntityMetadata::builder()
.entity_type_name("Jans::Access_token".to_string())
.token_id("jti".to_string())
.required_claims(HashSet::from(["role".to_string()])) .build();
let result = validate_required_claims(&claims, "access_token", &metadata);
assert!(
matches!(
result.unwrap_err(),
TrustedIssuerError::MissingRequiredClaim { claim, .. } if claim == "role"
),
"expected MissingRequiredClaim error for 'role'"
);
}
#[test]
fn test_validate_required_claims_missing_jti() {
let claims = serde_json::json!({
"sub": "user123",
"role": "admin"
});
let metadata = TokenEntityMetadata::builder()
.entity_type_name("Jans::Access_token".to_string())
.token_id("jti".to_string()) .required_claims(HashSet::from(["jti".to_string()])) .build();
let result = validate_required_claims(&claims, "access_token", &metadata);
assert!(
matches!(
result.unwrap_err(),
TrustedIssuerError::MissingRequiredClaim { claim, .. } if claim == "jti"
),
"expected MissingRequiredClaim error for 'jti'"
);
}
#[test]
fn test_validate_required_claims_mapping_fields_not_required() {
let claims = serde_json::json!({
"iss": "https://issuer.com"
});
let metadata = TokenEntityMetadata::builder()
.entity_type_name("Jans::Access_token".to_string())
.token_id("jti".to_string()) .required_claims(HashSet::new()) .build();
let result = validate_required_claims(&claims, "access_token", &metadata);
assert!(result.is_ok());
}
}