Skip to main content

tower_mcp/client/
oauth_flow.rs

1//! Cohesive OAuth authorization-code state machine for MCP clients.
2//!
3//! [`OAuthAuthorizationFlow`] composes the lower-level discovery,
4//! registration, PKCE, redirect, token, refresh, persistence, and scope
5//! escalation pieces into one reusable client.
6
7use std::collections::HashMap;
8use std::fmt;
9use std::sync::Arc;
10use std::time::{Duration, SystemTime, UNIX_EPOCH};
11
12use async_trait::async_trait;
13use base64::Engine;
14use tokio::sync::{Mutex, RwLock, oneshot};
15
16use super::oauth::{
17    OAuthBearerChallenge, OAuthClientError, OAuthScopeEscalationHandler,
18    OAuthScopeEscalationRequest, OAuthTokenEndpointAuthMethod, TokenProvider,
19};
20use super::oauth_authcode::{
21    OAuthAuthorizationServerMetadata, OAuthClientRegistration, OAuthClientRegistrationMethod,
22    OAuthClientRegistrationOptions, OAuthClientRegistrationStore, OAuthProtectedResourceMetadata,
23};
24
25/// HTTP method used by an OAuth protocol request.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27#[non_exhaustive]
28pub enum OAuthHttpMethod {
29    /// HTTP GET.
30    Get,
31    /// HTTP POST.
32    Post,
33}
34
35/// Body of an OAuth protocol HTTP request.
36#[derive(Debug, Clone)]
37#[non_exhaustive]
38pub enum OAuthHttpBody {
39    /// No request body.
40    Empty,
41    /// `application/x-www-form-urlencoded` fields.
42    Form(Vec<(String, String)>),
43    /// JSON request body.
44    Json(serde_json::Value),
45}
46
47/// Transport-neutral OAuth protocol HTTP request.
48#[derive(Debug, Clone)]
49pub struct OAuthHttpRequest {
50    /// HTTP method.
51    pub method: OAuthHttpMethod,
52    /// Absolute request URL.
53    pub url: String,
54    /// Request headers.
55    pub headers: Vec<(String, String)>,
56    /// Request body.
57    pub body: OAuthHttpBody,
58}
59
60impl OAuthHttpRequest {
61    fn get(url: impl Into<String>) -> Self {
62        Self {
63            method: OAuthHttpMethod::Get,
64            url: url.into(),
65            headers: Vec::new(),
66            body: OAuthHttpBody::Empty,
67        }
68    }
69
70    fn post_form(url: impl Into<String>, fields: Vec<(String, String)>) -> Self {
71        Self {
72            method: OAuthHttpMethod::Post,
73            url: url.into(),
74            headers: Vec::new(),
75            body: OAuthHttpBody::Form(fields),
76        }
77    }
78
79    fn post_json(url: impl Into<String>, value: serde_json::Value) -> Self {
80        Self {
81            method: OAuthHttpMethod::Post,
82            url: url.into(),
83            headers: Vec::new(),
84            body: OAuthHttpBody::Json(value),
85        }
86    }
87
88    fn basic_auth(mut self, client_id: &str, client_secret: &str) -> Self {
89        // RFC 6749 section 2.3.1 applies application/x-www-form-urlencoded
90        // encoding to both credentials before constructing HTTP Basic auth.
91        let client_id = urlencoding::encode(client_id);
92        let client_secret = urlencoding::encode(client_secret);
93        let encoded = base64::engine::general_purpose::STANDARD
94            .encode(format!("{client_id}:{client_secret}"));
95        self.headers
96            .push(("authorization".to_string(), format!("Basic {encoded}")));
97        self
98    }
99}
100
101/// Transport-neutral OAuth protocol HTTP response.
102#[derive(Debug, Clone)]
103pub struct OAuthHttpResponse {
104    /// Numeric HTTP status code.
105    pub status: u16,
106    /// Response headers. Repeated fields are retained as separate entries.
107    pub headers: Vec<(String, String)>,
108    /// Complete response body.
109    pub body: Vec<u8>,
110}
111
112impl OAuthHttpResponse {
113    /// Return whether the response status is in the 2xx range.
114    pub fn is_success(&self) -> bool {
115        (200..300).contains(&self.status)
116    }
117
118    /// Iterate over values for a case-insensitive header name.
119    pub fn header_values<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a str> + 'a {
120        self.headers
121            .iter()
122            .filter(move |(candidate, _)| candidate.eq_ignore_ascii_case(name))
123            .map(|(_, value)| value.as_str())
124    }
125
126    fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T, OAuthClientError> {
127        serde_json::from_slice(&self.body)
128            .map_err(|error| OAuthClientError::InvalidResponse(error.to_string()))
129    }
130
131    fn error_body(&self) -> String {
132        String::from_utf8_lossy(&self.body)
133            .chars()
134            .take(1024)
135            .collect()
136    }
137}
138
139/// Application-supplied HTTP abstraction for OAuth protocol traffic.
140///
141/// Implementations control proxying, TLS, telemetry, retries, and test
142/// behavior without requiring the authorization state machine to depend on a
143/// particular HTTP stack.
144#[async_trait]
145pub trait OAuthHttpClient: Send + Sync + 'static {
146    /// Execute one OAuth protocol request.
147    async fn execute(
148        &self,
149        request: OAuthHttpRequest,
150    ) -> Result<OAuthHttpResponse, OAuthClientError>;
151}
152
153/// [`OAuthHttpClient`] backed by reqwest.
154#[derive(Clone)]
155pub struct ReqwestOAuthHttpClient {
156    client: reqwest::Client,
157}
158
159impl fmt::Debug for ReqwestOAuthHttpClient {
160    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
161        formatter
162            .debug_struct("ReqwestOAuthHttpClient")
163            .finish_non_exhaustive()
164    }
165}
166
167impl ReqwestOAuthHttpClient {
168    /// Wrap an application-configured reqwest client.
169    pub fn new(client: reqwest::Client) -> Self {
170        Self { client }
171    }
172
173    /// Create a client that does not automatically follow redirects.
174    pub fn without_redirects() -> Result<Self, OAuthClientError> {
175        reqwest::Client::builder()
176            .redirect(reqwest::redirect::Policy::none())
177            .build()
178            .map(Self::new)
179            .map_err(|error| OAuthClientError::Http(error.to_string()))
180    }
181}
182
183#[async_trait]
184impl OAuthHttpClient for ReqwestOAuthHttpClient {
185    async fn execute(
186        &self,
187        request: OAuthHttpRequest,
188    ) -> Result<OAuthHttpResponse, OAuthClientError> {
189        let method = match request.method {
190            OAuthHttpMethod::Get => reqwest::Method::GET,
191            OAuthHttpMethod::Post => reqwest::Method::POST,
192        };
193        let mut builder = self.client.request(method, &request.url);
194        for (name, value) in request.headers {
195            builder = builder.header(name, value);
196        }
197        builder = match request.body {
198            OAuthHttpBody::Empty => builder,
199            OAuthHttpBody::Form(fields) => builder.form(&fields),
200            OAuthHttpBody::Json(value) => builder.json(&value),
201        };
202        let response = builder
203            .send()
204            .await
205            .map_err(|error| OAuthClientError::Http(error.to_string()))?;
206        let status = response.status().as_u16();
207        let headers = response
208            .headers()
209            .iter()
210            .filter_map(|(name, value)| {
211                value
212                    .to_str()
213                    .ok()
214                    .map(|value| (name.as_str().to_string(), value.to_string()))
215            })
216            .collect();
217        let body = response
218            .bytes()
219            .await
220            .map_err(|error| OAuthClientError::Http(error.to_string()))?
221            .to_vec();
222        Ok(OAuthHttpResponse {
223            status,
224            headers,
225            body,
226        })
227    }
228}
229
230/// Binding key that prevents token reuse across resources, issuers, or clients.
231#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
232pub struct OAuthTokenBinding {
233    /// Canonical MCP protected-resource identifier.
234    pub resource: String,
235    /// Exact authorization-server issuer.
236    pub issuer: String,
237    /// OAuth client identifier.
238    pub client_id: String,
239}
240
241/// Persistable OAuth authorization-code token set.
242///
243/// This value contains bearer credentials and must be protected as a secret.
244#[derive(Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
245pub struct OAuthStoredToken {
246    /// Access token.
247    pub access_token: String,
248    /// Refresh token, when issued.
249    pub refresh_token: Option<String>,
250    /// Expiration time as Unix seconds.
251    ///
252    /// This is [`u64::MAX`] when the authorization server did not advertise
253    /// an `expires_in` lifetime.
254    pub expires_at: u64,
255    /// Scopes represented by this token.
256    pub scopes: Vec<String>,
257}
258
259impl fmt::Debug for OAuthStoredToken {
260    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
261        formatter
262            .debug_struct("OAuthStoredToken")
263            .field("access_token", &"[REDACTED]")
264            .field(
265                "refresh_token",
266                &self.refresh_token.as_ref().map(|_| "[REDACTED]"),
267            )
268            .field("expires_at", &self.expires_at)
269            .field("scopes", &self.scopes)
270            .finish()
271    }
272}
273
274/// Persistent storage for resource/issuer/client-bound OAuth tokens.
275#[async_trait]
276pub trait OAuthTokenStore: Send + Sync {
277    /// Load a token set for an exact binding.
278    async fn load(
279        &self,
280        binding: &OAuthTokenBinding,
281    ) -> Result<Option<OAuthStoredToken>, OAuthClientError>;
282
283    /// Save a token set for an exact binding.
284    async fn save(
285        &self,
286        binding: &OAuthTokenBinding,
287        token: &OAuthStoredToken,
288    ) -> Result<(), OAuthClientError>;
289
290    /// Remove a token set for an exact binding.
291    async fn remove(&self, binding: &OAuthTokenBinding) -> Result<(), OAuthClientError>;
292}
293
294/// Process-local OAuth token store.
295#[derive(Clone, Default)]
296pub struct MemoryOAuthTokenStore {
297    tokens: Arc<RwLock<HashMap<OAuthTokenBinding, OAuthStoredToken>>>,
298}
299
300impl fmt::Debug for MemoryOAuthTokenStore {
301    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
302        formatter
303            .debug_struct("MemoryOAuthTokenStore")
304            .finish_non_exhaustive()
305    }
306}
307
308impl MemoryOAuthTokenStore {
309    /// Create an empty process-local token store.
310    pub fn new() -> Self {
311        Self::default()
312    }
313}
314
315#[async_trait]
316impl OAuthTokenStore for MemoryOAuthTokenStore {
317    async fn load(
318        &self,
319        binding: &OAuthTokenBinding,
320    ) -> Result<Option<OAuthStoredToken>, OAuthClientError> {
321        Ok(self.tokens.read().await.get(binding).cloned())
322    }
323
324    async fn save(
325        &self,
326        binding: &OAuthTokenBinding,
327        token: &OAuthStoredToken,
328    ) -> Result<(), OAuthClientError> {
329        self.tokens
330            .write()
331            .await
332            .insert(binding.clone(), token.clone());
333        Ok(())
334    }
335
336    async fn remove(&self, binding: &OAuthTokenBinding) -> Result<(), OAuthClientError> {
337        self.tokens.write().await.remove(binding);
338        Ok(())
339    }
340}
341
342/// Persistable PKCE and CSRF state for an in-progress authorization.
343///
344/// This contains a PKCE verifier and possibly a client secret. Store it with
345/// the same protections as OAuth credentials.
346#[derive(Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
347pub struct OAuthPendingAuthorizationState {
348    /// CSRF state key.
349    pub state: String,
350    /// PKCE verifier.
351    pub code_verifier: String,
352    /// Exact redirect URI.
353    pub redirect_uri: String,
354    /// Canonical resource identifier.
355    pub resource: String,
356    /// Exact authorization-server issuer.
357    pub issuer: String,
358    /// Token endpoint selected from validated metadata.
359    pub token_endpoint: String,
360    /// Resolved client registration.
361    pub registration: OAuthClientRegistration,
362    /// Token endpoint authentication method.
363    pub token_endpoint_auth_method: OAuthTokenEndpointAuthMethod,
364    /// Requested scopes.
365    pub scopes: Vec<String>,
366    /// Whether an authorization-response `iss` parameter is required.
367    pub iss_required: bool,
368}
369
370impl fmt::Debug for OAuthPendingAuthorizationState {
371    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
372        formatter
373            .debug_struct("OAuthPendingAuthorizationState")
374            .field("state", &"[REDACTED]")
375            .field("code_verifier", &"[REDACTED]")
376            .field("redirect_uri", &self.redirect_uri)
377            .field("resource", &self.resource)
378            .field("issuer", &self.issuer)
379            .field("token_endpoint", &self.token_endpoint)
380            .field("registration", &self.registration)
381            .field(
382                "token_endpoint_auth_method",
383                &self.token_endpoint_auth_method,
384            )
385            .field("scopes", &self.scopes)
386            .field("iss_required", &self.iss_required)
387            .finish()
388    }
389}
390
391/// Persistent storage for PKCE and CSRF state.
392#[async_trait]
393pub trait OAuthAuthorizationStateStore: Send + Sync {
394    /// Load pending authorization state by the exact CSRF state key.
395    async fn load(
396        &self,
397        state: &str,
398    ) -> Result<Option<OAuthPendingAuthorizationState>, OAuthClientError>;
399
400    /// Save pending authorization state.
401    async fn save(&self, state: &OAuthPendingAuthorizationState) -> Result<(), OAuthClientError>;
402
403    /// Remove consumed or abandoned state.
404    async fn remove(&self, state: &str) -> Result<(), OAuthClientError>;
405}
406
407/// Process-local PKCE and CSRF state store.
408#[derive(Clone, Default)]
409pub struct MemoryOAuthAuthorizationStateStore {
410    states: Arc<RwLock<HashMap<String, OAuthPendingAuthorizationState>>>,
411}
412
413impl fmt::Debug for MemoryOAuthAuthorizationStateStore {
414    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
415        formatter
416            .debug_struct("MemoryOAuthAuthorizationStateStore")
417            .finish_non_exhaustive()
418    }
419}
420
421impl MemoryOAuthAuthorizationStateStore {
422    /// Create an empty process-local state store.
423    pub fn new() -> Self {
424        Self::default()
425    }
426}
427
428#[async_trait]
429impl OAuthAuthorizationStateStore for MemoryOAuthAuthorizationStateStore {
430    async fn load(
431        &self,
432        state: &str,
433    ) -> Result<Option<OAuthPendingAuthorizationState>, OAuthClientError> {
434        Ok(self.states.read().await.get(state).cloned())
435    }
436
437    async fn save(&self, state: &OAuthPendingAuthorizationState) -> Result<(), OAuthClientError> {
438        self.states
439            .write()
440            .await
441            .insert(state.state.clone(), state.clone());
442        Ok(())
443    }
444
445    async fn remove(&self, state: &str) -> Result<(), OAuthClientError> {
446        self.states.write().await.remove(state);
447        Ok(())
448    }
449}
450
451/// Redirect handling policy for an authorization flow.
452#[derive(Debug, Clone, PartialEq, Eq)]
453#[non_exhaustive]
454pub enum OAuthRedirectPolicy {
455    /// Host a one-shot callback on the loopback interface.
456    Loopback {
457        /// Requested port, or `None` for an ephemeral port.
458        port: Option<u16>,
459        /// Callback path, beginning with `/`.
460        callback_path: String,
461    },
462    /// Use an application-owned redirect URI and complete the flow by passing
463    /// the returned callback URL to [`OAuthAuthorizationFlow::complete_callback_url`].
464    Fixed {
465        /// Exact registered redirect URI.
466        redirect_uri: String,
467    },
468}
469
470impl OAuthRedirectPolicy {
471    /// Use an ephemeral loopback port and `/callback`.
472    pub fn loopback() -> Self {
473        Self::Loopback {
474            port: None,
475            callback_path: "/callback".to_string(),
476        }
477    }
478
479    /// Use a specific loopback port and callback path.
480    pub fn loopback_at(port: u16, callback_path: impl Into<String>) -> Self {
481        Self::Loopback {
482            port: Some(port),
483            callback_path: callback_path.into(),
484        }
485    }
486
487    /// Use an application-owned redirect URI.
488    pub fn fixed(redirect_uri: impl Into<String>) -> Self {
489        Self::Fixed {
490            redirect_uri: redirect_uri.into(),
491        }
492    }
493}
494
495/// Authorization request presented to an application or browser integration.
496#[derive(Debug, Clone, PartialEq, Eq)]
497pub struct OAuthAuthorizationRequest {
498    /// URL that the user agent should open.
499    pub authorization_url: String,
500    /// Exact redirect URI registered for this attempt.
501    pub redirect_uri: String,
502    /// Canonical resource identifier.
503    pub resource: String,
504    /// Exact authorization-server issuer.
505    pub issuer: String,
506    /// Requested scopes.
507    pub scopes: Vec<String>,
508}
509
510/// Result returned by an application authorization handler.
511#[derive(Debug, Clone, PartialEq, Eq)]
512#[non_exhaustive]
513pub enum OAuthAuthorizationAction {
514    /// The handler presented the URL; wait for the configured loopback callback.
515    AwaitLoopback,
516    /// The handler captured and returned the complete callback URL.
517    CallbackUrl(String),
518}
519
520/// Application hook that presents or automates an authorization redirect.
521#[async_trait]
522pub trait OAuthAuthorizationHandler: Send + Sync + 'static {
523    /// Present the authorization request and choose how it completes.
524    async fn authorize(
525        &self,
526        request: OAuthAuthorizationRequest,
527    ) -> Result<OAuthAuthorizationAction, OAuthClientError>;
528}
529
530/// Input to a private-key JWT signer.
531#[derive(Debug, Clone, PartialEq, Eq)]
532pub struct OAuthClientAssertionRequest {
533    /// OAuth client identifier (`iss` and `sub` in the assertion).
534    pub client_id: String,
535    /// Token endpoint URL (`aud` in the assertion).
536    pub token_endpoint: String,
537    /// Exact authorization-server issuer selected for the flow.
538    pub authorization_server_issuer: String,
539}
540
541/// Application hook for `private_key_jwt` client authentication.
542///
543/// Implementations normally create a short-lived signed JWT containing `iss`,
544/// `sub`, `aud`, `iat`, `exp`, and a unique `jti`.
545#[async_trait]
546pub trait OAuthClientAssertionSigner: Send + Sync + 'static {
547    /// Create a client assertion for one token request.
548    async fn sign_client_assertion(
549        &self,
550        request: OAuthClientAssertionRequest,
551    ) -> Result<String, OAuthClientError>;
552}
553
554#[derive(Clone)]
555struct ActiveToken {
556    binding: OAuthTokenBinding,
557    token: OAuthStoredToken,
558    token_endpoint: String,
559    registration: OAuthClientRegistration,
560    auth_method: OAuthTokenEndpointAuthMethod,
561}
562
563struct FlowInner {
564    resource_url: String,
565    registration_options: OAuthClientRegistrationOptions,
566    pre_registered_client: Option<(String, Option<String>)>,
567    registration_store: Arc<dyn OAuthClientRegistrationStore>,
568    token_store: Arc<dyn OAuthTokenStore>,
569    state_store: Arc<dyn OAuthAuthorizationStateStore>,
570    http: Arc<dyn OAuthHttpClient>,
571    redirect_policy: OAuthRedirectPolicy,
572    preferred_issuer: Option<String>,
573    refresh_buffer: Duration,
574    authorization_handler: Option<Arc<dyn OAuthAuthorizationHandler>>,
575    assertion_signer: Option<Arc<dyn OAuthClientAssertionSigner>>,
576    current: RwLock<Option<ActiveToken>>,
577    authorization_lock: Mutex<()>,
578    refresh_lock: Mutex<()>,
579}
580
581/// Reusable OAuth authorization-code state machine and token provider.
582///
583/// Build this type with [`OAuthAuthorizationFlow::builder`], call
584/// [`authorize`](Self::authorize) for a fully driven flow or
585/// [`begin`](Self::begin) for explicit pending/authorized states, then install
586/// the same value as both [`TokenProvider`] and [`OAuthScopeEscalationHandler`].
587#[derive(Clone)]
588pub struct OAuthAuthorizationFlow {
589    inner: Arc<FlowInner>,
590}
591
592impl fmt::Debug for OAuthAuthorizationFlow {
593    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
594        formatter
595            .debug_struct("OAuthAuthorizationFlow")
596            .field("resource_url", &self.inner.resource_url)
597            .field("redirect_policy", &self.inner.redirect_policy)
598            .field("preferred_issuer", &self.inner.preferred_issuer)
599            .finish_non_exhaustive()
600    }
601}
602
603/// Builder for [`OAuthAuthorizationFlow`].
604pub struct OAuthAuthorizationFlowBuilder {
605    resource_url: String,
606    registration_options: OAuthClientRegistrationOptions,
607    pre_registered_client: Option<(String, Option<String>)>,
608    registration_store: Option<Arc<dyn OAuthClientRegistrationStore>>,
609    token_store: Option<Arc<dyn OAuthTokenStore>>,
610    state_store: Option<Arc<dyn OAuthAuthorizationStateStore>>,
611    http: Option<Arc<dyn OAuthHttpClient>>,
612    redirect_policy: Option<OAuthRedirectPolicy>,
613    preferred_issuer: Option<String>,
614    refresh_buffer: Duration,
615    authorization_handler: Option<Arc<dyn OAuthAuthorizationHandler>>,
616    assertion_signer: Option<Arc<dyn OAuthClientAssertionSigner>>,
617}
618
619impl fmt::Debug for OAuthAuthorizationFlowBuilder {
620    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
621        formatter
622            .debug_struct("OAuthAuthorizationFlowBuilder")
623            .field("resource_url", &self.resource_url)
624            .field("registration_options", &self.registration_options)
625            .field("redirect_policy", &self.redirect_policy)
626            .field("preferred_issuer", &self.preferred_issuer)
627            .field("refresh_buffer", &self.refresh_buffer)
628            .finish_non_exhaustive()
629    }
630}
631
632impl OAuthAuthorizationFlowBuilder {
633    /// Configure available client-registration mechanisms.
634    pub fn registration_options(mut self, options: OAuthClientRegistrationOptions) -> Self {
635        self.registration_options = options;
636        self
637    }
638
639    /// Configure pre-registered credentials and bind them to the issuer selected
640    /// during discovery.
641    ///
642    /// This is the convenient form for applications that know their client
643    /// credentials before they know which authorization-server issuer the
644    /// protected resource will advertise. It takes priority over CIMD and DCR,
645    /// just like [`OAuthClientRegistrationOptions::with_pre_registered`].
646    pub fn pre_registered_client(
647        mut self,
648        client_id: impl Into<String>,
649        client_secret: Option<String>,
650    ) -> Self {
651        self.pre_registered_client = Some((client_id.into(), client_secret));
652        self
653    }
654
655    /// Configure issuer-bound client-credential persistence.
656    pub fn registration_store(
657        mut self,
658        store: impl OAuthClientRegistrationStore + 'static,
659    ) -> Self {
660        self.registration_store = Some(Arc::new(store));
661        self
662    }
663
664    /// Configure token persistence.
665    pub fn token_store(mut self, store: impl OAuthTokenStore + 'static) -> Self {
666        self.token_store = Some(Arc::new(store));
667        self
668    }
669
670    /// Configure persisted PKCE and CSRF state.
671    pub fn state_store(mut self, store: impl OAuthAuthorizationStateStore + 'static) -> Self {
672        self.state_store = Some(Arc::new(store));
673        self
674    }
675
676    /// Configure the OAuth protocol HTTP implementation.
677    pub fn http_client(mut self, client: impl OAuthHttpClient) -> Self {
678        self.http = Some(Arc::new(client));
679        self
680    }
681
682    /// Configure explicit redirect handling.
683    pub fn redirect_policy(mut self, policy: OAuthRedirectPolicy) -> Self {
684        self.redirect_policy = Some(policy);
685        self
686    }
687
688    /// Select one exact issuer when the resource advertises multiple servers.
689    pub fn preferred_authorization_server(mut self, issuer: impl Into<String>) -> Self {
690        self.preferred_issuer = Some(issuer.into());
691        self
692    }
693
694    /// Set the pre-expiry refresh buffer.
695    pub fn refresh_buffer(mut self, buffer: Duration) -> Self {
696        self.refresh_buffer = buffer;
697        self
698    }
699
700    /// Configure automatic browser/headless authorization handling.
701    pub fn authorization_handler(mut self, handler: impl OAuthAuthorizationHandler) -> Self {
702        self.authorization_handler = Some(Arc::new(handler));
703        self
704    }
705
706    /// Configure `private_key_jwt` assertion signing.
707    pub fn client_assertion_signer(mut self, signer: impl OAuthClientAssertionSigner) -> Self {
708        self.assertion_signer = Some(Arc::new(signer));
709        self
710    }
711
712    /// Build the state machine.
713    ///
714    /// # Errors
715    ///
716    /// Returns an error when no redirect policy was supplied or when the
717    /// default reqwest client cannot be constructed.
718    pub fn build(self) -> Result<OAuthAuthorizationFlow, OAuthClientError> {
719        let redirect_policy = self.redirect_policy.ok_or_else(|| {
720            OAuthClientError::BuildError(
721                "OAuthAuthorizationFlow requires an explicit redirect policy".to_string(),
722            )
723        })?;
724        validate_redirect_policy(&redirect_policy)?;
725        let http = match self.http {
726            Some(http) => http,
727            None => Arc::new(ReqwestOAuthHttpClient::without_redirects()?),
728        };
729        Ok(OAuthAuthorizationFlow {
730            inner: Arc::new(FlowInner {
731                resource_url: self.resource_url,
732                registration_options: self.registration_options,
733                pre_registered_client: self.pre_registered_client,
734                registration_store: self.registration_store.unwrap_or_else(|| {
735                    Arc::new(super::oauth_authcode::MemoryOAuthClientRegistrationStore::new())
736                }),
737                token_store: self
738                    .token_store
739                    .unwrap_or_else(|| Arc::new(MemoryOAuthTokenStore::new())),
740                state_store: self
741                    .state_store
742                    .unwrap_or_else(|| Arc::new(MemoryOAuthAuthorizationStateStore::new())),
743                http,
744                redirect_policy,
745                preferred_issuer: self.preferred_issuer,
746                refresh_buffer: self.refresh_buffer,
747                authorization_handler: self.authorization_handler,
748                assertion_signer: self.assertion_signer,
749                current: RwLock::new(None),
750                authorization_lock: Mutex::new(()),
751                refresh_lock: Mutex::new(()),
752            }),
753        })
754    }
755}
756
757fn validate_redirect_policy(policy: &OAuthRedirectPolicy) -> Result<(), OAuthClientError> {
758    let (redirect_uri, callback_path) = match policy {
759        OAuthRedirectPolicy::Fixed { redirect_uri } => (Some(redirect_uri.as_str()), None),
760        OAuthRedirectPolicy::Loopback { callback_path, .. } => (None, Some(callback_path)),
761    };
762    if let Some(uri) = redirect_uri {
763        let parsed = reqwest::Url::parse(uri)
764            .map_err(|error| OAuthClientError::BuildError(error.to_string()))?;
765        if parsed.fragment().is_some() {
766            return Err(OAuthClientError::BuildError(
767                "OAuth redirect URI must not contain a fragment".to_string(),
768            ));
769        }
770    }
771    if let Some(path) = callback_path
772        && (!path.starts_with('/') || path.contains('?') || path.contains('#'))
773    {
774        return Err(OAuthClientError::BuildError(
775            "loopback callback path must begin with `/` and contain no query or fragment"
776                .to_string(),
777        ));
778    }
779    Ok(())
780}
781
782/// Result of beginning an OAuth authorization attempt.
783#[non_exhaustive]
784pub enum OAuthAuthorizationStart {
785    /// A persisted, sufficiently scoped token was restored without redirecting.
786    Authorized {
787        /// Scopes restored from the token store.
788        scopes: Vec<String>,
789    },
790    /// User-agent authorization is required.
791    Pending(OAuthPendingAuthorization),
792}
793
794impl fmt::Debug for OAuthAuthorizationStart {
795    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
796        match self {
797            Self::Authorized { scopes } => formatter
798                .debug_struct("Authorized")
799                .field("scopes", scopes)
800                .finish(),
801            Self::Pending(pending) => formatter.debug_tuple("Pending").field(pending).finish(),
802        }
803    }
804}
805
806/// An authorization attempt waiting for a callback.
807pub struct OAuthPendingAuthorization {
808    flow: OAuthAuthorizationFlow,
809    request: OAuthAuthorizationRequest,
810    state: String,
811    callback_rx: Mutex<Option<oneshot::Receiver<String>>>,
812}
813
814impl fmt::Debug for OAuthPendingAuthorization {
815    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
816        formatter
817            .debug_struct("OAuthPendingAuthorization")
818            .field("request", &self.request)
819            .field("state", &"[REDACTED]")
820            .finish_non_exhaustive()
821    }
822}
823
824impl OAuthPendingAuthorization {
825    /// Authorization request to present to a user agent or headless harness.
826    pub fn request(&self) -> &OAuthAuthorizationRequest {
827        &self.request
828    }
829
830    /// Complete this attempt with the callback URL returned by the AS.
831    pub async fn complete_callback_url(self, callback_url: &str) -> Result<(), OAuthClientError> {
832        self.flow
833            .complete_callback_url_for_state(callback_url, Some(&self.state))
834            .await
835    }
836
837    /// Wait for a configured loopback redirect and complete the attempt.
838    pub async fn wait_for_callback(self) -> Result<(), OAuthClientError> {
839        self.wait_for_callback_with_timeout(Duration::from_secs(300))
840            .await
841    }
842
843    /// Wait for a loopback redirect with a custom timeout.
844    pub async fn wait_for_callback_with_timeout(
845        self,
846        timeout: Duration,
847    ) -> Result<(), OAuthClientError> {
848        let receiver = self.callback_rx.lock().await.take().ok_or_else(|| {
849            OAuthClientError::Redirect(
850                "this redirect policy does not own a loopback callback".to_string(),
851            )
852        })?;
853        let callback_url = tokio::time::timeout(timeout, receiver)
854            .await
855            .map_err(|_| OAuthClientError::Redirect("OAuth callback timed out".to_string()))?
856            .map_err(|_| {
857                OAuthClientError::Redirect("OAuth callback listener closed".to_string())
858            })?;
859        self.flow
860            .complete_callback_url_for_state(&callback_url, Some(&self.state))
861            .await
862    }
863}
864
865impl OAuthAuthorizationFlow {
866    /// Begin building a state machine for `resource_url`.
867    pub fn builder(resource_url: impl Into<String>) -> OAuthAuthorizationFlowBuilder {
868        OAuthAuthorizationFlowBuilder {
869            resource_url: resource_url.into(),
870            registration_options: OAuthClientRegistrationOptions::new(),
871            pre_registered_client: None,
872            registration_store: None,
873            token_store: None,
874            state_store: None,
875            http: None,
876            redirect_policy: None,
877            preferred_issuer: None,
878            refresh_buffer: Duration::from_secs(30),
879            authorization_handler: None,
880            assertion_signer: None,
881        }
882    }
883
884    /// Discover, register, restore or create authorization state, and return
885    /// the next explicit state.
886    pub async fn begin<I, S>(&self, scopes: I) -> Result<OAuthAuthorizationStart, OAuthClientError>
887    where
888        I: IntoIterator<Item = S>,
889        S: AsRef<str>,
890    {
891        self.begin_with_challenge(unique_scopes(scopes), None).await
892    }
893
894    /// Drive a complete authorization using the configured
895    /// [`OAuthAuthorizationHandler`].
896    pub async fn authorize<I, S>(&self, scopes: I) -> Result<(), OAuthClientError>
897    where
898        I: IntoIterator<Item = S>,
899        S: AsRef<str>,
900    {
901        self.authorize_with_challenge(unique_scopes(scopes), None)
902            .await
903    }
904
905    async fn authorize_with_challenge(
906        &self,
907        scopes: Vec<String>,
908        challenge: Option<OAuthBearerChallenge>,
909    ) -> Result<(), OAuthClientError> {
910        let _guard = self.inner.authorization_lock.lock().await;
911        let start = self.begin_with_challenge(scopes, challenge).await?;
912        let OAuthAuthorizationStart::Pending(pending) = start else {
913            return Ok(());
914        };
915        let handler = self.inner.authorization_handler.as_ref().ok_or_else(|| {
916            OAuthClientError::Redirect(
917                "authorization is pending; configure an OAuthAuthorizationHandler or use begin()"
918                    .to_string(),
919            )
920        })?;
921        match handler.authorize(pending.request().clone()).await? {
922            OAuthAuthorizationAction::AwaitLoopback => pending.wait_for_callback().await,
923            OAuthAuthorizationAction::CallbackUrl(url) => pending.complete_callback_url(&url).await,
924        }
925    }
926
927    async fn begin_with_challenge(
928        &self,
929        explicit_scopes: Vec<String>,
930        challenge: Option<OAuthBearerChallenge>,
931    ) -> Result<OAuthAuthorizationStart, OAuthClientError> {
932        let discovery = discover_with_http(
933            &self.inner.resource_url,
934            challenge,
935            self.inner.http.as_ref(),
936        )
937        .await?;
938        let metadata =
939            select_authorization_server(&discovery, self.inner.preferred_issuer.as_deref())?;
940        require_s256(&metadata)?;
941
942        let state = random_urlsafe(16);
943        let code_verifier = random_urlsafe(32);
944        let code_challenge = pkce_challenge(&code_verifier);
945        let (redirect_uri, callback_rx) =
946            prepare_redirect(&self.inner.redirect_policy, &state).await?;
947
948        let mut registration_options = self.inner.registration_options.clone();
949        if registration_options.pre_registered.is_none()
950            && let Some((client_id, client_secret)) = &self.inner.pre_registered_client
951        {
952            registration_options.pre_registered = Some(OAuthClientRegistration::pre_registered(
953                metadata.issuer.clone(),
954                client_id.clone(),
955                client_secret.clone(),
956            ));
957        }
958        if let Some(dynamic) = registration_options.dynamic_registration.as_mut() {
959            dynamic.redirect_uris = vec![redirect_uri.clone()];
960            if metadata
961                .grant_types_supported
962                .iter()
963                .any(|grant| grant == "refresh_token")
964                && !dynamic
965                    .grant_types
966                    .iter()
967                    .any(|grant| grant == "refresh_token")
968            {
969                dynamic.grant_types.push("refresh_token".to_string());
970            }
971            dynamic.token_endpoint_auth_method = preferred_registration_auth_method(
972                &metadata.token_endpoint_auth_methods_supported,
973                self.inner.assertion_signer.is_some(),
974            )
975            .to_string();
976        }
977        let registration = resolve_registration_with_http(
978            self.inner.http.as_ref(),
979            &metadata,
980            &registration_options,
981            self.inner.registration_store.as_ref(),
982        )
983        .await?;
984
985        let auth_method = OAuthTokenEndpointAuthMethod::select_with_private_key(
986            &metadata.token_endpoint_auth_methods_supported,
987            registration.client_secret().is_some(),
988            self.inner.assertion_signer.is_some(),
989        )?;
990        let scopes = select_scopes(&explicit_scopes, &discovery, &metadata);
991        let binding = OAuthTokenBinding {
992            resource: discovery.resource.clone(),
993            issuer: metadata.issuer.clone(),
994            client_id: registration.client_id().to_string(),
995        };
996
997        if let Some(token) = self.inner.token_store.load(&binding).await?
998            && scopes_are_covered(&scopes, &token.scopes)
999            && (token_is_valid(&token, self.inner.refresh_buffer) || token.refresh_token.is_some())
1000        {
1001            *self.inner.current.write().await = Some(ActiveToken {
1002                binding,
1003                token: token.clone(),
1004                token_endpoint: metadata.token_endpoint,
1005                registration,
1006                auth_method,
1007            });
1008            if !token_is_valid(&token, self.inner.refresh_buffer) {
1009                TokenProvider::get_token(self).await?;
1010            }
1011            return Ok(OAuthAuthorizationStart::Authorized {
1012                scopes: token.scopes,
1013            });
1014        }
1015
1016        let pending_state = OAuthPendingAuthorizationState {
1017            state: state.clone(),
1018            code_verifier,
1019            redirect_uri: redirect_uri.clone(),
1020            resource: discovery.resource.clone(),
1021            issuer: metadata.issuer.clone(),
1022            token_endpoint: metadata.token_endpoint.clone(),
1023            registration: registration.clone(),
1024            token_endpoint_auth_method: auth_method,
1025            scopes: scopes.clone(),
1026            iss_required: metadata.authorization_response_iss_parameter_supported,
1027        };
1028        self.inner.state_store.save(&pending_state).await?;
1029
1030        let mut authorization_url = reqwest::Url::parse(&metadata.authorization_endpoint)
1031            .map_err(|error| OAuthClientError::Discovery(error.to_string()))?;
1032        {
1033            let mut query = authorization_url.query_pairs_mut();
1034            query
1035                .append_pair("response_type", "code")
1036                .append_pair("client_id", registration.client_id())
1037                .append_pair("redirect_uri", &redirect_uri)
1038                .append_pair("state", &state)
1039                .append_pair("code_challenge", &code_challenge)
1040                .append_pair("code_challenge_method", "S256")
1041                .append_pair("resource", &discovery.resource);
1042            if !scopes.is_empty() {
1043                query.append_pair("scope", &scopes.join(" "));
1044            }
1045        }
1046        Ok(OAuthAuthorizationStart::Pending(
1047            OAuthPendingAuthorization {
1048                flow: self.clone(),
1049                request: OAuthAuthorizationRequest {
1050                    authorization_url: authorization_url.to_string(),
1051                    redirect_uri,
1052                    resource: discovery.resource,
1053                    issuer: metadata.issuer,
1054                    scopes,
1055                },
1056                state,
1057                callback_rx: Mutex::new(callback_rx),
1058            },
1059        ))
1060    }
1061
1062    /// Resume a persisted authorization attempt from a callback URL.
1063    pub async fn complete_callback_url(&self, callback_url: &str) -> Result<(), OAuthClientError> {
1064        self.complete_callback_url_for_state(callback_url, None)
1065            .await
1066    }
1067
1068    async fn complete_callback_url_for_state(
1069        &self,
1070        callback_url: &str,
1071        expected_state: Option<&str>,
1072    ) -> Result<(), OAuthClientError> {
1073        let parsed = parse_callback_url(callback_url)?;
1074        if let Some(expected) = expected_state
1075            && parsed.state != expected
1076        {
1077            return Err(OAuthClientError::InvalidResponse(
1078                "OAuth callback state mismatch".to_string(),
1079            ));
1080        }
1081        let pending = self
1082            .inner
1083            .state_store
1084            .load(&parsed.state)
1085            .await?
1086            .ok_or_else(|| {
1087                OAuthClientError::StateStore(
1088                    "no persisted OAuth authorization matches the callback state".to_string(),
1089                )
1090            })?;
1091        validate_callback_target(callback_url, &pending.redirect_uri)?;
1092        validate_callback_issuer(
1093            parsed.issuer.as_deref(),
1094            &pending.issuer,
1095            pending.iss_required,
1096        )?;
1097
1098        let fields = vec![
1099            ("grant_type".to_string(), "authorization_code".to_string()),
1100            ("code".to_string(), parsed.code),
1101            ("redirect_uri".to_string(), pending.redirect_uri.clone()),
1102            ("code_verifier".to_string(), pending.code_verifier.clone()),
1103            ("resource".to_string(), pending.resource.clone()),
1104        ];
1105        let response = send_token_request(
1106            self.inner.http.as_ref(),
1107            &pending.token_endpoint,
1108            &pending.issuer,
1109            &pending.registration,
1110            pending.token_endpoint_auth_method,
1111            fields,
1112            self.inner.assertion_signer.as_deref(),
1113        )
1114        .await?;
1115        let token = token_from_response(response, &pending.scopes, None)?;
1116        let binding = OAuthTokenBinding {
1117            resource: pending.resource.clone(),
1118            issuer: pending.issuer.clone(),
1119            client_id: pending.registration.client_id().to_string(),
1120        };
1121        self.inner.token_store.save(&binding, &token).await?;
1122        self.inner.state_store.remove(&parsed.state).await?;
1123        *self.inner.current.write().await = Some(ActiveToken {
1124            binding,
1125            token,
1126            token_endpoint: pending.token_endpoint,
1127            registration: pending.registration,
1128            auth_method: pending.token_endpoint_auth_method,
1129        });
1130        Ok(())
1131    }
1132
1133    /// Return the currently authorized scope set, if any.
1134    pub async fn authorized_scopes(&self) -> Option<Vec<String>> {
1135        self.inner
1136            .current
1137            .read()
1138            .await
1139            .as_ref()
1140            .map(|active| active.token.scopes.clone())
1141    }
1142}
1143
1144#[async_trait]
1145impl TokenProvider for OAuthAuthorizationFlow {
1146    async fn get_token(&self) -> Result<String, OAuthClientError> {
1147        if let Some(active) = self.inner.current.read().await.as_ref()
1148            && token_is_valid(&active.token, self.inner.refresh_buffer)
1149        {
1150            return Ok(active.token.access_token.clone());
1151        }
1152
1153        let _guard = self.inner.refresh_lock.lock().await;
1154        let active = self.inner.current.read().await.clone().ok_or_else(|| {
1155            OAuthClientError::TokenRequest(
1156                "OAuthAuthorizationFlow is not authorized; call authorize() or begin()".to_string(),
1157            )
1158        })?;
1159        if token_is_valid(&active.token, self.inner.refresh_buffer) {
1160            return Ok(active.token.access_token);
1161        }
1162        let refresh_token = active.token.refresh_token.as_deref().ok_or_else(|| {
1163            OAuthClientError::TokenRequest(
1164                "OAuth access token expired and no refresh token is available".to_string(),
1165            )
1166        })?;
1167        let mut fields = vec![
1168            ("grant_type".to_string(), "refresh_token".to_string()),
1169            ("refresh_token".to_string(), refresh_token.to_string()),
1170            ("resource".to_string(), active.binding.resource.clone()),
1171        ];
1172        if !active.token.scopes.is_empty() {
1173            fields.push(("scope".to_string(), active.token.scopes.join(" ")));
1174        }
1175        let response = send_token_request(
1176            self.inner.http.as_ref(),
1177            &active.token_endpoint,
1178            &active.binding.issuer,
1179            &active.registration,
1180            active.auth_method,
1181            fields,
1182            self.inner.assertion_signer.as_deref(),
1183        )
1184        .await?;
1185        let token = token_from_response(
1186            response,
1187            &active.token.scopes,
1188            active.token.refresh_token.clone(),
1189        )?;
1190        self.inner.token_store.save(&active.binding, &token).await?;
1191        let access_token = token.access_token.clone();
1192        *self.inner.current.write().await = Some(ActiveToken { token, ..active });
1193        Ok(access_token)
1194    }
1195}
1196
1197#[async_trait]
1198impl OAuthScopeEscalationHandler for OAuthAuthorizationFlow {
1199    async fn reauthorize(
1200        &self,
1201        request: OAuthScopeEscalationRequest,
1202    ) -> Result<(), OAuthClientError> {
1203        if request.resource != self.inner.resource_url {
1204            return Err(OAuthClientError::ScopeEscalation(format!(
1205                "scope challenge resource `{}` does not match flow resource `{}`",
1206                request.resource, self.inner.resource_url
1207            )));
1208        }
1209        let challenge = OAuthBearerChallenge {
1210            error: Some("insufficient_scope".to_string()),
1211            scopes: request.challenge.required_scopes,
1212            resource_metadata: request.challenge.resource_metadata,
1213            error_description: request.challenge.error_description,
1214        };
1215        self.authorize_with_challenge(request.requested_scopes, Some(challenge))
1216            .await
1217            .map_err(|error| OAuthClientError::ScopeEscalation(error.to_string()))
1218    }
1219}
1220
1221#[derive(Debug)]
1222struct FlowDiscovery {
1223    resource: String,
1224    protected_resource: OAuthProtectedResourceMetadata,
1225    authorization_servers: Vec<OAuthAuthorizationServerMetadata>,
1226    challenge: Option<OAuthBearerChallenge>,
1227}
1228
1229async fn discover_with_http(
1230    resource_url: &str,
1231    challenge: Option<OAuthBearerChallenge>,
1232    http: &dyn OAuthHttpClient,
1233) -> Result<FlowDiscovery, OAuthClientError> {
1234    let challenge = match challenge {
1235        Some(challenge) => Some(challenge),
1236        None => {
1237            // Probe a protected MCP operation rather than `initialize`, which
1238            // servers commonly leave public. A POST also works with MCP
1239            // endpoints that do not implement GET and therefore expose their
1240            // RFC 9728 discovery hint only on normal protocol requests.
1241            let mut request = OAuthHttpRequest::post_json(
1242                resource_url,
1243                serde_json::json!({
1244                    "jsonrpc": "2.0",
1245                    "id": "tower-mcp-oauth-discovery",
1246                    "method": "tools/list",
1247                    "params": {}
1248                }),
1249            );
1250            request.headers.push((
1251                "accept".to_string(),
1252                "application/json, text/event-stream".to_string(),
1253            ));
1254            let response = http.execute(request).await?;
1255            response
1256                .header_values("www-authenticate")
1257                .find_map(OAuthBearerChallenge::from_www_authenticate)
1258        }
1259    };
1260    let protected_resource = if let Some(url) = challenge
1261        .as_ref()
1262        .and_then(|challenge| challenge.resource_metadata.as_deref())
1263    {
1264        fetch_json(http, url).await?
1265    } else {
1266        let mut discovered: Option<OAuthProtectedResourceMetadata> = None;
1267        for url in protected_resource_metadata_urls(resource_url)? {
1268            if let Ok(metadata) = fetch_json(http, &url).await {
1269                discovered = Some(metadata);
1270                break;
1271            }
1272        }
1273        discovered.ok_or_else(|| {
1274            OAuthClientError::Discovery(format!(
1275                "could not discover Protected Resource Metadata for `{resource_url}`"
1276            ))
1277        })?
1278    };
1279    validate_resource_identifier(resource_url, &protected_resource.resource)?;
1280    if protected_resource.authorization_servers.is_empty() {
1281        return Err(OAuthClientError::Discovery(
1282            "protected resource metadata omitted authorization_servers".to_string(),
1283        ));
1284    }
1285
1286    let mut authorization_servers = Vec::new();
1287    let mut last_error = None;
1288    for issuer in &protected_resource.authorization_servers {
1289        match discover_authorization_server(http, issuer).await {
1290            Ok(metadata) => authorization_servers.push(metadata),
1291            Err(error) => last_error = Some(error),
1292        }
1293    }
1294    if authorization_servers.is_empty() {
1295        return Err(last_error.unwrap_or_else(|| {
1296            OAuthClientError::Discovery(
1297                "protected resource advertised no usable authorization server".to_string(),
1298            )
1299        }));
1300    }
1301    Ok(FlowDiscovery {
1302        resource: protected_resource.resource.clone(),
1303        protected_resource,
1304        authorization_servers,
1305        challenge,
1306    })
1307}
1308
1309async fn discover_authorization_server(
1310    http: &dyn OAuthHttpClient,
1311    issuer: &str,
1312) -> Result<OAuthAuthorizationServerMetadata, OAuthClientError> {
1313    let mut last_error = None;
1314    for url in authorization_server_metadata_urls(issuer)? {
1315        match fetch_json::<OAuthAuthorizationServerMetadata>(http, &url).await {
1316            Ok(metadata) if metadata.issuer == issuer => return Ok(metadata),
1317            Ok(metadata) => {
1318                last_error = Some(OAuthClientError::Discovery(format!(
1319                    "authorization server issuer mismatch: expected `{issuer}`, got `{}`",
1320                    metadata.issuer
1321                )))
1322            }
1323            Err(error) => last_error = Some(error),
1324        }
1325    }
1326    Err(last_error.unwrap_or_else(|| {
1327        OAuthClientError::Discovery(format!(
1328            "could not discover authorization server metadata for `{issuer}`"
1329        ))
1330    }))
1331}
1332
1333async fn fetch_json<T: serde::de::DeserializeOwned>(
1334    http: &dyn OAuthHttpClient,
1335    url: &str,
1336) -> Result<T, OAuthClientError> {
1337    let response = http.execute(OAuthHttpRequest::get(url)).await?;
1338    if !response.is_success() {
1339        return Err(OAuthClientError::Discovery(format!(
1340            "GET `{url}` returned HTTP {}: {}",
1341            response.status,
1342            response.error_body()
1343        )));
1344    }
1345    response.json()
1346}
1347
1348fn protected_resource_metadata_urls(resource_url: &str) -> Result<Vec<String>, OAuthClientError> {
1349    let parsed = reqwest::Url::parse(resource_url)
1350        .map_err(|error| OAuthClientError::Discovery(error.to_string()))?;
1351    let origin = parsed.origin().ascii_serialization();
1352    let path = parsed.path();
1353    let mut urls = Vec::new();
1354    if !path.is_empty() && path != "/" {
1355        urls.push(format!(
1356            "{origin}/.well-known/oauth-protected-resource{path}"
1357        ));
1358    }
1359    urls.push(format!("{origin}/.well-known/oauth-protected-resource"));
1360    urls.dedup();
1361    Ok(urls)
1362}
1363
1364fn authorization_server_metadata_urls(issuer: &str) -> Result<Vec<String>, OAuthClientError> {
1365    let parsed = reqwest::Url::parse(issuer)
1366        .map_err(|error| OAuthClientError::Discovery(error.to_string()))?;
1367    let origin = parsed.origin().ascii_serialization();
1368    let path = parsed.path();
1369    let trimmed = issuer.trim_end_matches('/');
1370    let mut urls = Vec::new();
1371    if !path.is_empty() && path != "/" {
1372        urls.push(format!(
1373            "{origin}/.well-known/oauth-authorization-server{path}"
1374        ));
1375        urls.push(format!("{origin}/.well-known/openid-configuration{path}"));
1376        urls.push(format!("{trimmed}/.well-known/openid-configuration"));
1377    } else {
1378        urls.push(format!("{origin}/.well-known/oauth-authorization-server"));
1379        urls.push(format!("{origin}/.well-known/openid-configuration"));
1380    }
1381    urls.dedup();
1382    Ok(urls)
1383}
1384
1385fn validate_resource_identifier(expected: &str, actual: &str) -> Result<(), OAuthClientError> {
1386    let expected = reqwest::Url::parse(expected)
1387        .map_err(|error| OAuthClientError::Discovery(error.to_string()))?;
1388    let actual = reqwest::Url::parse(actual)
1389        .map_err(|error| OAuthClientError::Discovery(error.to_string()))?;
1390    // An origin-level canonical resource may cover a more specific MCP
1391    // endpoint on that origin (for example resource `https://example.com`
1392    // with endpoint `https://example.com/mcp`). A non-root resource path must
1393    // be an exact path-segment prefix, never a merely textual prefix.
1394    let expected_path = expected.path();
1395    let actual_path = actual.path();
1396    let path_matches = actual_path == "/"
1397        || expected_path == actual_path
1398        || (actual_path.ends_with('/') && expected_path.starts_with(actual_path))
1399        || expected_path
1400            .strip_prefix(actual_path)
1401            .is_some_and(|suffix| suffix.starts_with('/'));
1402    let query_matches = actual.query().is_none() || actual.query() == expected.query();
1403    let matches = actual.fragment().is_none()
1404        && expected.scheme() == actual.scheme()
1405        && expected.host_str() == actual.host_str()
1406        && expected.port_or_known_default() == actual.port_or_known_default()
1407        && path_matches
1408        && query_matches;
1409    if matches {
1410        Ok(())
1411    } else {
1412        Err(OAuthClientError::Discovery(format!(
1413            "protected resource mismatch: expected `{expected}`, got `{actual}`"
1414        )))
1415    }
1416}
1417
1418fn select_authorization_server(
1419    discovery: &FlowDiscovery,
1420    preferred_issuer: Option<&str>,
1421) -> Result<OAuthAuthorizationServerMetadata, OAuthClientError> {
1422    match preferred_issuer {
1423        Some(issuer) => discovery
1424            .authorization_servers
1425            .iter()
1426            .find(|metadata| metadata.issuer == issuer)
1427            .cloned()
1428            .ok_or_else(|| {
1429                OAuthClientError::Discovery(format!(
1430                    "preferred authorization server `{issuer}` was not advertised"
1431                ))
1432            }),
1433        None => discovery
1434            .authorization_servers
1435            .first()
1436            .cloned()
1437            .ok_or_else(|| OAuthClientError::Discovery("no authorization server found".into())),
1438    }
1439}
1440
1441fn require_s256(metadata: &OAuthAuthorizationServerMetadata) -> Result<(), OAuthClientError> {
1442    if metadata
1443        .code_challenge_methods_supported
1444        .iter()
1445        .any(|method| method == "S256")
1446    {
1447        Ok(())
1448    } else {
1449        Err(OAuthClientError::Discovery(format!(
1450            "authorization server `{}` does not advertise PKCE S256",
1451            metadata.issuer
1452        )))
1453    }
1454}
1455
1456fn select_scopes(
1457    explicit: &[String],
1458    discovery: &FlowDiscovery,
1459    metadata: &OAuthAuthorizationServerMetadata,
1460) -> Vec<String> {
1461    let selected = if !explicit.is_empty() {
1462        explicit.to_vec()
1463    } else if let Some(challenge) = &discovery.challenge
1464        && !challenge.scopes.is_empty()
1465    {
1466        challenge.scopes.clone()
1467    } else if !discovery.protected_resource.scopes_supported.is_empty() {
1468        discovery.protected_resource.scopes_supported.clone()
1469    } else {
1470        metadata.scopes_supported.clone()
1471    };
1472    let mut selected = unique_scopes(selected);
1473    let refresh_supported = metadata.grant_types_supported.is_empty()
1474        || metadata
1475            .grant_types_supported
1476            .iter()
1477            .any(|grant| grant == "refresh_token");
1478    if refresh_supported
1479        && metadata
1480            .scopes_supported
1481            .iter()
1482            .any(|scope| scope == "offline_access")
1483        && !selected.iter().any(|scope| scope == "offline_access")
1484    {
1485        selected.push("offline_access".to_string());
1486    }
1487    selected
1488}
1489
1490fn preferred_registration_auth_method(
1491    advertised: &[String],
1492    has_private_key_signer: bool,
1493) -> &'static str {
1494    if has_private_key_signer && advertised.iter().any(|method| method == "private_key_jwt") {
1495        "private_key_jwt"
1496    } else if advertised
1497        .iter()
1498        .any(|method| method == "client_secret_basic")
1499    {
1500        "client_secret_basic"
1501    } else if advertised
1502        .iter()
1503        .any(|method| method == "client_secret_post")
1504    {
1505        "client_secret_post"
1506    } else {
1507        "none"
1508    }
1509}
1510
1511async fn resolve_registration_with_http(
1512    http: &dyn OAuthHttpClient,
1513    metadata: &OAuthAuthorizationServerMetadata,
1514    options: &OAuthClientRegistrationOptions,
1515    store: &dyn OAuthClientRegistrationStore,
1516) -> Result<OAuthClientRegistration, OAuthClientError> {
1517    if let Some(registration) = &options.pre_registered {
1518        if registration.method() != OAuthClientRegistrationMethod::PreRegistered {
1519            return Err(OAuthClientError::BuildError(
1520                "pre_registered must contain pre-registered credentials".to_string(),
1521            ));
1522        }
1523        if registration.bound_issuer() != Some(metadata.issuer.as_str()) {
1524            return Err(OAuthClientError::BuildError(format!(
1525                "pre-registered credentials are bound to issuer {:?}, not `{}`",
1526                registration.bound_issuer(),
1527                metadata.issuer
1528            )));
1529        }
1530        return Ok(registration.clone());
1531    }
1532
1533    if metadata.client_id_metadata_document_supported
1534        && let Some(client_id) = &options.client_id_metadata_document
1535    {
1536        validate_cimd_url(client_id)?;
1537        return Ok(OAuthClientRegistration::client_id_metadata_document(
1538            client_id.clone(),
1539        ));
1540    }
1541
1542    if options.dynamic_registration.is_some()
1543        && let Some(registration) = store.load(&metadata.issuer).await?
1544    {
1545        if registration.method() != OAuthClientRegistrationMethod::Dynamic
1546            || registration.bound_issuer() != Some(metadata.issuer.as_str())
1547        {
1548            return Err(OAuthClientError::CredentialStore(format!(
1549                "stored registration is not dynamically bound to `{}`",
1550                metadata.issuer
1551            )));
1552        }
1553        return Ok(registration);
1554    }
1555
1556    if let (Some(endpoint), Some(request)) = (
1557        metadata.registration_endpoint.as_deref(),
1558        options.dynamic_registration.as_ref(),
1559    ) {
1560        if request.redirect_uris.is_empty() {
1561            return Err(OAuthClientError::BuildError(
1562                "dynamic registration requires a redirect URI".to_string(),
1563            ));
1564        }
1565        let value = serde_json::to_value(request)
1566            .map_err(|error| OAuthClientError::Registration(error.to_string()))?;
1567        let response = http
1568            .execute(OAuthHttpRequest::post_json(endpoint, value))
1569            .await?;
1570        if !response.is_success() {
1571            return Err(OAuthClientError::Registration(format!(
1572                "dynamic registration returned HTTP {}: {}",
1573                response.status,
1574                response.error_body()
1575            )));
1576        }
1577        #[derive(serde::Deserialize)]
1578        struct RegistrationResponse {
1579            client_id: String,
1580            client_secret: Option<String>,
1581        }
1582        let registered: RegistrationResponse = response.json()?;
1583        let registration = OAuthClientRegistration::dynamically_registered(
1584            metadata.issuer.clone(),
1585            registered.client_id,
1586            registered.client_secret,
1587        );
1588        store.save(&metadata.issuer, &registration).await?;
1589        return Ok(registration);
1590    }
1591
1592    Err(OAuthClientError::BuildError(
1593        "authorization server supports none of the configured client registration mechanisms"
1594            .to_string(),
1595    ))
1596}
1597
1598fn validate_cimd_url(client_id: &str) -> Result<(), OAuthClientError> {
1599    let url = reqwest::Url::parse(client_id).map_err(|error| {
1600        OAuthClientError::BuildError(format!("invalid CIMD client ID `{client_id}`: {error}"))
1601    })?;
1602    if url.scheme() != "https" || url.path() == "/" {
1603        return Err(OAuthClientError::BuildError(format!(
1604            "CIMD client ID `{client_id}` must use HTTPS and contain a path"
1605        )));
1606    }
1607    Ok(())
1608}
1609
1610async fn send_token_request(
1611    http: &dyn OAuthHttpClient,
1612    token_endpoint: &str,
1613    issuer: &str,
1614    registration: &OAuthClientRegistration,
1615    method: OAuthTokenEndpointAuthMethod,
1616    mut fields: Vec<(String, String)>,
1617    assertion_signer: Option<&dyn OAuthClientAssertionSigner>,
1618) -> Result<OAuthHttpResponse, OAuthClientError> {
1619    let mut request = OAuthHttpRequest::post_form(token_endpoint, Vec::new());
1620    match method {
1621        OAuthTokenEndpointAuthMethod::None => {
1622            fields.push((
1623                "client_id".to_string(),
1624                registration.client_id().to_string(),
1625            ));
1626        }
1627        OAuthTokenEndpointAuthMethod::ClientSecretBasic => {
1628            let secret = registration.client_secret().ok_or_else(|| {
1629                OAuthClientError::BuildError(
1630                    "client_secret_basic selected without a client secret".to_string(),
1631                )
1632            })?;
1633            request = request.basic_auth(registration.client_id(), secret);
1634        }
1635        OAuthTokenEndpointAuthMethod::ClientSecretPost => {
1636            let secret = registration.client_secret().ok_or_else(|| {
1637                OAuthClientError::BuildError(
1638                    "client_secret_post selected without a client secret".to_string(),
1639                )
1640            })?;
1641            fields.push((
1642                "client_id".to_string(),
1643                registration.client_id().to_string(),
1644            ));
1645            fields.push(("client_secret".to_string(), secret.to_string()));
1646        }
1647        OAuthTokenEndpointAuthMethod::PrivateKeyJwt => {
1648            let signer = assertion_signer.ok_or_else(|| {
1649                OAuthClientError::BuildError(
1650                    "private_key_jwt selected without a client assertion signer".to_string(),
1651                )
1652            })?;
1653            let assertion = signer
1654                .sign_client_assertion(OAuthClientAssertionRequest {
1655                    client_id: registration.client_id().to_string(),
1656                    token_endpoint: token_endpoint.to_string(),
1657                    authorization_server_issuer: issuer.to_string(),
1658                })
1659                .await?;
1660            fields.push((
1661                "client_id".to_string(),
1662                registration.client_id().to_string(),
1663            ));
1664            fields.push((
1665                "client_assertion_type".to_string(),
1666                "urn:ietf:params:oauth:client-assertion-type:jwt-bearer".to_string(),
1667            ));
1668            fields.push(("client_assertion".to_string(), assertion));
1669        }
1670    }
1671    request.body = OAuthHttpBody::Form(fields);
1672    let response = http.execute(request).await?;
1673    if !response.is_success() {
1674        return Err(OAuthClientError::TokenRequest(format!(
1675            "token endpoint returned HTTP {}: {}",
1676            response.status,
1677            response.error_body()
1678        )));
1679    }
1680    Ok(response)
1681}
1682
1683fn token_from_response(
1684    response: OAuthHttpResponse,
1685    requested_scopes: &[String],
1686    previous_refresh_token: Option<String>,
1687) -> Result<OAuthStoredToken, OAuthClientError> {
1688    #[derive(serde::Deserialize)]
1689    struct TokenResponse {
1690        access_token: String,
1691        token_type: String,
1692        expires_in: Option<u64>,
1693        refresh_token: Option<String>,
1694        scope: Option<String>,
1695    }
1696    let response: TokenResponse = response.json()?;
1697    if !response.token_type.eq_ignore_ascii_case("bearer") {
1698        return Err(OAuthClientError::InvalidResponse(format!(
1699            "token endpoint returned unsupported token type `{}`",
1700            response.token_type
1701        )));
1702    }
1703    let scopes = response
1704        .scope
1705        .as_deref()
1706        .map(|scope| unique_scopes(scope.split_ascii_whitespace()))
1707        .unwrap_or_else(|| requested_scopes.to_vec());
1708    Ok(OAuthStoredToken {
1709        access_token: response.access_token,
1710        refresh_token: response.refresh_token.or(previous_refresh_token),
1711        expires_at: response
1712            .expires_in
1713            .map(|lifetime| unix_time().saturating_add(lifetime))
1714            .unwrap_or(u64::MAX),
1715        scopes,
1716    })
1717}
1718
1719fn token_is_valid(token: &OAuthStoredToken, buffer: Duration) -> bool {
1720    unix_time().saturating_add(buffer.as_secs()) < token.expires_at
1721}
1722
1723fn scopes_are_covered(requested: &[String], granted: &[String]) -> bool {
1724    requested
1725        .iter()
1726        .all(|scope| granted.iter().any(|granted| granted == scope))
1727}
1728
1729fn unix_time() -> u64 {
1730    SystemTime::now()
1731        .duration_since(UNIX_EPOCH)
1732        .unwrap_or_default()
1733        .as_secs()
1734}
1735
1736fn unique_scopes<I, S>(scopes: I) -> Vec<String>
1737where
1738    I: IntoIterator<Item = S>,
1739    S: AsRef<str>,
1740{
1741    let mut unique = Vec::new();
1742    for scope in scopes {
1743        for scope in scope.as_ref().split_ascii_whitespace() {
1744            if !scope.is_empty() && !unique.iter().any(|existing| existing == scope) {
1745                unique.push(scope.to_string());
1746            }
1747        }
1748    }
1749    unique
1750}
1751
1752fn random_urlsafe(bytes: usize) -> String {
1753    let mut value = vec![0_u8; bytes];
1754    getrandom::fill(&mut value).expect("getrandom failed");
1755    base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(value)
1756}
1757
1758fn pkce_challenge(verifier: &str) -> String {
1759    use sha2::{Digest, Sha256};
1760    base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(Sha256::digest(verifier.as_bytes()))
1761}
1762
1763async fn prepare_redirect(
1764    policy: &OAuthRedirectPolicy,
1765    _state: &str,
1766) -> Result<(String, Option<oneshot::Receiver<String>>), OAuthClientError> {
1767    match policy {
1768        OAuthRedirectPolicy::Fixed { redirect_uri } => Ok((redirect_uri.clone(), None)),
1769        OAuthRedirectPolicy::Loopback {
1770            port,
1771            callback_path,
1772        } => {
1773            let listener = tokio::net::TcpListener::bind(("127.0.0.1", port.unwrap_or(0)))
1774                .await
1775                .map_err(|error| OAuthClientError::Redirect(error.to_string()))?;
1776            let actual_port = listener
1777                .local_addr()
1778                .map_err(|error| OAuthClientError::Redirect(error.to_string()))?
1779                .port();
1780            let redirect_uri = format!("http://127.0.0.1:{actual_port}{callback_path}");
1781            let (sender, receiver) = oneshot::channel();
1782            let callback_base = format!("http://127.0.0.1:{actual_port}");
1783            tokio::spawn(run_loopback_callback(listener, sender, callback_base));
1784            Ok((redirect_uri, Some(receiver)))
1785        }
1786    }
1787}
1788
1789async fn run_loopback_callback(
1790    listener: tokio::net::TcpListener,
1791    mut sender: oneshot::Sender<String>,
1792    callback_base: String,
1793) {
1794    use tokio::io::{AsyncReadExt, AsyncWriteExt};
1795
1796    let accepted = tokio::select! {
1797        _ = sender.closed() => return,
1798        accepted = listener.accept() => accepted,
1799    };
1800    let Ok((mut stream, _)) = accepted else {
1801        return;
1802    };
1803    let mut bytes = vec![0_u8; 8192];
1804    let Ok(read) = stream.read(&mut bytes).await else {
1805        return;
1806    };
1807    let request = String::from_utf8_lossy(&bytes[..read]);
1808    let target = request
1809        .lines()
1810        .next()
1811        .and_then(|line| line.split_ascii_whitespace().nth(1));
1812    let (status, body) = if target.is_some() {
1813        ("200 OK", "Authorization received. You can close this tab.")
1814    } else {
1815        ("400 Bad Request", "Invalid OAuth callback.")
1816    };
1817    let response = format!(
1818        "HTTP/1.1 {status}\r\nContent-Type: text/plain\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
1819        body.len()
1820    );
1821    let _ = stream.write_all(response.as_bytes()).await;
1822    let _ = stream.flush().await;
1823    if let Some(target) = target {
1824        let _ = sender.send(format!("{callback_base}{target}"));
1825    }
1826}
1827
1828struct ParsedCallback {
1829    code: String,
1830    state: String,
1831    issuer: Option<String>,
1832}
1833
1834fn parse_callback_url(callback_url: &str) -> Result<ParsedCallback, OAuthClientError> {
1835    let url = reqwest::Url::parse(callback_url)
1836        .map_err(|error| OAuthClientError::InvalidResponse(error.to_string()))?;
1837    let mut code = None;
1838    let mut state = None;
1839    let mut issuer = None;
1840    let mut error = None;
1841    let mut error_description = None;
1842    for (name, value) in url.query_pairs() {
1843        match name.as_ref() {
1844            "code" => code = Some(value.into_owned()),
1845            "state" => state = Some(value.into_owned()),
1846            "iss" => issuer = Some(value.into_owned()),
1847            "error" => error = Some(value.into_owned()),
1848            "error_description" => error_description = Some(value.into_owned()),
1849            _ => {}
1850        }
1851    }
1852    if let Some(error) = error {
1853        return Err(OAuthClientError::InvalidResponse(format!(
1854            "authorization server returned `{error}`{}",
1855            error_description
1856                .map(|description| format!(": {description}"))
1857                .unwrap_or_default()
1858        )));
1859    }
1860    Ok(ParsedCallback {
1861        code: code.ok_or_else(|| {
1862            OAuthClientError::InvalidResponse("callback omitted authorization code".to_string())
1863        })?,
1864        state: state.ok_or_else(|| {
1865            OAuthClientError::InvalidResponse("callback omitted state".to_string())
1866        })?,
1867        issuer,
1868    })
1869}
1870
1871fn validate_callback_target(
1872    callback_url: &str,
1873    redirect_uri: &str,
1874) -> Result<(), OAuthClientError> {
1875    let callback = reqwest::Url::parse(callback_url)
1876        .map_err(|error| OAuthClientError::InvalidResponse(error.to_string()))?;
1877    let expected = reqwest::Url::parse(redirect_uri)
1878        .map_err(|error| OAuthClientError::InvalidResponse(error.to_string()))?;
1879    if callback.scheme() == expected.scheme()
1880        && callback.host_str() == expected.host_str()
1881        && callback.port_or_known_default() == expected.port_or_known_default()
1882        && callback.path() == expected.path()
1883    {
1884        Ok(())
1885    } else {
1886        Err(OAuthClientError::InvalidResponse(
1887            "callback URL does not match the configured redirect URI".to_string(),
1888        ))
1889    }
1890}
1891
1892fn validate_callback_issuer(
1893    actual: Option<&str>,
1894    expected: &str,
1895    required: bool,
1896) -> Result<(), OAuthClientError> {
1897    match actual {
1898        Some(actual) if actual == expected => Ok(()),
1899        Some(actual) => Err(OAuthClientError::InvalidResponse(format!(
1900            "authorization response issuer mismatch: expected `{expected}`, got `{actual}`"
1901        ))),
1902        None if required => Err(OAuthClientError::InvalidResponse(
1903            "authorization response omitted required `iss`".to_string(),
1904        )),
1905        None => Ok(()),
1906    }
1907}
1908
1909#[cfg(test)]
1910mod tests {
1911    use std::sync::atomic::{AtomicUsize, Ordering};
1912
1913    use super::*;
1914    use crate::client::OAuthScopeChallenge;
1915
1916    #[derive(Debug, Clone, Copy)]
1917    enum RegistrationMode {
1918        PreRegistered,
1919        Cimd,
1920        Dynamic,
1921        PrivateKeyJwt,
1922    }
1923
1924    #[derive(Clone)]
1925    struct MockOAuthHttp {
1926        mode: RegistrationMode,
1927        requests: Arc<Mutex<Vec<OAuthHttpRequest>>>,
1928        token_requests: Arc<AtomicUsize>,
1929        expire_initial_token: bool,
1930    }
1931
1932    impl MockOAuthHttp {
1933        fn new(mode: RegistrationMode) -> Self {
1934            Self {
1935                mode,
1936                requests: Arc::new(Mutex::new(Vec::new())),
1937                token_requests: Arc::new(AtomicUsize::new(0)),
1938                expire_initial_token: false,
1939            }
1940        }
1941
1942        fn expiring(mut self) -> Self {
1943            self.expire_initial_token = true;
1944            self
1945        }
1946
1947        async fn requests(&self) -> Vec<OAuthHttpRequest> {
1948            self.requests.lock().await.clone()
1949        }
1950
1951        fn response(status: u16, body: serde_json::Value) -> OAuthHttpResponse {
1952            OAuthHttpResponse {
1953                status,
1954                headers: Vec::new(),
1955                body: serde_json::to_vec(&body).unwrap(),
1956            }
1957        }
1958    }
1959
1960    #[async_trait]
1961    impl OAuthHttpClient for MockOAuthHttp {
1962        async fn execute(
1963            &self,
1964            request: OAuthHttpRequest,
1965        ) -> Result<OAuthHttpResponse, OAuthClientError> {
1966            self.requests.lock().await.push(request.clone());
1967            let path = reqwest::Url::parse(&request.url)
1968                .unwrap()
1969                .path()
1970                .to_string();
1971            match path.as_str() {
1972                "/mcp" => Ok(OAuthHttpResponse {
1973                    status: 401,
1974                    headers: vec![(
1975                        "www-authenticate".to_string(),
1976                        "Bearer resource_metadata=\"https://mcp.example.com/prm\", scope=\"challenge.scope\""
1977                            .to_string(),
1978                    )],
1979                    body: Vec::new(),
1980                }),
1981                "/prm" => Ok(Self::response(
1982                    200,
1983                    serde_json::json!({
1984                        "resource": "https://mcp.example.com/mcp",
1985                        "authorization_servers": ["https://auth.example.com/issuer"],
1986                        "scopes_supported": ["prm.scope"]
1987                    }),
1988                )),
1989                "/.well-known/oauth-authorization-server/issuer" => {
1990                    let (cimd, methods) = match self.mode {
1991                        RegistrationMode::PreRegistered => (false, vec!["client_secret_basic"]),
1992                        RegistrationMode::Cimd => (true, vec!["none"]),
1993                        RegistrationMode::Dynamic => (false, vec!["none"]),
1994                        RegistrationMode::PrivateKeyJwt => (false, vec!["private_key_jwt"]),
1995                    };
1996                    Ok(Self::response(
1997                        200,
1998                        serde_json::json!({
1999                            "issuer": "https://auth.example.com/issuer",
2000                            "authorization_endpoint": "https://auth.example.com/authorize",
2001                            "token_endpoint": "https://auth.example.com/token",
2002                            "registration_endpoint": "https://auth.example.com/register",
2003                            "client_id_metadata_document_supported": cimd,
2004                            "authorization_response_iss_parameter_supported": true,
2005                            "code_challenge_methods_supported": ["S256"],
2006                            "token_endpoint_auth_methods_supported": methods,
2007                            "grant_types_supported": ["authorization_code", "refresh_token"],
2008                            "scopes_supported": ["challenge.scope", "prm.scope", "extra.scope", "offline_access"]
2009                        }),
2010                    ))
2011                }
2012                "/register" => Ok(Self::response(
2013                    201,
2014                    serde_json::json!({ "client_id": "dynamic-client" }),
2015                )),
2016                "/token" => {
2017                    let request_number = self.token_requests.fetch_add(1, Ordering::SeqCst);
2018                    let fields = match &request.body {
2019                        OAuthHttpBody::Form(fields) => fields,
2020                        body => panic!("expected form token request, got {body:?}"),
2021                    };
2022                    let grant = fields
2023                        .iter()
2024                        .find(|(name, _)| name == "grant_type")
2025                        .map(|(_, value)| value.as_str())
2026                        .unwrap();
2027                    let scope = fields
2028                        .iter()
2029                        .find(|(name, _)| name == "scope")
2030                        .map(|(_, value)| value.clone())
2031                        .unwrap_or_else(|| {
2032                            if request_number == 0 {
2033                                "challenge.scope offline_access".to_string()
2034                            } else {
2035                                "challenge.scope offline_access extra.scope".to_string()
2036                            }
2037                        });
2038                    if grant == "refresh_token" {
2039                        Ok(Self::response(
2040                            200,
2041                            serde_json::json!({
2042                                "access_token": "refreshed-token",
2043                                "token_type": "Bearer",
2044                                "expires_in": 3600,
2045                                "scope": scope
2046                            }),
2047                        ))
2048                    } else {
2049                        Ok(Self::response(
2050                            200,
2051                            serde_json::json!({
2052                                "access_token": format!("access-token-{request_number}"),
2053                                "token_type": "Bearer",
2054                                "expires_in": if self.expire_initial_token { 0 } else { 3600 },
2055                                "refresh_token": "refresh-token",
2056                                "scope": scope
2057                            }),
2058                        ))
2059                    }
2060                }
2061                other => Err(OAuthClientError::Http(format!(
2062                    "unexpected mock request path `{other}`"
2063                ))),
2064            }
2065        }
2066    }
2067
2068    #[derive(Clone, Default)]
2069    struct AutomaticAuthorizationHandler {
2070        calls: Arc<AtomicUsize>,
2071    }
2072
2073    #[derive(Clone, Default)]
2074    struct TestAssertionSigner;
2075
2076    #[async_trait]
2077    impl OAuthClientAssertionSigner for TestAssertionSigner {
2078        async fn sign_client_assertion(
2079            &self,
2080            request: OAuthClientAssertionRequest,
2081        ) -> Result<String, OAuthClientError> {
2082            assert_eq!(request.client_id, "signed-client");
2083            assert_eq!(request.token_endpoint, "https://auth.example.com/token");
2084            assert_eq!(
2085                request.authorization_server_issuer,
2086                "https://auth.example.com/issuer"
2087            );
2088            Ok("signed-client-assertion".to_string())
2089        }
2090    }
2091
2092    #[async_trait]
2093    impl OAuthAuthorizationHandler for AutomaticAuthorizationHandler {
2094        async fn authorize(
2095            &self,
2096            request: OAuthAuthorizationRequest,
2097        ) -> Result<OAuthAuthorizationAction, OAuthClientError> {
2098            self.calls.fetch_add(1, Ordering::SeqCst);
2099            let authorization_url = reqwest::Url::parse(&request.authorization_url).unwrap();
2100            let state = authorization_url
2101                .query_pairs()
2102                .find(|(name, _)| name == "state")
2103                .unwrap()
2104                .1
2105                .into_owned();
2106            let mut callback = reqwest::Url::parse(&request.redirect_uri).unwrap();
2107            callback
2108                .query_pairs_mut()
2109                .append_pair("code", "authorization-code")
2110                .append_pair("state", &state)
2111                .append_pair("iss", &request.issuer);
2112            Ok(OAuthAuthorizationAction::CallbackUrl(callback.to_string()))
2113        }
2114    }
2115
2116    fn dynamic_options() -> OAuthClientRegistrationOptions {
2117        OAuthClientRegistrationOptions::new()
2118            .with_client_id_metadata_document("https://client.example.com/metadata.json")
2119            .with_dynamic_registration(
2120                super::super::oauth_authcode::OAuthDynamicClientRegistration::native(
2121                    "test-client",
2122                    std::iter::empty::<String>(),
2123                ),
2124            )
2125    }
2126
2127    fn flow_builder(
2128        http: MockOAuthHttp,
2129        options: OAuthClientRegistrationOptions,
2130        handler: AutomaticAuthorizationHandler,
2131    ) -> OAuthAuthorizationFlowBuilder {
2132        OAuthAuthorizationFlow::builder("https://mcp.example.com/mcp")
2133            .http_client(http)
2134            .redirect_policy(OAuthRedirectPolicy::fixed(
2135                "http://127.0.0.1:23456/callback",
2136            ))
2137            .registration_options(options)
2138            .authorization_handler(handler)
2139    }
2140
2141    #[tokio::test]
2142    async fn preregistered_flow_binds_resource_and_uses_basic_auth() {
2143        let http = MockOAuthHttp::new(RegistrationMode::PreRegistered);
2144        let flow = flow_builder(
2145            http.clone(),
2146            OAuthClientRegistrationOptions::new(),
2147            AutomaticAuthorizationHandler::default(),
2148        )
2149        .pre_registered_client("pre:client", Some("pre secret".to_string()))
2150        .build()
2151        .unwrap();
2152
2153        flow.authorize(std::iter::empty::<&str>()).await.unwrap();
2154        assert_eq!(flow.get_token().await.unwrap(), "access-token-0");
2155        assert_eq!(
2156            flow.authorized_scopes().await.unwrap(),
2157            vec!["challenge.scope", "offline_access"]
2158        );
2159
2160        let requests = http.requests().await;
2161        let probe = requests.first().unwrap();
2162        assert_eq!(probe.method, OAuthHttpMethod::Post);
2163        let OAuthHttpBody::Json(probe_body) = &probe.body else {
2164            panic!("expected JSON MCP probe")
2165        };
2166        assert_eq!(probe_body["method"], "tools/list");
2167        assert!(
2168            !requests
2169                .iter()
2170                .any(|request| request.url.ends_with("/register"))
2171        );
2172        let token_request = requests
2173            .iter()
2174            .find(|request| request.url.ends_with("/token"))
2175            .unwrap();
2176        assert!(token_request.headers.iter().any(|(name, value)| {
2177            name == "authorization"
2178                && value
2179                    == &format!(
2180                        "Basic {}",
2181                        base64::engine::general_purpose::STANDARD
2182                            .encode("pre%3Aclient:pre%20secret")
2183                    )
2184        }));
2185        let OAuthHttpBody::Form(fields) = &token_request.body else {
2186            panic!("expected token form")
2187        };
2188        assert!(fields.iter().any(|field| field
2189            == &(
2190                "resource".to_string(),
2191                "https://mcp.example.com/mcp".to_string()
2192            )));
2193    }
2194
2195    #[tokio::test]
2196    async fn private_key_jwt_is_used_when_advertised() {
2197        let http = MockOAuthHttp::new(RegistrationMode::PrivateKeyJwt);
2198        let options = OAuthClientRegistrationOptions::new().with_pre_registered(
2199            OAuthClientRegistration::pre_registered(
2200                "https://auth.example.com/issuer",
2201                "signed-client",
2202                None,
2203            ),
2204        );
2205        let flow = flow_builder(
2206            http.clone(),
2207            options,
2208            AutomaticAuthorizationHandler::default(),
2209        )
2210        .client_assertion_signer(TestAssertionSigner)
2211        .build()
2212        .unwrap();
2213
2214        flow.authorize(["challenge.scope"]).await.unwrap();
2215        let requests = http.requests().await;
2216        let token_request = requests
2217            .iter()
2218            .find(|request| request.url.ends_with("/token"))
2219            .unwrap();
2220        let OAuthHttpBody::Form(fields) = &token_request.body else {
2221            panic!("expected token form")
2222        };
2223        assert!(fields.iter().any(|field| field
2224            == &(
2225                "client_assertion_type".to_string(),
2226                "urn:ietf:params:oauth:client-assertion-type:jwt-bearer".to_string()
2227            )));
2228        assert!(fields.iter().any(|field| field
2229            == &(
2230                "client_assertion".to_string(),
2231                "signed-client-assertion".to_string()
2232            )));
2233    }
2234
2235    #[tokio::test]
2236    async fn cimd_takes_priority_over_dynamic_registration() {
2237        let http = MockOAuthHttp::new(RegistrationMode::Cimd);
2238        let flow = flow_builder(
2239            http.clone(),
2240            dynamic_options(),
2241            AutomaticAuthorizationHandler::default(),
2242        )
2243        .build()
2244        .unwrap();
2245
2246        flow.authorize(["explicit.scope"]).await.unwrap();
2247        let requests = http.requests().await;
2248        assert!(
2249            !requests
2250                .iter()
2251                .any(|request| request.url.ends_with("/register"))
2252        );
2253        let authorization = requests
2254            .iter()
2255            .find(|request| request.url.ends_with("/token"))
2256            .unwrap();
2257        let OAuthHttpBody::Form(fields) = &authorization.body else {
2258            panic!("expected form")
2259        };
2260        assert!(fields.iter().any(|field| field
2261            == &(
2262                "client_id".to_string(),
2263                "https://client.example.com/metadata.json".to_string()
2264            )));
2265        assert!(fields.iter().any(|field| field
2266            == &(
2267                "resource".to_string(),
2268                "https://mcp.example.com/mcp".to_string()
2269            )));
2270    }
2271
2272    #[tokio::test]
2273    async fn dynamic_registration_is_persisted_and_reused() {
2274        let http = MockOAuthHttp::new(RegistrationMode::Dynamic);
2275        let registrations = super::super::oauth_authcode::MemoryOAuthClientRegistrationStore::new();
2276
2277        for _ in 0..2 {
2278            let flow = flow_builder(
2279                http.clone(),
2280                dynamic_options(),
2281                AutomaticAuthorizationHandler::default(),
2282            )
2283            .registration_store(registrations.clone())
2284            .build()
2285            .unwrap();
2286            flow.authorize(["challenge.scope"]).await.unwrap();
2287        }
2288
2289        let requests = http.requests().await;
2290        assert_eq!(
2291            requests
2292                .iter()
2293                .filter(|request| request.url.ends_with("/register"))
2294                .count(),
2295            1
2296        );
2297        let registration_request = requests
2298            .iter()
2299            .find(|request| request.url.ends_with("/register"))
2300            .unwrap();
2301        let OAuthHttpBody::Json(value) = &registration_request.body else {
2302            panic!("expected registration JSON")
2303        };
2304        assert_eq!(value["redirect_uris"][0], "http://127.0.0.1:23456/callback");
2305    }
2306
2307    #[tokio::test]
2308    async fn expired_token_refreshes_and_preserves_binding() {
2309        let http = MockOAuthHttp::new(RegistrationMode::Dynamic).expiring();
2310        let flow = flow_builder(
2311            http.clone(),
2312            dynamic_options(),
2313            AutomaticAuthorizationHandler::default(),
2314        )
2315        .refresh_buffer(Duration::ZERO)
2316        .build()
2317        .unwrap();
2318
2319        flow.authorize(["challenge.scope"]).await.unwrap();
2320        assert_eq!(flow.get_token().await.unwrap(), "refreshed-token");
2321        let requests = http.requests().await;
2322        let refresh = requests
2323            .iter()
2324            .rfind(|request| request.url.ends_with("/token"))
2325            .unwrap();
2326        let OAuthHttpBody::Form(fields) = &refresh.body else {
2327            panic!("expected refresh form")
2328        };
2329        assert!(
2330            fields
2331                .iter()
2332                .any(|field| field == &("grant_type".to_string(), "refresh_token".to_string()))
2333        );
2334        assert!(fields.iter().any(|field| field
2335            == &(
2336                "resource".to_string(),
2337                "https://mcp.example.com/mcp".to_string()
2338            )));
2339    }
2340
2341    #[tokio::test]
2342    async fn expired_persisted_token_refreshes_after_flow_rebuild() {
2343        let http = MockOAuthHttp::new(RegistrationMode::Dynamic).expiring();
2344        let tokens = MemoryOAuthTokenStore::new();
2345        let registrations = super::super::oauth_authcode::MemoryOAuthClientRegistrationStore::new();
2346        let first_handler = AutomaticAuthorizationHandler::default();
2347
2348        let first = flow_builder(http.clone(), dynamic_options(), first_handler)
2349            .registration_store(registrations.clone())
2350            .token_store(tokens.clone())
2351            .refresh_buffer(Duration::ZERO)
2352            .build()
2353            .unwrap();
2354        first.authorize(["challenge.scope"]).await.unwrap();
2355
2356        let restored_handler = AutomaticAuthorizationHandler::default();
2357        let restored_calls = restored_handler.calls.clone();
2358        let restored = flow_builder(http, dynamic_options(), restored_handler)
2359            .registration_store(registrations)
2360            .token_store(tokens)
2361            .refresh_buffer(Duration::ZERO)
2362            .build()
2363            .unwrap();
2364
2365        let start = restored.begin(["challenge.scope"]).await.unwrap();
2366        assert!(matches!(start, OAuthAuthorizationStart::Authorized { .. }));
2367        assert_eq!(restored_calls.load(Ordering::SeqCst), 0);
2368        assert_eq!(restored.get_token().await.unwrap(), "refreshed-token");
2369    }
2370
2371    #[tokio::test]
2372    async fn dropped_loopback_attempt_releases_its_listener() {
2373        let probe = tokio::net::TcpListener::bind(("127.0.0.1", 0))
2374            .await
2375            .unwrap();
2376        let port = probe.local_addr().unwrap().port();
2377        drop(probe);
2378
2379        let (_, receiver) = prepare_redirect(
2380            &OAuthRedirectPolicy::loopback_at(port, "/callback"),
2381            "state",
2382        )
2383        .await
2384        .unwrap();
2385        drop(receiver);
2386
2387        tokio::time::timeout(Duration::from_secs(1), async {
2388            loop {
2389                match tokio::net::TcpListener::bind(("127.0.0.1", port)).await {
2390                    Ok(listener) => break drop(listener),
2391                    Err(_) => tokio::task::yield_now().await,
2392                }
2393            }
2394        })
2395        .await
2396        .expect("dropped OAuth attempt kept its loopback port bound");
2397    }
2398
2399    #[tokio::test]
2400    async fn scope_escalation_reauthorizes_same_provider() {
2401        let http = MockOAuthHttp::new(RegistrationMode::Dynamic);
2402        let handler = AutomaticAuthorizationHandler::default();
2403        let calls = handler.calls.clone();
2404        let flow = flow_builder(http, dynamic_options(), handler)
2405            .build()
2406            .unwrap();
2407        flow.authorize(std::iter::empty::<&str>()).await.unwrap();
2408
2409        flow.reauthorize(OAuthScopeEscalationRequest {
2410            resource: "https://mcp.example.com/mcp".to_string(),
2411            operation: "tools/call:admin".to_string(),
2412            challenge: OAuthScopeChallenge {
2413                required_scopes: vec!["extra.scope".to_string()],
2414                resource_metadata: Some("https://mcp.example.com/prm".to_string()),
2415                error_description: None,
2416            },
2417            previous_scopes: vec!["challenge.scope".to_string(), "offline_access".to_string()],
2418            requested_scopes: vec![
2419                "challenge.scope".to_string(),
2420                "offline_access".to_string(),
2421                "extra.scope".to_string(),
2422            ],
2423            attempt: 1,
2424        })
2425        .await
2426        .unwrap();
2427
2428        assert_eq!(calls.load(Ordering::SeqCst), 2);
2429        assert_eq!(
2430            flow.authorized_scopes().await.unwrap(),
2431            vec!["challenge.scope", "offline_access", "extra.scope"]
2432        );
2433        assert_eq!(flow.get_token().await.unwrap(), "access-token-1");
2434    }
2435
2436    #[tokio::test]
2437    async fn persisted_pkce_state_can_complete_after_flow_rebuild() {
2438        let http = MockOAuthHttp::new(RegistrationMode::Dynamic);
2439        let state_store = MemoryOAuthAuthorizationStateStore::new();
2440        let token_store = MemoryOAuthTokenStore::new();
2441        let registrations = super::super::oauth_authcode::MemoryOAuthClientRegistrationStore::new();
2442        let first = flow_builder(
2443            http.clone(),
2444            dynamic_options(),
2445            AutomaticAuthorizationHandler::default(),
2446        )
2447        .state_store(state_store.clone())
2448        .token_store(token_store.clone())
2449        .registration_store(registrations.clone())
2450        .build()
2451        .unwrap();
2452        let OAuthAuthorizationStart::Pending(pending) = first.begin(["prm.scope"]).await.unwrap()
2453        else {
2454            panic!("expected pending flow")
2455        };
2456        let request = pending.request().clone();
2457        let authorization_url = reqwest::Url::parse(&request.authorization_url).unwrap();
2458        let state = authorization_url
2459            .query_pairs()
2460            .find(|(name, _)| name == "state")
2461            .unwrap()
2462            .1
2463            .into_owned();
2464        drop(pending);
2465
2466        let second = flow_builder(
2467            http,
2468            dynamic_options(),
2469            AutomaticAuthorizationHandler::default(),
2470        )
2471        .state_store(state_store)
2472        .token_store(token_store)
2473        .registration_store(registrations)
2474        .build()
2475        .unwrap();
2476        let mut callback = reqwest::Url::parse(&request.redirect_uri).unwrap();
2477        callback
2478            .query_pairs_mut()
2479            .append_pair("code", "persisted-code")
2480            .append_pair("state", &state)
2481            .append_pair("iss", &request.issuer);
2482        second
2483            .complete_callback_url(callback.as_str())
2484            .await
2485            .unwrap();
2486        assert_eq!(second.get_token().await.unwrap(), "access-token-0");
2487    }
2488
2489    #[test]
2490    fn token_without_lifetime_remains_valid_until_the_server_rejects_it() {
2491        let token = token_from_response(
2492            MockOAuthHttp::response(
2493                200,
2494                serde_json::json!({
2495                    "access_token": "access-token",
2496                    "token_type": "Bearer"
2497                }),
2498            ),
2499            &["read".to_string()],
2500            None,
2501        )
2502        .unwrap();
2503
2504        assert_eq!(token.expires_at, u64::MAX);
2505        assert!(token_is_valid(&token, Duration::from_secs(30)));
2506    }
2507
2508    #[test]
2509    fn token_response_rejects_non_bearer_token_types() {
2510        let error = token_from_response(
2511            MockOAuthHttp::response(
2512                200,
2513                serde_json::json!({
2514                    "access_token": "access-token",
2515                    "token_type": "DPoP"
2516                }),
2517            ),
2518            &[],
2519            None,
2520        )
2521        .unwrap_err();
2522
2523        assert!(error.to_string().contains("unsupported token type `DPoP`"));
2524    }
2525
2526    #[test]
2527    fn scope_selection_falls_back_from_challenge_to_resource_metadata() {
2528        let metadata: OAuthAuthorizationServerMetadata =
2529            serde_json::from_value(serde_json::json!({
2530                "issuer": "https://auth.example.com/issuer",
2531                "authorization_endpoint": "https://auth.example.com/authorize",
2532                "token_endpoint": "https://auth.example.com/token",
2533                "grant_types_supported": ["authorization_code"],
2534                "scopes_supported": ["as.scope"]
2535            }))
2536            .unwrap();
2537        let protected_resource: OAuthProtectedResourceMetadata =
2538            serde_json::from_value(serde_json::json!({
2539                "resource": "https://mcp.example.com/mcp",
2540                "authorization_servers": ["https://auth.example.com/issuer"],
2541                "scopes_supported": ["prm.scope"]
2542            }))
2543            .unwrap();
2544        let mut discovery = FlowDiscovery {
2545            resource: protected_resource.resource.clone(),
2546            protected_resource,
2547            authorization_servers: vec![metadata.clone()],
2548            challenge: None,
2549        };
2550
2551        assert_eq!(select_scopes(&[], &discovery, &metadata), ["prm.scope"]);
2552        discovery.challenge = Some(OAuthBearerChallenge {
2553            error: None,
2554            scopes: vec!["challenge.scope".to_string()],
2555            resource_metadata: None,
2556            error_description: None,
2557        });
2558        assert_eq!(
2559            select_scopes(&[], &discovery, &metadata),
2560            ["challenge.scope"]
2561        );
2562        assert_eq!(
2563            select_scopes(&["explicit.scope".to_string()], &discovery, &metadata),
2564            ["explicit.scope"]
2565        );
2566    }
2567
2568    #[test]
2569    fn resource_identifier_may_be_a_canonical_parent_on_the_same_origin() {
2570        assert!(
2571            validate_resource_identifier("https://mcp.example.com/mcp", "https://mcp.example.com")
2572                .is_ok()
2573        );
2574        assert!(
2575            validate_resource_identifier(
2576                "https://mcp.example.com/tenant/mcp",
2577                "https://mcp.example.com/tenant"
2578            )
2579            .is_ok()
2580        );
2581        assert!(
2582            validate_resource_identifier(
2583                "https://mcp.example.com/tenant-evil/mcp",
2584                "https://mcp.example.com/tenant"
2585            )
2586            .is_err()
2587        );
2588        assert!(
2589            validate_resource_identifier(
2590                "https://other.example.com/mcp",
2591                "https://mcp.example.com"
2592            )
2593            .is_err()
2594        );
2595    }
2596}