use std::sync::Arc;
use std::time::SystemTime;
use crate::domain::mcp::ResourceUri;
use crate::domain::mcp::authorization::AuthorizationModel;
use crate::domain::registry::{AuthContext, Scope, ScopeSet};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Audience(Arc<str>);
impl Audience {
#[must_use]
pub fn new(value: impl AsRef<str>) -> Self {
Self(Arc::from(value.as_ref().trim()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl From<&str> for Audience {
fn from(value: &str) -> Self {
Self::new(value)
}
}
impl From<String> for Audience {
fn from(value: String) -> Self {
Self::new(value)
}
}
impl From<&ResourceUri> for Audience {
fn from(value: &ResourceUri) -> Self {
Self::new(value.as_str())
}
}
impl From<ResourceUri> for Audience {
fn from(value: ResourceUri) -> Self {
Self::new(value.as_str())
}
}
impl std::fmt::Display for Audience {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct TokenClaims {
subject: Option<Arc<str>>,
issuer: Option<Arc<str>>,
audience: Vec<Audience>,
expires_at: Option<SystemTime>,
not_before: Option<SystemTime>,
scopes: Vec<Arc<str>>,
}
impl TokenClaims {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_subject(mut self, subject: impl Into<Arc<str>>) -> Self {
self.subject = Some(subject.into());
self
}
#[must_use]
pub fn with_issuer(mut self, issuer: impl Into<Arc<str>>) -> Self {
self.issuer = Some(issuer.into());
self
}
#[must_use]
pub fn with_audience(mut self, audience: impl Into<Audience>) -> Self {
self.audience.push(audience.into());
self
}
#[must_use]
pub fn with_audience_string(mut self, audience: impl AsRef<str>) -> Self {
self.audience.push(Audience::new(audience));
self
}
#[must_use]
pub const fn with_expiry(mut self, at: SystemTime) -> Self {
self.expires_at = Some(at);
self
}
#[must_use]
pub const fn with_not_before(mut self, at: SystemTime) -> Self {
self.not_before = Some(at);
self
}
#[must_use]
pub fn with_scope(mut self, scope: impl Into<Arc<str>>) -> Self {
self.scopes.push(scope.into());
self
}
#[must_use]
pub fn subject(&self) -> Option<&str> {
self.subject.as_deref()
}
#[must_use]
pub fn issuer(&self) -> Option<&str> {
self.issuer.as_deref()
}
#[must_use]
pub fn audience(&self) -> &[Audience] {
&self.audience
}
#[must_use]
pub fn scopes(&self) -> &[Arc<str>] {
&self.scopes
}
#[must_use]
pub fn is_audience(&self, audience: &ResourceUri) -> bool {
self.audience
.iter()
.any(|entry| entry.as_str() == audience.as_str())
}
#[must_use]
pub fn is_audience_str(&self, audience: &str) -> bool {
self.audience.iter().any(|entry| entry.as_str() == audience)
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum Refusal {
#[error("the access token has expired")]
Expired,
#[error("the access token is not valid yet")]
NotYetValid,
#[error("the access token is not bound to this resource")]
AudienceMismatch,
#[error("the access token was issued by an unexpected issuer")]
IssuerMismatch,
#[error("the access token names no subject")]
NoSubject,
#[error("the access token carries no recognised scope")]
Unscoped,
#[error("the access token is not a well-formed JWS")]
Malformed,
#[error("the access token's signature could not be verified")]
Unverified,
}
pub trait ScopePolicy: Send + Sync {
fn resolve(&self, scope: &str) -> Option<Scope>;
fn principal(&self, subject: &str, scopes: &[Arc<str>]) -> String {
let _ = scopes;
subject.to_owned()
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NoScopes;
impl ScopePolicy for NoScopes {
fn resolve(&self, _scope: &str) -> Option<Scope> {
None
}
}
#[derive(Debug, Clone)]
pub struct TokenPolicy<'a> {
resource: &'a ResourceUri,
model: AuthorizationModel,
expected_issuer: Option<Arc<str>>,
expected_audiences: Option<Arc<[Audience]>>,
}
impl<'a> TokenPolicy<'a> {
#[must_use]
pub const fn new(resource: &'a ResourceUri, model: AuthorizationModel) -> Self {
Self {
resource,
model,
expected_issuer: None,
expected_audiences: None,
}
}
#[must_use]
pub fn with_expected_issuer(mut self, issuer: impl Into<Arc<str>>) -> Self {
self.expected_issuer = Some(issuer.into());
self
}
#[must_use]
pub fn with_expected_audience(mut self, audience: impl Into<Audience>) -> Self {
self.expected_audiences = Some(Arc::from([audience.into()]));
self
}
#[must_use]
pub fn with_expected_audiences<I, T>(mut self, audiences: I) -> Self
where
I: IntoIterator<Item = T>,
T: Into<Audience>,
{
self.expected_audiences = Some(Arc::from(
audiences.into_iter().map(Into::into).collect::<Vec<_>>(),
));
self
}
#[must_use]
pub fn expected_audiences(&self) -> Option<&[Audience]> {
self.expected_audiences.as_deref()
}
#[must_use]
fn accepts_audience(&self, claims: &TokenClaims) -> bool {
if claims.is_audience(self.resource) {
return true;
}
self.expected_audiences
.as_deref()
.is_some_and(|audiences| audiences.iter().any(|a| claims.is_audience_str(a.as_str())))
}
#[must_use]
pub const fn accepts_any_issuer(&self) -> bool {
self.expected_issuer.is_none()
}
}
pub fn verify(
claims: &TokenClaims,
policy: &TokenPolicy<'_>,
scopes: &dyn ScopePolicy,
now: SystemTime,
) -> Result<AuthContext, Refusal> {
if !policy.model.audience_bound() {
return Err(Refusal::AudienceMismatch);
}
if claims.expires_at.is_some_and(|expiry| expiry <= now) {
return Err(Refusal::Expired);
}
if claims.not_before.is_some_and(|start| start > now) {
return Err(Refusal::NotYetValid);
}
if !policy.accepts_audience(claims) {
return Err(Refusal::AudienceMismatch);
}
if let Some(expected) = &policy.expected_issuer
&& claims
.issuer()
.is_some_and(|issuer| issuer != expected.as_ref())
{
return Err(Refusal::IssuerMismatch);
}
let subject = claims.subject().ok_or(Refusal::NoSubject)?;
let mut resolved = ScopeSet::empty();
for scope in claims.scopes() {
if let Some(scope) = scopes.resolve(scope) {
resolved = resolved.with(scope);
}
}
if resolved.is_empty() {
return Err(Refusal::Unscoped);
}
Ok(AuthContext::new(
scopes.principal(subject, &claims.scopes),
resolved,
))
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum SourceError {
#[error(transparent)]
Refused(#[from] Refusal),
#[error("the authorization server could not be asked: {0}")]
Unavailable(String),
}
pub trait TokenSource: Send + Sync {
fn claims(
&self,
presented: &str,
) -> impl Future<Output = Result<TokenClaims, SourceError>> + Send;
}
pub async fn authenticate(
source: &impl TokenSource,
policy: &TokenPolicy<'_>,
scopes: &dyn ScopePolicy,
bearer: &str,
now: SystemTime,
) -> Result<AuthContext, SourceError> {
let claims = source.claims(bearer).await?;
Ok(verify(&claims, policy, scopes, now)?)
}
#[cfg(test)]
#[path = "token/tests.rs"]
mod tests;