1use std::convert::Infallible;
18use std::fmt;
19use std::future::Future;
20use std::pin::Pin;
21use std::sync::Arc;
22use std::task::{Context, Poll};
23use std::time::Duration;
24
25use bytes::Bytes;
26use http::header::{self, HeaderMap, HeaderValue};
27use http::{Method, Request, Response, StatusCode};
28use serde_json::{Map, Value, json};
29
30use super::http::HttpBody;
31use crate::auth::{AuthContext, ScopeImplies, TokenVerifier};
32
33const WELL_KNOWN: &str = "/.well-known/oauth-protected-resource";
35
36const MAX_TOKEN_BYTES: usize = 16 * 1024;
38
39const DEFAULT_VERIFY_TIMEOUT: Duration = Duration::from_secs(10);
41
42#[derive(Clone)]
46pub struct OAuth {
47 inner: Arc<Inner>,
48}
49
50struct Inner {
51 resource: String,
52 metadata_url: String,
53 metadata_paths: Vec<String>,
54 metadata_body: Bytes,
55 challenge_scope: Option<String>,
57 required_scopes: Vec<String>,
58 verifier: Box<dyn TokenVerifier>,
59 implies: Option<ScopeImplies>,
60 verify_timeout: Duration,
61}
62
63impl fmt::Debug for OAuth {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 f.debug_struct("OAuth")
66 .field("resource", &self.inner.resource)
67 .field("metadata_url", &self.inner.metadata_url)
68 .field("required_scopes", &self.inner.required_scopes)
69 .finish_non_exhaustive()
70 }
71}
72
73impl OAuth {
74 #[must_use]
79 pub fn builder(resource: impl Into<String>, verifier: impl TokenVerifier) -> OAuthBuilder {
80 OAuthBuilder {
81 resource: resource.into(),
82 verifier: Box::new(verifier),
83 authorization_servers: Vec::new(),
84 scopes_supported: Vec::new(),
85 required_scopes: Vec::new(),
86 implies: None,
87 resource_name: None,
88 verify_timeout: DEFAULT_VERIFY_TIMEOUT,
89 }
90 }
91
92 #[must_use]
94 pub fn resource(&self) -> &str {
95 &self.inner.resource
96 }
97
98 #[must_use]
101 pub fn metadata_url(&self) -> &str {
102 &self.inner.metadata_url
103 }
104
105 #[must_use]
108 pub fn metadata_paths(&self) -> &[String] {
109 &self.inner.metadata_paths
110 }
111
112 #[must_use]
116 pub fn metadata_service(&self) -> ProtectedResourceMetadata {
117 ProtectedResourceMetadata {
118 body: self.inner.metadata_body.clone(),
119 }
120 }
121
122 pub(crate) async fn authenticate(&self, headers: &HeaderMap) -> Result<AuthContext, Challenge> {
124 let mut values = headers.get_all(header::AUTHORIZATION).iter();
125 let Some(value) = values.next() else {
126 return Err(Challenge::Missing);
127 };
128 if values.next().is_some() {
129 return Err(Challenge::Malformed);
130 }
131 let Some((scheme, token)) = value.to_str().ok().and_then(|v| v.split_once(' ')) else {
132 return Err(Challenge::Missing);
133 };
134 if !scheme.eq_ignore_ascii_case("bearer") {
135 return Err(Challenge::Missing);
137 }
138 let token = token.trim_matches(' ');
139 if token.is_empty() || token.len() > MAX_TOKEN_BYTES || !is_b64token(token) {
140 return Err(Challenge::InvalidToken);
141 }
142 let verified =
143 tokio::time::timeout(self.inner.verify_timeout, self.inner.verifier.verify(token))
144 .await;
145 let principal = match verified {
146 Ok(Ok(principal)) => principal,
147 Ok(Err(error)) => {
148 tracing::debug!(reason = error.reason(), "rejected an access token");
149 return Err(Challenge::InvalidToken);
150 }
151 Err(_elapsed) => {
152 tracing::warn!(
153 "token verification took longer than {:?}\n = help: the verifier may be \
154 waiting on the authorization server; raise OAuthBuilder::verify_timeout(..) \
155 or check its availability",
156 self.inner.verify_timeout
157 );
158 return Err(Challenge::Unavailable);
159 }
160 };
161 let auth = AuthContext::new(principal, self.inner.implies);
162 let missing = auth.missing(&self.inner.required_scopes);
163 if !missing.is_empty() {
164 tracing::debug!(
165 ?missing,
166 "an access token lacks the server's required scopes"
167 );
168 return Err(Challenge::InsufficientScope(
169 self.inner.required_scopes.join(" "),
170 ));
171 }
172 Ok(auth)
173 }
174
175 pub(crate) fn www_authenticate(&self, challenge: &Challenge) -> Option<HeaderValue> {
177 let mut value = format!("Bearer resource_metadata=\"{}\"", self.inner.metadata_url);
178 match challenge {
179 Challenge::Missing | Challenge::Malformed | Challenge::Unavailable => {
180 if let Some(scope) = &self.inner.challenge_scope {
181 value.push_str(&format!(", scope=\"{scope}\""));
182 }
183 if matches!(challenge, Challenge::Malformed) {
184 value.push_str(", error=\"invalid_request\"");
185 }
186 }
187 Challenge::InvalidToken => {
188 if let Some(scope) = &self.inner.challenge_scope {
189 value.push_str(&format!(", scope=\"{scope}\""));
190 }
191 value.push_str(", error=\"invalid_token\"");
192 }
193 Challenge::InsufficientScope(scope) => {
194 value.push_str(&format!(
195 ", scope=\"{scope}\", error=\"insufficient_scope\""
196 ));
197 }
198 }
199 HeaderValue::from_str(&value).ok()
200 }
201}
202
203#[derive(Debug, Clone, PartialEq, Eq)]
205pub(crate) enum Challenge {
206 Missing,
208 Malformed,
210 InvalidToken,
212 InsufficientScope(String),
214 Unavailable,
216}
217
218impl Challenge {
219 pub(crate) fn status(&self) -> StatusCode {
220 match self {
221 Self::Missing | Self::InvalidToken => StatusCode::UNAUTHORIZED,
222 Self::Malformed => StatusCode::BAD_REQUEST,
223 Self::InsufficientScope(_) => StatusCode::FORBIDDEN,
224 Self::Unavailable => StatusCode::SERVICE_UNAVAILABLE,
225 }
226 }
227
228 pub(crate) fn message(&self) -> &'static str {
230 match self {
231 Self::Missing => "Authorization required",
232 Self::Malformed => "Malformed Authorization header",
233 Self::InvalidToken => "Invalid or expired access token",
234 Self::InsufficientScope(_) => "Insufficient scope",
235 Self::Unavailable => "Authorization is temporarily unavailable; retry later.",
236 }
237 }
238}
239
240pub struct OAuthBuilder {
242 resource: String,
243 verifier: Box<dyn TokenVerifier>,
244 authorization_servers: Vec<String>,
245 scopes_supported: Vec<String>,
246 required_scopes: Vec<String>,
247 implies: Option<ScopeImplies>,
248 resource_name: Option<String>,
249 verify_timeout: Duration,
250}
251
252impl fmt::Debug for OAuthBuilder {
253 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
254 f.debug_struct("OAuthBuilder")
255 .field("resource", &self.resource)
256 .field("authorization_servers", &self.authorization_servers)
257 .finish_non_exhaustive()
258 }
259}
260
261impl OAuthBuilder {
262 #[must_use]
265 pub fn authorization_server(mut self, issuer: impl Into<String>) -> Self {
266 self.authorization_servers.push(issuer.into());
267 self
268 }
269
270 #[must_use]
274 pub fn scopes_supported<I, S>(mut self, scopes: I) -> Self
275 where
276 I: IntoIterator<Item = S>,
277 S: Into<String>,
278 {
279 self.scopes_supported = scopes.into_iter().map(Into::into).collect();
280 self
281 }
282
283 #[must_use]
285 pub fn required_scopes<I, S>(mut self, scopes: I) -> Self
286 where
287 I: IntoIterator<Item = S>,
288 S: Into<String>,
289 {
290 self.required_scopes = scopes.into_iter().map(Into::into).collect();
291 self
292 }
293
294 #[must_use]
299 pub fn scope_implies(mut self, implies: ScopeImplies) -> Self {
300 self.implies = Some(implies);
301 self
302 }
303
304 #[must_use]
306 pub fn resource_name(mut self, name: impl Into<String>) -> Self {
307 self.resource_name = Some(name.into());
308 self
309 }
310
311 #[must_use]
314 pub fn verify_timeout(mut self, timeout: Duration) -> Self {
315 self.verify_timeout = timeout;
316 self
317 }
318
319 pub fn build(self) -> Result<OAuth, OAuthError> {
327 let resource =
328 canonical_url(&self.resource).map_err(|reason| OAuthError::InvalidResource {
329 resource: self.resource.clone(),
330 reason,
331 })?;
332 if self.authorization_servers.is_empty() {
333 return Err(OAuthError::NoAuthorizationServer);
334 }
335 for issuer in &self.authorization_servers {
336 canonical_url(issuer).map_err(|reason| OAuthError::InvalidAuthorizationServer {
337 issuer: issuer.clone(),
338 reason,
339 })?;
340 }
341 for scope in self.scopes_supported.iter().chain(&self.required_scopes) {
342 if !crate::auth::is_valid_scope(scope) {
343 return Err(OAuthError::InvalidScope(scope.clone()));
344 }
345 if scope == "offline_access" {
346 return Err(OAuthError::OfflineAccess);
347 }
348 }
349 if self.verify_timeout.is_zero() {
350 return Err(OAuthError::InvalidTimeout);
351 }
352
353 let (origin, path) = split_origin(&resource);
354 let path = path.trim_end_matches('/');
355 let metadata_url = format!("{origin}{WELL_KNOWN}{path}");
356 let mut metadata_paths = vec![format!("{WELL_KNOWN}{path}")];
357 if !path.is_empty() {
358 metadata_paths.push(WELL_KNOWN.to_owned());
359 }
360
361 let mut metadata = Map::new();
362 metadata.insert("resource".to_owned(), Value::from(resource.clone()));
363 metadata.insert(
364 "authorization_servers".to_owned(),
365 json!(self.authorization_servers),
366 );
367 if !self.scopes_supported.is_empty() {
368 metadata.insert("scopes_supported".to_owned(), json!(self.scopes_supported));
369 }
370 metadata.insert("bearer_methods_supported".to_owned(), json!(["header"]));
371 if let Some(name) = self.resource_name {
372 metadata.insert("resource_name".to_owned(), Value::from(name));
373 }
374 let metadata_body = serde_json::to_vec(&Value::Object(metadata))
375 .map_err(|_| OAuthError::InvalidResource {
376 resource: resource.clone(),
377 reason: "could not serialize the metadata".to_owned(),
378 })?
379 .into();
380
381 let challenge = if self.required_scopes.is_empty() {
382 &self.scopes_supported
383 } else {
384 &self.required_scopes
385 };
386 let challenge_scope = (!challenge.is_empty()).then(|| challenge.join(" "));
387
388 Ok(OAuth {
389 inner: Arc::new(Inner {
390 resource,
391 metadata_url,
392 metadata_paths,
393 metadata_body,
394 challenge_scope,
395 required_scopes: self.required_scopes,
396 verifier: self.verifier,
397 implies: self.implies,
398 verify_timeout: self.verify_timeout,
399 }),
400 })
401 }
402}
403
404#[derive(Clone, PartialEq, Eq, thiserror::Error)]
406#[non_exhaustive]
407pub enum OAuthError {
408 #[error("invalid resource `{resource}`: {reason}")]
410 InvalidResource {
411 resource: String,
413 reason: String,
415 },
416 #[error("invalid authorization server `{issuer}`: {reason}")]
418 InvalidAuthorizationServer {
419 issuer: String,
421 reason: String,
423 },
424 #[error("no authorization server")]
426 NoAuthorizationServer,
427 #[error("invalid scope `{0}`")]
429 InvalidScope(String),
430 #[error("`offline_access` is not a resource scope")]
432 OfflineAccess,
433 #[error("the token verification timeout must be positive")]
435 InvalidTimeout,
436}
437
438impl OAuthError {
439 #[must_use]
441 pub fn help(&self) -> &'static str {
442 match self {
443 Self::InvalidResource { .. } => {
444 "use the URL clients connect to, such as `https://mcp.example.com/mcp`: https \
445 (http only on localhost), no fragment, no user name"
446 }
447 Self::InvalidAuthorizationServer { .. } => {
448 "use the issuer URL of the authorization server, such as \
449 `https://auth.example.com` (https; http only on localhost)"
450 }
451 Self::NoAuthorizationServer => {
452 "add the issuer that grants tokens for this server with \
453 `.authorization_server(\"https://auth.example.com\")`"
454 }
455 Self::InvalidScope(_) => {
456 "scopes are non-empty and use visible ASCII except `\"` and `\\`, without spaces"
457 }
458 Self::OfflineAccess => {
459 "refresh tokens are the client's concern: remove `offline_access` (the \
460 specification says servers should not list it)"
461 }
462 Self::InvalidTimeout => "use a timeout such as `Duration::from_secs(10)`",
463 }
464 }
465}
466
467impl fmt::Debug for OAuthError {
468 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
469 write!(f, "{self}\n = help: {}", self.help())
470 }
471}
472
473#[derive(Clone, Debug)]
476pub struct ProtectedResourceMetadata {
477 body: Bytes,
478}
479
480impl ProtectedResourceMetadata {
481 pub(crate) fn respond(&self, method: &Method) -> Response<HttpBody> {
482 let mut response = match *method {
483 Method::GET | Method::HEAD => {
484 let mut response = Response::new(HttpBody::full(self.body.clone()));
485 let headers = response.headers_mut();
486 headers.insert(
487 header::CONTENT_TYPE,
488 HeaderValue::from_static("application/json"),
489 );
490 headers.insert(
491 header::CACHE_CONTROL,
492 HeaderValue::from_static("public, max-age=3600"),
493 );
494 response
495 }
496 Method::OPTIONS => {
497 let mut response = Response::new(HttpBody::full(Bytes::new()));
498 *response.status_mut() = StatusCode::NO_CONTENT;
499 response.headers_mut().insert(
500 header::ACCESS_CONTROL_ALLOW_METHODS,
501 HeaderValue::from_static("GET"),
502 );
503 response
504 }
505 _ => {
506 let mut response = Response::new(HttpBody::full(Bytes::new()));
507 *response.status_mut() = StatusCode::METHOD_NOT_ALLOWED;
508 response
509 .headers_mut()
510 .insert(header::ALLOW, HeaderValue::from_static("GET"));
511 response
512 }
513 };
514 response.headers_mut().insert(
515 header::ACCESS_CONTROL_ALLOW_ORIGIN,
516 HeaderValue::from_static("*"),
517 );
518 response
519 }
520}
521
522impl<B> tower_service::Service<Request<B>> for ProtectedResourceMetadata {
523 type Response = Response<HttpBody>;
524 type Error = Infallible;
525 type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Infallible>> + Send>>;
526
527 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
528 Poll::Ready(Ok(()))
529 }
530
531 fn call(&mut self, request: Request<B>) -> Self::Future {
532 let response = self.respond(request.method());
533 Box::pin(async move { Ok(response) })
534 }
535}
536
537fn is_b64token(token: &str) -> bool {
539 let body = token.trim_end_matches('=');
540 !body.is_empty()
541 && body.bytes().all(|b| {
542 b.is_ascii_alphanumeric() || matches!(b, b'-' | b'.' | b'_' | b'~' | b'+' | b'/')
543 })
544}
545
546pub(crate) fn canonical_url(url: &str) -> Result<String, String> {
549 let (scheme, rest) = url
550 .split_once("://")
551 .ok_or_else(|| "missing scheme (expected `https://`)".to_owned())?;
552 let scheme = scheme.to_ascii_lowercase();
553 if url.contains('#') {
554 return Err("must not contain a fragment".to_owned());
555 }
556 if url
557 .chars()
558 .any(|c| c.is_whitespace() || c.is_control() || c == '"' || c == '\\')
559 {
560 return Err("contains spaces, quotes or control characters".to_owned());
561 }
562 let end = rest.find(['/', '?']).unwrap_or(rest.len());
563 let (authority, tail) = rest.split_at(end);
564 if authority.is_empty() {
565 return Err("missing host".to_owned());
566 }
567 if authority.contains('@') {
568 return Err("must not contain a user name".to_owned());
569 }
570 let authority = authority.to_ascii_lowercase();
571 let host = super::http::strip_port(&authority);
572 match scheme.as_str() {
573 "https" => {}
574 "http" if super::http::is_localhost(host) => {}
575 "http" => return Err("must use https (http is only allowed on localhost)".to_owned()),
576 _ => return Err("must use https".to_owned()),
577 }
578 Ok(format!("{scheme}://{authority}{tail}"))
579}
580
581fn split_origin(url: &str) -> (&str, &str) {
583 let after_scheme = url.find("://").map_or(0, |i| i + 3);
584 let path_start = url
585 .get(after_scheme..)
586 .and_then(|rest| rest.find(['/', '?']))
587 .map_or(url.len(), |i| i + after_scheme);
588 let (origin, rest) = url.split_at(path_start);
589 let path = rest.split('?').next().unwrap_or_default();
590 (origin, path)
591}
592
593#[cfg(test)]
594mod tests {
595 use super::*;
596 use crate::auth::{Principal, TokenError};
597
598 fn verifier(token: &str) -> Result<Principal, TokenError> {
599 match token {
600 "good" => Ok(Principal::new("alice").with_scopes(["mcp"])),
601 "bare" => Ok(Principal::new("bob")),
602 _ => Err(TokenError::new("unknown token")),
603 }
604 }
605
606 fn oauth() -> OAuth {
607 OAuth::builder("https://MCP.example.com/mcp", verifier)
608 .authorization_server("https://auth.example.com")
609 .scopes_supported(["mcp"])
610 .required_scopes(["mcp"])
611 .build()
612 .unwrap()
613 }
614
615 fn headers(auth: &[&str]) -> HeaderMap {
616 let mut headers = HeaderMap::new();
617 for value in auth {
618 headers.append(header::AUTHORIZATION, HeaderValue::from_str(value).unwrap());
619 }
620 headers
621 }
622
623 #[test]
624 fn metadata_locations() {
625 let oauth = oauth();
626 assert_eq!(oauth.resource(), "https://mcp.example.com/mcp");
627 assert_eq!(
628 oauth.metadata_url(),
629 "https://mcp.example.com/.well-known/oauth-protected-resource/mcp"
630 );
631 assert_eq!(
632 oauth.metadata_paths(),
633 [
634 "/.well-known/oauth-protected-resource/mcp",
635 "/.well-known/oauth-protected-resource"
636 ]
637 );
638 let root = OAuth::builder("https://mcp.example.com", verifier)
639 .authorization_server("https://auth.example.com")
640 .build()
641 .unwrap();
642 assert_eq!(
643 root.metadata_paths(),
644 ["/.well-known/oauth-protected-resource"]
645 );
646 let body: Value = serde_json::from_slice(&oauth.inner.metadata_body).unwrap();
647 assert_eq!(
648 body,
649 json!({
650 "resource": "https://mcp.example.com/mcp",
651 "authorization_servers": ["https://auth.example.com"],
652 "scopes_supported": ["mcp"],
653 "bearer_methods_supported": ["header"],
654 })
655 );
656 }
657
658 #[test]
659 fn invalid_settings_are_rejected() {
660 let build = |resource: &str| {
661 OAuth::builder(resource, verifier)
662 .authorization_server("https://auth.example.com")
663 .build()
664 };
665 assert!(build("http://mcp.example.com/mcp").is_err());
666 assert!(build("mcp.example.com").is_err());
667 assert!(build("https://mcp.example.com/#x").is_err());
668 assert!(build("https://user@mcp.example.com").is_err());
669 assert!(build("http://localhost:8000/mcp").is_ok());
670 assert_eq!(
671 OAuth::builder("https://a.example", verifier)
672 .build()
673 .unwrap_err(),
674 OAuthError::NoAuthorizationServer
675 );
676 let bad_scope = OAuth::builder("https://a.example", verifier)
677 .authorization_server("https://auth.example.com")
678 .scopes_supported(["a b"])
679 .build();
680 assert!(matches!(bad_scope, Err(OAuthError::InvalidScope(_))));
681 let offline = OAuth::builder("https://a.example", verifier)
682 .authorization_server("https://auth.example.com")
683 .scopes_supported(["offline_access"])
684 .build();
685 assert_eq!(offline.unwrap_err(), OAuthError::OfflineAccess);
686 }
687
688 #[tokio::test]
689 async fn challenges() {
690 let oauth = oauth();
691 assert!(oauth.authenticate(&headers(&["Bearer good"])).await.is_ok());
692 assert!(oauth.authenticate(&headers(&["bearer good"])).await.is_ok());
693 assert_eq!(
694 oauth.authenticate(&headers(&[])).await.unwrap_err(),
695 Challenge::Missing
696 );
697 assert_eq!(
698 oauth
699 .authenticate(&headers(&["Basic Zm9vOmJhcg=="]))
700 .await
701 .unwrap_err(),
702 Challenge::Missing
703 );
704 assert_eq!(
705 oauth
706 .authenticate(&headers(&["Bearer good", "Bearer good"]))
707 .await
708 .unwrap_err(),
709 Challenge::Malformed
710 );
711 for bad in ["Bearer nope", "Bearer ", "Bearer a\"b", "Bearer =="] {
712 assert_eq!(
713 oauth.authenticate(&headers(&[bad])).await.unwrap_err(),
714 Challenge::InvalidToken,
715 "{bad}"
716 );
717 }
718 assert_eq!(
719 oauth
720 .authenticate(&headers(&["Bearer bare"]))
721 .await
722 .unwrap_err(),
723 Challenge::InsufficientScope("mcp".to_owned())
724 );
725
726 let value = |c: &Challenge| {
727 oauth
728 .www_authenticate(c)
729 .unwrap()
730 .to_str()
731 .unwrap()
732 .to_owned()
733 };
734 assert_eq!(
735 value(&Challenge::Missing),
736 "Bearer resource_metadata=\"https://mcp.example.com/.well-known/oauth-protected-resource/mcp\", scope=\"mcp\""
737 );
738 assert!(value(&Challenge::InvalidToken).ends_with("error=\"invalid_token\""));
739 assert!(
740 value(&Challenge::InsufficientScope("a b".to_owned()))
741 .ends_with("scope=\"a b\", error=\"insufficient_scope\"")
742 );
743 }
744
745 #[tokio::test]
746 async fn slow_verifiers_time_out() {
747 struct Slow;
748 impl TokenVerifier for Slow {
749 fn verify<'a>(&'a self, _: &'a str) -> crate::auth::VerifyFuture<'a> {
750 Box::pin(async {
751 tokio::time::sleep(Duration::from_secs(60)).await;
752 Err(TokenError::new("late"))
753 })
754 }
755 }
756 let oauth = OAuth::builder("https://a.example", Slow)
757 .authorization_server("https://auth.example.com")
758 .verify_timeout(Duration::from_millis(10))
759 .build()
760 .unwrap();
761 assert_eq!(
762 oauth
763 .authenticate(&headers(&["Bearer t"]))
764 .await
765 .unwrap_err(),
766 Challenge::Unavailable
767 );
768 }
769
770 #[test]
771 fn metadata_service_answers_get_only() {
772 let service = oauth().metadata_service();
773 let get = service.respond(&Method::GET);
774 assert_eq!(get.status(), StatusCode::OK);
775 assert_eq!(get.headers()[header::ACCESS_CONTROL_ALLOW_ORIGIN], "*");
776 assert_eq!(
777 service.respond(&Method::POST).status(),
778 StatusCode::METHOD_NOT_ALLOWED
779 );
780 }
781}