use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use async_trait::async_trait;
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
use rvoip_core_traits::identity::IdentityAssurance;
use rvoip_core_traits::ids::IdentityId;
use serde::Deserialize;
use crate::bearer::{BearerAuthError, BearerValidator};
use crate::providers::{
CredentialAuthError, TokenRevocationChecker, TokenRevocationContext, TokenRevocationStatus,
};
#[derive(Debug, Deserialize)]
struct Claims {
sub: String,
#[serde(default)]
iss: Option<String>,
#[serde(default)]
iat: Option<u64>,
#[serde(default)]
exp: Option<u64>,
#[serde(default)]
jti: Option<String>,
#[serde(default)]
scope: Option<String>,
#[serde(default)]
scopes: Option<Vec<String>>,
#[serde(default)]
roles: Option<Vec<String>>,
#[serde(default)]
realm_access: Option<RoleAccess>,
#[serde(default)]
resource_access: Option<HashMap<String, RoleAccess>>,
}
#[derive(Debug, Deserialize)]
struct RoleAccess {
#[serde(default)]
roles: Vec<String>,
}
pub struct JwtValidator {
decoding_key: DecodingKey,
validation: Validation,
revocation_checker: Option<Arc<dyn TokenRevocationChecker>>,
}
impl JwtValidator {
pub fn from_decoding_key(decoding_key: DecodingKey, algorithm: Algorithm) -> Self {
let mut validation = Validation::new(algorithm);
validation.validate_aud = false;
Self {
decoding_key,
validation,
revocation_checker: None,
}
}
pub fn from_hmac_secret(secret: &[u8]) -> Self {
let mut validation = Validation::new(Algorithm::HS256);
validation.set_audience::<&str>(&[]);
validation.validate_aud = false;
Self {
decoding_key: DecodingKey::from_secret(secret),
validation,
revocation_checker: None,
}
}
pub fn from_rsa_pem(pem: &[u8]) -> Result<Self, BearerAuthError> {
let key = DecodingKey::from_rsa_pem(pem)
.map_err(|e| BearerAuthError::Unavailable(format!("invalid RSA PEM: {e}")))?;
let mut validation = Validation::new(Algorithm::RS256);
validation.validate_aud = false;
Ok(Self {
decoding_key: key,
validation,
revocation_checker: None,
})
}
pub fn from_ec_pem(pem: &[u8]) -> Result<Self, BearerAuthError> {
let key = DecodingKey::from_ec_pem(pem)
.map_err(|e| BearerAuthError::Unavailable(format!("invalid EC PEM: {e}")))?;
let mut validation = Validation::new(Algorithm::ES256);
validation.validate_aud = false;
Ok(Self {
decoding_key: key,
validation,
revocation_checker: None,
})
}
pub fn with_algorithm(mut self, algorithm: Algorithm) -> Self {
self.validation.algorithms = vec![algorithm];
self
}
pub fn with_revocation_checker(mut self, checker: Arc<dyn TokenRevocationChecker>) -> Self {
self.revocation_checker = Some(checker);
self
}
pub fn with_audience<I, S>(mut self, audiences: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let auds: HashSet<String> = audiences
.into_iter()
.map(|s| s.as_ref().to_string())
.collect();
self.validation
.set_audience(&auds.into_iter().collect::<Vec<_>>());
self.validation.validate_aud = true;
self
}
pub fn with_issuer<I, S>(mut self, issuers: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
self.validation.set_issuer(
&issuers
.into_iter()
.map(|s| s.as_ref().to_string())
.collect::<Vec<_>>(),
);
self
}
pub fn into_arc(self) -> Arc<dyn BearerValidator> {
Arc::new(self)
}
}
#[async_trait]
impl BearerValidator for JwtValidator {
async fn validate(&self, token: &str) -> Result<IdentityAssurance, BearerAuthError> {
if token.is_empty() {
return Err(BearerAuthError::Empty);
}
let data = decode::<Claims>(token, &self.decoding_key, &self.validation)
.map_err(|e| BearerAuthError::Invalid(e.to_string()))?;
let claims = data.claims;
let revocation_context = revocation_context_from_claims(&claims);
check_revocation(
self.revocation_checker.as_ref(),
revocation_context.as_ref(),
)
.await?;
let identity = IdentityId::from_string(claims.sub);
let scopes = scopes_from_claims(
claims.scope,
claims.scopes,
claims.roles,
claims.realm_access,
claims.resource_access,
);
Ok(IdentityAssurance::UserAuthorized {
identity: identity.clone(),
user_id: identity,
scopes,
})
}
}
async fn check_revocation(
checker: Option<&Arc<dyn TokenRevocationChecker>>,
context: Option<&TokenRevocationContext>,
) -> Result<(), BearerAuthError> {
let Some(checker) = checker else {
return Ok(());
};
let Some(context) = context else {
return Err(BearerAuthError::Invalid(
"token missing jti for revocation check".into(),
));
};
match checker.check_token(context).await {
Ok(TokenRevocationStatus::Active) => Ok(()),
Ok(TokenRevocationStatus::Revoked) => Err(BearerAuthError::Invalid("token revoked".into())),
Err(CredentialAuthError::Invalid) | Err(CredentialAuthError::PolicyRejected(_)) => Err(
BearerAuthError::Invalid("revocation check rejected token".into()),
),
Err(CredentialAuthError::Unavailable(err)) => Err(BearerAuthError::Unavailable(err)),
}
}
fn revocation_context_from_claims(claims: &Claims) -> Option<TokenRevocationContext> {
let token_id = claims.jti.clone()?;
let mut context = TokenRevocationContext::new(token_id).with_subject(claims.sub.clone());
if let Some(issuer) = claims.iss.clone() {
context = context.with_issuer(issuer);
}
context = context.with_times(
claims.iat.and_then(unix_seconds_to_system_time),
claims.exp.and_then(unix_seconds_to_system_time),
);
Some(context)
}
fn unix_seconds_to_system_time(seconds: u64) -> Option<SystemTime> {
UNIX_EPOCH.checked_add(Duration::from_secs(seconds))
}
fn scopes_from_claims(
scope: Option<String>,
scopes: Option<Vec<String>>,
roles: Option<Vec<String>>,
realm_access: Option<RoleAccess>,
resource_access: Option<HashMap<String, RoleAccess>>,
) -> Vec<String> {
let mut values = Vec::new();
if let Some(scope) = scope {
values.extend(scope.split_whitespace().map(str::to_string));
}
if let Some(scopes) = scopes {
for scope in scopes {
push_unique(&mut values, scope);
}
}
if let Some(roles) = roles {
for role in roles {
push_unique(&mut values, format!("role:{role}"));
}
}
if let Some(realm_access) = realm_access {
for role in realm_access.roles {
push_unique(&mut values, format!("realm:{role}"));
}
}
if let Some(resource_access) = resource_access {
for (client, access) in resource_access {
for role in access.roles {
push_unique(&mut values, format!("{client}:{role}"));
}
}
}
values
}
fn push_unique(values: &mut Vec<String>, value: String) {
if !values.contains(&value) {
values.push(value);
}
}