Skip to main content

flymcprust_core/transport/
oauth.rs

1//! OAuth 2.1 resource server for the Streamable HTTP transport.
2//!
3//! With [`OAuth`] in [`HttpConfig`](super::http::HttpConfig::oauth), every
4//! request needs `Authorization: Bearer <token>`:
5//!
6//! - no token: `401` with `WWW-Authenticate: Bearer resource_metadata="…"`
7//!   (plus the scopes to ask for), which starts the client's OAuth flow;
8//! - an invalid or expired token: `401` with `error="invalid_token"`;
9//! - a valid token without the scopes the request needs: `403` with
10//!   `error="insufficient_scope"` and every scope the request needs.
11//!
12//! The Protected Resource Metadata document (RFC 9728) tells clients which
13//! authorization server issues tokens for this server. [`serve`](super::http::serve)
14//! publishes it at the well-known URIs; other frameworks mount
15//! [`OAuth::metadata_service`] at [`OAuth::metadata_paths`].
16
17use 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
33/// The well-known URI suffix of RFC 9728.
34const WELL_KNOWN: &str = "/.well-known/oauth-protected-resource";
35
36/// Longest bearer token accepted (larger ones are rejected unverified).
37const MAX_TOKEN_BYTES: usize = 16 * 1024;
38
39/// Default time allowed to verify a token.
40const DEFAULT_VERIFY_TIMEOUT: Duration = Duration::from_secs(10);
41
42/// OAuth 2.1 resource-server settings. Build with [`OAuth::builder`].
43///
44/// Cheap to clone.
45#[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    /// `scope` of the `401` challenge.
56    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    /// Starts the settings of the MCP server identified by `resource`, its
75    /// canonical URI (for example `https://mcp.example.com/mcp`, the URL
76    /// clients connect to). Tokens are checked by `verifier`, which must
77    /// only accept tokens issued for `resource` (audience).
78    #[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    /// The canonical URI of the server.
93    #[must_use]
94    pub fn resource(&self) -> &str {
95        &self.inner.resource
96    }
97
98    /// The URL of the Protected Resource Metadata document, sent in
99    /// `WWW-Authenticate` challenges.
100    #[must_use]
101    pub fn metadata_url(&self) -> &str {
102        &self.inner.metadata_url
103    }
104
105    /// The paths at which clients look for the metadata document: the
106    /// path-specific well-known URI first, then the root one.
107    #[must_use]
108    pub fn metadata_paths(&self) -> &[String] {
109        &self.inner.metadata_paths
110    }
111
112    /// A `tower::Service` that answers with the metadata document. Mount it
113    /// at each of [`metadata_paths`](Self::metadata_paths) when not using
114    /// [`serve`](super::http::serve).
115    #[must_use]
116    pub fn metadata_service(&self) -> ProtectedResourceMetadata {
117        ProtectedResourceMetadata {
118            body: self.inner.metadata_body.clone(),
119        }
120    }
121
122    /// Authenticates a request from its headers.
123    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            // Another scheme: tell the client which one we expect.
136            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    /// The `WWW-Authenticate` value for a challenge.
176    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/// Why a request was not authorized.
204#[derive(Debug, Clone, PartialEq, Eq)]
205pub(crate) enum Challenge {
206    /// No bearer token (`401`).
207    Missing,
208    /// Several `Authorization` headers (`400`).
209    Malformed,
210    /// The token failed verification (`401`).
211    InvalidToken,
212    /// The token lacks these space-separated scopes (`403`).
213    InsufficientScope(String),
214    /// The verifier did not answer in time (`503`).
215    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    /// The client-facing message of the JSON-RPC error body.
229    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
240/// Builder of [`OAuth`].
241pub 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    /// Adds an authorization server (its issuer URL) that issues tokens for
263    /// this server. At least one is required.
264    #[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    /// The scopes listed in the metadata document (`scopes_supported`): the
271    /// minimal set for basic use. Handler scopes are requested later, when
272    /// a `403` asks for them.
273    #[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    /// Scopes every request needs; tokens without them get `403`.
284    #[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    /// Declares a scope hierarchy: `implies(granted, required)` returns
295    /// whether a granted scope satisfies a required one (for example,
296    /// `files` implying `files:read`). The specification requires servers
297    /// to honor their hierarchies.
298    #[must_use]
299    pub fn scope_implies(mut self, implies: ScopeImplies) -> Self {
300        self.implies = Some(implies);
301        self
302    }
303
304    /// A human-readable name shown by clients (`resource_name`).
305    #[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    /// Time allowed to verify a token (including fetching signing keys).
312    /// Slower verifications get `503`. Default: 10 s.
313    #[must_use]
314    pub fn verify_timeout(mut self, timeout: Duration) -> Self {
315        self.verify_timeout = timeout;
316        self
317    }
318
319    /// Validates the settings.
320    ///
321    /// # Errors
322    ///
323    /// Fails when the resource or an authorization server is not a valid
324    /// URL (`https`, or `http` on localhost), when no authorization server
325    /// is given, or when a scope is malformed.
326    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/// Invalid [`OAuth`] settings.
405#[derive(Clone, PartialEq, Eq, thiserror::Error)]
406#[non_exhaustive]
407pub enum OAuthError {
408    /// The resource is not a valid canonical URI.
409    #[error("invalid resource `{resource}`: {reason}")]
410    InvalidResource {
411        /// The configured value.
412        resource: String,
413        /// What is wrong with it.
414        reason: String,
415    },
416    /// An authorization server URL is invalid.
417    #[error("invalid authorization server `{issuer}`: {reason}")]
418    InvalidAuthorizationServer {
419        /// The configured value.
420        issuer: String,
421        /// What is wrong with it.
422        reason: String,
423    },
424    /// No authorization server was given.
425    #[error("no authorization server")]
426    NoAuthorizationServer,
427    /// A scope contains characters OAuth does not allow.
428    #[error("invalid scope `{0}`")]
429    InvalidScope(String),
430    /// `offline_access` was listed.
431    #[error("`offline_access` is not a resource scope")]
432    OfflineAccess,
433    /// The verification timeout is zero.
434    #[error("the token verification timeout must be positive")]
435    InvalidTimeout,
436}
437
438impl OAuthError {
439    /// How to fix the error.
440    #[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/// Serves the Protected Resource Metadata document (RFC 9728) on `GET`.
474/// The document is public, so any origin may read it.
475#[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
537/// RFC 6750 `b64token`: the characters a bearer token may contain.
538fn 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
546/// Validates an absolute `https` URL (or `http` on localhost) and lowercases
547/// its scheme and host, as RFC 8707 canonical URIs are.
548pub(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
581/// Splits a canonical URL into origin and path (without the query).
582fn 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}