#[cfg(feature = "oauth")]
mod jwt;
#[cfg(feature = "oauth")]
pub use jwt::{Algorithm, JwtError, JwtVerifier, JwtVerifierBuilder};
use std::collections::BTreeSet;
use std::fmt;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use serde_json::{Map, Value};
use sha2::{Digest as _, Sha256};
use crate::context::Ctx;
use crate::error::McpError;
#[derive(Debug, Clone, PartialEq)]
pub struct Principal {
subject: String,
issuer: Option<String>,
scopes: BTreeSet<String>,
claims: Map<String, Value>,
}
impl Principal {
#[must_use]
pub fn new(subject: impl Into<String>) -> Self {
Self {
subject: subject.into(),
issuer: None,
scopes: BTreeSet::new(),
claims: Map::new(),
}
}
#[must_use]
pub fn with_issuer(mut self, issuer: impl Into<String>) -> Self {
self.issuer = Some(issuer.into());
self
}
#[must_use]
pub fn with_scopes<I, S>(mut self, scopes: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.scopes = scopes.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub fn with_claims(mut self, claims: Map<String, Value>) -> Self {
self.claims = claims;
self
}
#[must_use]
pub fn subject(&self) -> &str {
&self.subject
}
#[must_use]
pub fn issuer(&self) -> Option<&str> {
self.issuer.as_deref()
}
#[must_use]
pub fn scopes(&self) -> &BTreeSet<String> {
&self.scopes
}
#[must_use]
pub fn has_scope(&self, scope: &str) -> bool {
self.scopes.contains(scope)
}
#[must_use]
pub fn claims(&self) -> &Map<String, Value> {
&self.claims
}
#[cfg_attr(not(feature = "http"), allow(dead_code))]
pub(crate) fn digest(&self) -> String {
let mut hasher = Sha256::new();
hasher.update(self.issuer.as_deref().unwrap_or_default());
hasher.update([0]);
hasher.update(&self.subject);
let digest = hasher.finalize();
digest.iter().take(16).map(|b| format!("{b:02x}")).collect()
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("invalid access token: {reason}")]
pub struct TokenError {
reason: String,
}
impl TokenError {
pub fn new(reason: impl Into<String>) -> Self {
Self {
reason: reason.into(),
}
}
#[must_use]
pub fn reason(&self) -> &str {
&self.reason
}
}
pub type VerifyFuture<'a> =
Pin<Box<dyn Future<Output = Result<Principal, TokenError>> + Send + 'a>>;
pub trait TokenVerifier: Send + Sync + 'static {
fn verify<'a>(&'a self, token: &'a str) -> VerifyFuture<'a>;
}
impl<F> TokenVerifier for F
where
F: Fn(&str) -> Result<Principal, TokenError> + Send + Sync + 'static,
{
fn verify<'a>(&'a self, token: &'a str) -> VerifyFuture<'a> {
let result = self(token);
Box::pin(async move { result })
}
}
pub(crate) fn is_valid_scope(scope: &str) -> bool {
!scope.is_empty()
&& scope
.bytes()
.all(|b| matches!(b, 0x21 | 0x23..=0x5B | 0x5D..=0x7E))
}
pub type ScopeImplies = fn(granted: &str, required: &str) -> bool;
#[derive(Clone)]
pub(crate) struct AuthContext {
pub(crate) principal: Arc<Principal>,
pub(crate) implies: Option<ScopeImplies>,
pub(crate) digest: String,
}
impl fmt::Debug for AuthContext {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AuthContext")
.field("subject", &self.principal.subject())
.finish_non_exhaustive()
}
}
impl AuthContext {
#[cfg_attr(not(feature = "http"), allow(dead_code))]
pub(crate) fn new(principal: Principal, implies: Option<ScopeImplies>) -> Self {
let digest = principal.digest();
Self {
principal: Arc::new(principal),
implies,
digest,
}
}
pub(crate) fn missing<'a>(&self, required: &'a [String]) -> Vec<&'a str> {
required
.iter()
.filter(|needed| {
!self.principal.scopes.iter().any(|granted| {
granted == *needed || self.implies.is_some_and(|f| f(granted, needed))
})
})
.map(String::as_str)
.collect()
}
pub(crate) fn check(&self, required: &[String]) -> Result<(), McpError> {
if self.missing(required).is_empty() {
Ok(())
} else {
Err(McpError::insufficient_scope(required))
}
}
}
impl Ctx {
#[must_use]
pub fn principal(&self) -> Option<Arc<Principal>> {
self.transport()
.get::<AuthContext>()
.map(|auth| Arc::clone(&auth.principal))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn auth(scopes: &[&str], implies: Option<ScopeImplies>) -> AuthContext {
AuthContext::new(
Principal::new("u").with_scopes(scopes.iter().copied()),
implies,
)
}
#[test]
fn missing_scopes_are_listed_together() {
let required = vec!["a".to_owned(), "b".to_owned(), "c".to_owned()];
assert_eq!(auth(&["b"], None).missing(&required), ["a", "c"]);
assert!(auth(&["a", "b", "c"], None).check(&required).is_ok());
}
#[test]
fn scope_hierarchies() {
fn implies(granted: &str, required: &str) -> bool {
required.starts_with(&format!("{granted}:"))
}
let required = vec!["files:read".to_owned()];
assert!(auth(&["files"], Some(implies)).check(&required).is_ok());
assert!(auth(&["files"], None).check(&required).is_err());
}
#[test]
fn digests_differ_by_issuer_and_subject() {
let a = Principal::new("alice").with_issuer("https://a");
let b = Principal::new("alice").with_issuer("https://b");
assert_ne!(a.digest(), b.digest());
assert_eq!(a.digest(), a.clone().digest());
assert_eq!(a.digest().len(), 32);
}
}