use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::time::SystemTime;
use super::{AuthContext, Authenticator, Credentials};
use crate::application::oauth::{ScopePolicy, TokenPolicy, TokenSource, authenticate};
use crate::domain::ProtocolVersion;
use crate::domain::authorization_model;
use crate::domain::mcp::ResourceMetadata;
pub trait WallClock: Send + Sync {
fn now(&self) -> SystemTime;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct SystemClock;
impl WallClock for SystemClock {
fn now(&self) -> SystemTime {
SystemTime::now()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Presented<'a> {
Bearer(&'a str),
Nothing,
}
fn bearer(header: Option<&str>) -> Presented<'_> {
let Some(header) = header else {
return Presented::Nothing;
};
let Some((scheme, token)) = header.split_once(char::is_whitespace) else {
return Presented::Nothing;
};
if !scheme.eq_ignore_ascii_case("Bearer") {
return Presented::Nothing;
}
let token = token.trim();
if token.is_empty() {
return Presented::Nothing;
}
Presented::Bearer(token)
}
pub struct OAuthAuthenticator<A> {
source: A,
metadata: Arc<ResourceMetadata>,
version: ProtocolVersion,
expected_issuer: Option<Arc<str>>,
scopes: Arc<dyn ScopePolicy>,
clock: Arc<dyn WallClock>,
}
impl<A> std::fmt::Debug for OAuthAuthenticator<A> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OAuthAuthenticator")
.field("resource", self.metadata.resource())
.field("version", &self.version)
.field("expected_issuer", &self.expected_issuer)
.finish_non_exhaustive()
}
}
impl<A> OAuthAuthenticator<A> {
#[must_use]
pub fn new(
source: A,
metadata: ResourceMetadata,
version: ProtocolVersion,
scopes: impl ScopePolicy + 'static,
clock: impl WallClock + 'static,
) -> Self {
Self {
source,
metadata: Arc::new(metadata),
version,
expected_issuer: None,
scopes: Arc::new(scopes),
clock: Arc::new(clock),
}
}
#[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 metadata(&self) -> &ResourceMetadata {
&self.metadata
}
fn policy(&self) -> TokenPolicy<'_> {
let mut policy =
TokenPolicy::new(self.metadata.resource(), authorization_model(self.version));
if let Some(issuer) = &self.expected_issuer {
policy = policy.with_expected_issuer(Arc::clone(issuer));
}
policy
}
}
impl<A> Authenticator for OAuthAuthenticator<A>
where
A: TokenSource,
{
fn authenticate<'a>(
&'a self,
presented: &'a Credentials<'_>,
) -> Pin<Box<dyn Future<Output = AuthContext> + Send + 'a>> {
let token = match bearer(presented.authorization()) {
Presented::Bearer(token) => token,
Presented::Nothing => {
return Box::pin(std::future::ready(AuthContext::anonymous()));
}
};
let source = &self.source;
let scopes = Arc::clone(&self.scopes);
let policy = self.policy();
let now = self.clock.now();
Box::pin(async move {
match authenticate(source, &policy, scopes.as_ref(), token, now).await {
Ok(context) => context,
Err(_refusal) => AuthContext::anonymous(),
}
})
}
}
#[cfg(test)]
#[path = "oauth/tests.rs"]
mod tests;