use std::sync::Arc;
use async_trait::async_trait;
use rvoip_core_traits::identity::{CredentialKind, IdentityAssurance};
use rvoip_core_traits::ids::IdentityId;
use crate::bearer::{BearerAuthError, BearerValidator};
#[async_trait]
pub trait ActorTokenValidator: Send + Sync {
async fn validate_actor(&self, token: &str) -> Result<ActorClaims, BearerAuthError>;
}
#[derive(Clone, Debug)]
pub struct ActorClaims {
pub identity: IdentityId,
pub scopes: Vec<String>,
}
pub struct AAuthValidator {
subject: Arc<dyn BearerValidator>,
actor: Arc<dyn ActorTokenValidator>,
}
impl AAuthValidator {
pub fn new(
subject: Arc<dyn BearerValidator>,
actor: Arc<dyn ActorTokenValidator>,
) -> Arc<Self> {
Arc::new(Self { subject, actor })
}
pub async fn validate_aauth(
&self,
subject_token: &str,
actor_token: &str,
) -> Result<IdentityAssurance, BearerAuthError> {
if subject_token.is_empty() {
return Err(BearerAuthError::Empty);
}
if actor_token.is_empty() {
return Err(BearerAuthError::Invalid(
"actor_token required for method=aauth".into(),
));
}
let subject_assurance = self.subject.validate(subject_token).await?;
let actor_claims = self.actor.validate_actor(actor_token).await?;
let (subject_identity, subject_scopes) = match subject_assurance {
IdentityAssurance::UserAuthorized {
user_id, scopes, ..
} => (user_id, scopes),
other => {
return Err(BearerAuthError::Invalid(format!(
"AAuth subject token must validate to UserAuthorized; got {}",
discriminant_label(&other)
)));
}
};
let mut merged_scopes = subject_scopes;
for s in actor_claims.scopes {
if !merged_scopes.contains(&s) {
merged_scopes.push(s);
}
}
let _ = CredentialKind::AAuth;
Ok(IdentityAssurance::UserAuthorized {
user_id: subject_identity,
identity: actor_claims.identity,
scopes: merged_scopes,
})
}
}
fn discriminant_label(a: &IdentityAssurance) -> &'static str {
match a {
IdentityAssurance::Anonymous => "Anonymous",
IdentityAssurance::Pseudonymous { .. } => "Pseudonymous",
IdentityAssurance::Identified { .. } => "Identified",
IdentityAssurance::TaskScoped { .. } => "TaskScoped",
IdentityAssurance::UserAuthorized { .. } => "UserAuthorized",
IdentityAssurance::DtlsFingerprint { .. } => "DtlsFingerprint",
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bearer::{bearer_stub, BearerValidator};
use async_trait::async_trait;
use rvoip_core_traits::identity::IdentityAssurance;
use rvoip_core_traits::ids::IdentityId;
struct StaticActor {
identity: IdentityId,
scopes: Vec<String>,
}
#[async_trait]
impl ActorTokenValidator for StaticActor {
async fn validate_actor(&self, token: &str) -> Result<ActorClaims, BearerAuthError> {
if token.is_empty() {
return Err(BearerAuthError::Empty);
}
Ok(ActorClaims {
identity: self.identity.clone(),
scopes: self.scopes.clone(),
})
}
}
struct StaticSubject {
user_id: IdentityId,
scopes: Vec<String>,
}
#[async_trait]
impl BearerValidator for StaticSubject {
async fn validate(&self, token: &str) -> Result<IdentityAssurance, BearerAuthError> {
if token.is_empty() {
return Err(BearerAuthError::Empty);
}
Ok(IdentityAssurance::UserAuthorized {
identity: self.user_id.clone(),
user_id: self.user_id.clone(),
scopes: self.scopes.clone(),
})
}
}
fn id(s: &str) -> IdentityId {
IdentityId::from_string(s.to_string())
}
#[tokio::test]
async fn aauth_combines_subject_and_actor_into_user_authorized() {
let subject = Arc::new(StaticSubject {
user_id: id("user:alice"),
scopes: vec!["calls.write".into()],
});
let actor = Arc::new(StaticActor {
identity: id("agent:assistant-7"),
scopes: vec!["calls.write".into(), "calls.transfer".into()],
});
let v = AAuthValidator::new(subject, actor);
let assurance = v
.validate_aauth("subject-tok", "actor-tok")
.await
.expect("aauth combine");
match assurance {
IdentityAssurance::UserAuthorized {
user_id,
identity,
scopes,
} => {
assert_eq!(user_id.as_str(), "user:alice");
assert_eq!(identity.as_str(), "agent:assistant-7");
assert_eq!(
scopes,
vec!["calls.write".to_string(), "calls.transfer".to_string()]
);
}
other => panic!(
"expected UserAuthorized; got {:?}",
discriminant_label(&other)
),
}
}
#[tokio::test]
async fn aauth_requires_actor_token() {
let subject = Arc::new(StaticSubject {
user_id: id("user:alice"),
scopes: vec![],
});
let actor = Arc::new(StaticActor {
identity: id("agent:7"),
scopes: vec![],
});
let v = AAuthValidator::new(subject, actor);
let err = v.validate_aauth("subj", "").await.unwrap_err();
match err {
BearerAuthError::Invalid(msg) => assert!(msg.contains("actor_token"), "{msg}"),
other => panic!("expected Invalid for empty actor token; got {other:?}"),
}
}
#[tokio::test]
async fn aauth_rejects_pseudonymous_subject() {
let subject = bearer_stub();
let actor = Arc::new(StaticActor {
identity: id("agent:7"),
scopes: vec![],
});
let v = AAuthValidator::new(subject, actor);
let err = v.validate_aauth("subj-tok", "actor-tok").await.unwrap_err();
match err {
BearerAuthError::Invalid(msg) => {
assert!(msg.contains("Pseudonymous") || msg.contains("unsupported"));
}
other => panic!("expected Invalid; got {other:?}"),
}
}
}