1#[cfg(feature = "oauth")]
12mod jwt;
13
14#[cfg(feature = "oauth")]
15pub use jwt::{Algorithm, JwtError, JwtVerifier, JwtVerifierBuilder};
16
17use std::collections::BTreeSet;
18use std::fmt;
19use std::future::Future;
20use std::pin::Pin;
21use std::sync::Arc;
22
23use serde_json::{Map, Value};
24use sha2::{Digest as _, Sha256};
25
26use crate::context::Ctx;
27use crate::error::McpError;
28
29#[derive(Debug, Clone, PartialEq)]
31pub struct Principal {
32 subject: String,
33 issuer: Option<String>,
34 scopes: BTreeSet<String>,
35 claims: Map<String, Value>,
36}
37
38impl Principal {
39 #[must_use]
41 pub fn new(subject: impl Into<String>) -> Self {
42 Self {
43 subject: subject.into(),
44 issuer: None,
45 scopes: BTreeSet::new(),
46 claims: Map::new(),
47 }
48 }
49
50 #[must_use]
52 pub fn with_issuer(mut self, issuer: impl Into<String>) -> Self {
53 self.issuer = Some(issuer.into());
54 self
55 }
56
57 #[must_use]
59 pub fn with_scopes<I, S>(mut self, scopes: I) -> Self
60 where
61 I: IntoIterator<Item = S>,
62 S: Into<String>,
63 {
64 self.scopes = scopes.into_iter().map(Into::into).collect();
65 self
66 }
67
68 #[must_use]
70 pub fn with_claims(mut self, claims: Map<String, Value>) -> Self {
71 self.claims = claims;
72 self
73 }
74
75 #[must_use]
77 pub fn subject(&self) -> &str {
78 &self.subject
79 }
80
81 #[must_use]
83 pub fn issuer(&self) -> Option<&str> {
84 self.issuer.as_deref()
85 }
86
87 #[must_use]
89 pub fn scopes(&self) -> &BTreeSet<String> {
90 &self.scopes
91 }
92
93 #[must_use]
97 pub fn has_scope(&self, scope: &str) -> bool {
98 self.scopes.contains(scope)
99 }
100
101 #[must_use]
103 pub fn claims(&self) -> &Map<String, Value> {
104 &self.claims
105 }
106
107 #[cfg_attr(not(feature = "http"), allow(dead_code))]
110 pub(crate) fn digest(&self) -> String {
111 let mut hasher = Sha256::new();
112 hasher.update(self.issuer.as_deref().unwrap_or_default());
113 hasher.update([0]);
114 hasher.update(&self.subject);
115 let digest = hasher.finalize();
116 digest.iter().take(16).map(|b| format!("{b:02x}")).collect()
117 }
118}
119
120#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
123#[error("invalid access token: {reason}")]
124pub struct TokenError {
125 reason: String,
126}
127
128impl TokenError {
129 pub fn new(reason: impl Into<String>) -> Self {
131 Self {
132 reason: reason.into(),
133 }
134 }
135
136 #[must_use]
138 pub fn reason(&self) -> &str {
139 &self.reason
140 }
141}
142
143pub type VerifyFuture<'a> =
145 Pin<Box<dyn Future<Output = Result<Principal, TokenError>> + Send + 'a>>;
146
147pub trait TokenVerifier: Send + Sync + 'static {
155 fn verify<'a>(&'a self, token: &'a str) -> VerifyFuture<'a>;
157}
158
159impl<F> TokenVerifier for F
160where
161 F: Fn(&str) -> Result<Principal, TokenError> + Send + Sync + 'static,
162{
163 fn verify<'a>(&'a self, token: &'a str) -> VerifyFuture<'a> {
164 let result = self(token);
165 Box::pin(async move { result })
166 }
167}
168
169pub(crate) fn is_valid_scope(scope: &str) -> bool {
172 !scope.is_empty()
173 && scope
174 .bytes()
175 .all(|b| matches!(b, 0x21 | 0x23..=0x5B | 0x5D..=0x7E))
176}
177
178pub type ScopeImplies = fn(granted: &str, required: &str) -> bool;
181
182#[derive(Clone)]
185pub(crate) struct AuthContext {
186 pub(crate) principal: Arc<Principal>,
187 pub(crate) implies: Option<ScopeImplies>,
188 pub(crate) digest: String,
190}
191
192impl fmt::Debug for AuthContext {
193 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194 f.debug_struct("AuthContext")
195 .field("subject", &self.principal.subject())
196 .finish_non_exhaustive()
197 }
198}
199
200impl AuthContext {
201 #[cfg_attr(not(feature = "http"), allow(dead_code))]
202 pub(crate) fn new(principal: Principal, implies: Option<ScopeImplies>) -> Self {
203 let digest = principal.digest();
204 Self {
205 principal: Arc::new(principal),
206 implies,
207 digest,
208 }
209 }
210
211 pub(crate) fn missing<'a>(&self, required: &'a [String]) -> Vec<&'a str> {
213 required
214 .iter()
215 .filter(|needed| {
216 !self.principal.scopes.iter().any(|granted| {
217 granted == *needed || self.implies.is_some_and(|f| f(granted, needed))
218 })
219 })
220 .map(String::as_str)
221 .collect()
222 }
223
224 pub(crate) fn check(&self, required: &[String]) -> Result<(), McpError> {
226 if self.missing(required).is_empty() {
227 Ok(())
228 } else {
229 Err(McpError::insufficient_scope(required))
230 }
231 }
232}
233
234impl Ctx {
235 #[must_use]
238 pub fn principal(&self) -> Option<Arc<Principal>> {
239 self.transport()
240 .get::<AuthContext>()
241 .map(|auth| Arc::clone(&auth.principal))
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248
249 fn auth(scopes: &[&str], implies: Option<ScopeImplies>) -> AuthContext {
250 AuthContext::new(
251 Principal::new("u").with_scopes(scopes.iter().copied()),
252 implies,
253 )
254 }
255
256 #[test]
257 fn missing_scopes_are_listed_together() {
258 let required = vec!["a".to_owned(), "b".to_owned(), "c".to_owned()];
259 assert_eq!(auth(&["b"], None).missing(&required), ["a", "c"]);
260 assert!(auth(&["a", "b", "c"], None).check(&required).is_ok());
261 }
262
263 #[test]
264 fn scope_hierarchies() {
265 fn implies(granted: &str, required: &str) -> bool {
266 required.starts_with(&format!("{granted}:"))
267 }
268 let required = vec!["files:read".to_owned()];
269 assert!(auth(&["files"], Some(implies)).check(&required).is_ok());
270 assert!(auth(&["files"], None).check(&required).is_err());
271 }
272
273 #[test]
274 fn digests_differ_by_issuer_and_subject() {
275 let a = Principal::new("alice").with_issuer("https://a");
276 let b = Principal::new("alice").with_issuer("https://b");
277 assert_ne!(a.digest(), b.digest());
278 assert_eq!(a.digest(), a.clone().digest());
279 assert_eq!(a.digest().len(), 32);
280 }
281}