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            && token_is_valid(&token, self.inner.refresh_buffer)
999            && scopes_are_covered(&scopes, &token.scopes)
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            return Ok(OAuthAuthorizationStart::Authorized {
1009                scopes: token.scopes,
1010            });
1011        }
1012
1013        let pending_state = OAuthPendingAuthorizationState {
1014            state: state.clone(),
1015            code_verifier,
1016            redirect_uri: redirect_uri.clone(),
1017            resource: discovery.resource.clone(),
1018            issuer: metadata.issuer.clone(),
1019            token_endpoint: metadata.token_endpoint.clone(),
1020            registration: registration.clone(),
1021            token_endpoint_auth_method: auth_method,
1022            scopes: scopes.clone(),
1023            iss_required: metadata.authorization_response_iss_parameter_supported,
1024        };
1025        self.inner.state_store.save(&pending_state).await?;
1026
1027        let mut authorization_url = reqwest::Url::parse(&metadata.authorization_endpoint)
1028            .map_err(|error| OAuthClientError::Discovery(error.to_string()))?;
1029        {
1030            let mut query = authorization_url.query_pairs_mut();
1031            query
1032                .append_pair("response_type", "code")
1033                .append_pair("client_id", registration.client_id())
1034                .append_pair("redirect_uri", &redirect_uri)
1035                .append_pair("state", &state)
1036                .append_pair("code_challenge", &code_challenge)
1037                .append_pair("code_challenge_method", "S256")
1038                .append_pair("resource", &discovery.resource);
1039            if !scopes.is_empty() {
1040                query.append_pair("scope", &scopes.join(" "));
1041            }
1042        }
1043        Ok(OAuthAuthorizationStart::Pending(
1044            OAuthPendingAuthorization {
1045                flow: self.clone(),
1046                request: OAuthAuthorizationRequest {
1047                    authorization_url: authorization_url.to_string(),
1048                    redirect_uri,
1049                    resource: discovery.resource,
1050                    issuer: metadata.issuer,
1051                    scopes,
1052                },
1053                state,
1054                callback_rx: Mutex::new(callback_rx),
1055            },
1056        ))
1057    }
1058
1059    /// Resume a persisted authorization attempt from a callback URL.
1060    pub async fn complete_callback_url(&self, callback_url: &str) -> Result<(), OAuthClientError> {
1061        self.complete_callback_url_for_state(callback_url, None)
1062            .await
1063    }
1064
1065    async fn complete_callback_url_for_state(
1066        &self,
1067        callback_url: &str,
1068        expected_state: Option<&str>,
1069    ) -> Result<(), OAuthClientError> {
1070        let parsed = parse_callback_url(callback_url)?;
1071        if let Some(expected) = expected_state
1072            && parsed.state != expected
1073        {
1074            return Err(OAuthClientError::InvalidResponse(
1075                "OAuth callback state mismatch".to_string(),
1076            ));
1077        }
1078        let pending = self
1079            .inner
1080            .state_store
1081            .load(&parsed.state)
1082            .await?
1083            .ok_or_else(|| {
1084                OAuthClientError::StateStore(
1085                    "no persisted OAuth authorization matches the callback state".to_string(),
1086                )
1087            })?;
1088        validate_callback_target(callback_url, &pending.redirect_uri)?;
1089        validate_callback_issuer(
1090            parsed.issuer.as_deref(),
1091            &pending.issuer,
1092            pending.iss_required,
1093        )?;
1094
1095        let fields = vec![
1096            ("grant_type".to_string(), "authorization_code".to_string()),
1097            ("code".to_string(), parsed.code),
1098            ("redirect_uri".to_string(), pending.redirect_uri.clone()),
1099            ("code_verifier".to_string(), pending.code_verifier.clone()),
1100            ("resource".to_string(), pending.resource.clone()),
1101        ];
1102        let response = send_token_request(
1103            self.inner.http.as_ref(),
1104            &pending.token_endpoint,
1105            &pending.issuer,
1106            &pending.registration,
1107            pending.token_endpoint_auth_method,
1108            fields,
1109            self.inner.assertion_signer.as_deref(),
1110        )
1111        .await?;
1112        let token = token_from_response(response, &pending.scopes, None)?;
1113        let binding = OAuthTokenBinding {
1114            resource: pending.resource.clone(),
1115            issuer: pending.issuer.clone(),
1116            client_id: pending.registration.client_id().to_string(),
1117        };
1118        self.inner.token_store.save(&binding, &token).await?;
1119        self.inner.state_store.remove(&parsed.state).await?;
1120        *self.inner.current.write().await = Some(ActiveToken {
1121            binding,
1122            token,
1123            token_endpoint: pending.token_endpoint,
1124            registration: pending.registration,
1125            auth_method: pending.token_endpoint_auth_method,
1126        });
1127        Ok(())
1128    }
1129
1130    /// Return the currently authorized scope set, if any.
1131    pub async fn authorized_scopes(&self) -> Option<Vec<String>> {
1132        self.inner
1133            .current
1134            .read()
1135            .await
1136            .as_ref()
1137            .map(|active| active.token.scopes.clone())
1138    }
1139}
1140
1141#[async_trait]
1142impl TokenProvider for OAuthAuthorizationFlow {
1143    async fn get_token(&self) -> Result<String, OAuthClientError> {
1144        if let Some(active) = self.inner.current.read().await.as_ref()
1145            && token_is_valid(&active.token, self.inner.refresh_buffer)
1146        {
1147            return Ok(active.token.access_token.clone());
1148        }
1149
1150        let _guard = self.inner.refresh_lock.lock().await;
1151        let active = self.inner.current.read().await.clone().ok_or_else(|| {
1152            OAuthClientError::TokenRequest(
1153                "OAuthAuthorizationFlow is not authorized; call authorize() or begin()".to_string(),
1154            )
1155        })?;
1156        if token_is_valid(&active.token, self.inner.refresh_buffer) {
1157            return Ok(active.token.access_token);
1158        }
1159        let refresh_token = active.token.refresh_token.as_deref().ok_or_else(|| {
1160            OAuthClientError::TokenRequest(
1161                "OAuth access token expired and no refresh token is available".to_string(),
1162            )
1163        })?;
1164        let mut fields = vec![
1165            ("grant_type".to_string(), "refresh_token".to_string()),
1166            ("refresh_token".to_string(), refresh_token.to_string()),
1167            ("resource".to_string(), active.binding.resource.clone()),
1168        ];
1169        if !active.token.scopes.is_empty() {
1170            fields.push(("scope".to_string(), active.token.scopes.join(" ")));
1171        }
1172        let response = send_token_request(
1173            self.inner.http.as_ref(),
1174            &active.token_endpoint,
1175            &active.binding.issuer,
1176            &active.registration,
1177            active.auth_method,
1178            fields,
1179            self.inner.assertion_signer.as_deref(),
1180        )
1181        .await?;
1182        let token = token_from_response(
1183            response,
1184            &active.token.scopes,
1185            active.token.refresh_token.clone(),
1186        )?;
1187        self.inner.token_store.save(&active.binding, &token).await?;
1188        let access_token = token.access_token.clone();
1189        *self.inner.current.write().await = Some(ActiveToken { token, ..active });
1190        Ok(access_token)
1191    }
1192}
1193
1194#[async_trait]
1195impl OAuthScopeEscalationHandler for OAuthAuthorizationFlow {
1196    async fn reauthorize(
1197        &self,
1198        request: OAuthScopeEscalationRequest,
1199    ) -> Result<(), OAuthClientError> {
1200        if request.resource != self.inner.resource_url {
1201            return Err(OAuthClientError::ScopeEscalation(format!(
1202                "scope challenge resource `{}` does not match flow resource `{}`",
1203                request.resource, self.inner.resource_url
1204            )));
1205        }
1206        let challenge = OAuthBearerChallenge {
1207            error: Some("insufficient_scope".to_string()),
1208            scopes: request.challenge.required_scopes,
1209            resource_metadata: request.challenge.resource_metadata,
1210            error_description: request.challenge.error_description,
1211        };
1212        self.authorize_with_challenge(request.requested_scopes, Some(challenge))
1213            .await
1214            .map_err(|error| OAuthClientError::ScopeEscalation(error.to_string()))
1215    }
1216}
1217
1218#[derive(Debug)]
1219struct FlowDiscovery {
1220    resource: String,
1221    protected_resource: OAuthProtectedResourceMetadata,
1222    authorization_servers: Vec<OAuthAuthorizationServerMetadata>,
1223    challenge: Option<OAuthBearerChallenge>,
1224}
1225
1226async fn discover_with_http(
1227    resource_url: &str,
1228    challenge: Option<OAuthBearerChallenge>,
1229    http: &dyn OAuthHttpClient,
1230) -> Result<FlowDiscovery, OAuthClientError> {
1231    let challenge = match challenge {
1232        Some(challenge) => Some(challenge),
1233        None => {
1234            // Probe a protected MCP operation rather than `initialize`, which
1235            // servers commonly leave public. A POST also works with MCP
1236            // endpoints that do not implement GET and therefore expose their
1237            // RFC 9728 discovery hint only on normal protocol requests.
1238            let mut request = OAuthHttpRequest::post_json(
1239                resource_url,
1240                serde_json::json!({
1241                    "jsonrpc": "2.0",
1242                    "id": "tower-mcp-oauth-discovery",
1243                    "method": "tools/list",
1244                    "params": {}
1245                }),
1246            );
1247            request.headers.push((
1248                "accept".to_string(),
1249                "application/json, text/event-stream".to_string(),
1250            ));
1251            let response = http.execute(request).await?;
1252            response
1253                .header_values("www-authenticate")
1254                .find_map(OAuthBearerChallenge::from_www_authenticate)
1255        }
1256    };
1257    let protected_resource = if let Some(url) = challenge
1258        .as_ref()
1259        .and_then(|challenge| challenge.resource_metadata.as_deref())
1260    {
1261        fetch_json(http, url).await?
1262    } else {
1263        let mut discovered: Option<OAuthProtectedResourceMetadata> = None;
1264        for url in protected_resource_metadata_urls(resource_url)? {
1265            if let Ok(metadata) = fetch_json(http, &url).await {
1266                discovered = Some(metadata);
1267                break;
1268            }
1269        }
1270        discovered.ok_or_else(|| {
1271            OAuthClientError::Discovery(format!(
1272                "could not discover Protected Resource Metadata for `{resource_url}`"
1273            ))
1274        })?
1275    };
1276    validate_resource_identifier(resource_url, &protected_resource.resource)?;
1277    if protected_resource.authorization_servers.is_empty() {
1278        return Err(OAuthClientError::Discovery(
1279            "protected resource metadata omitted authorization_servers".to_string(),
1280        ));
1281    }
1282
1283    let mut authorization_servers = Vec::new();
1284    let mut last_error = None;
1285    for issuer in &protected_resource.authorization_servers {
1286        match discover_authorization_server(http, issuer).await {
1287            Ok(metadata) => authorization_servers.push(metadata),
1288            Err(error) => last_error = Some(error),
1289        }
1290    }
1291    if authorization_servers.is_empty() {
1292        return Err(last_error.unwrap_or_else(|| {
1293            OAuthClientError::Discovery(
1294                "protected resource advertised no usable authorization server".to_string(),
1295            )
1296        }));
1297    }
1298    Ok(FlowDiscovery {
1299        resource: protected_resource.resource.clone(),
1300        protected_resource,
1301        authorization_servers,
1302        challenge,
1303    })
1304}
1305
1306async fn discover_authorization_server(
1307    http: &dyn OAuthHttpClient,
1308    issuer: &str,
1309) -> Result<OAuthAuthorizationServerMetadata, OAuthClientError> {
1310    let mut last_error = None;
1311    for url in authorization_server_metadata_urls(issuer)? {
1312        match fetch_json::<OAuthAuthorizationServerMetadata>(http, &url).await {
1313            Ok(metadata) if metadata.issuer == issuer => return Ok(metadata),
1314            Ok(metadata) => {
1315                last_error = Some(OAuthClientError::Discovery(format!(
1316                    "authorization server issuer mismatch: expected `{issuer}`, got `{}`",
1317                    metadata.issuer
1318                )))
1319            }
1320            Err(error) => last_error = Some(error),
1321        }
1322    }
1323    Err(last_error.unwrap_or_else(|| {
1324        OAuthClientError::Discovery(format!(
1325            "could not discover authorization server metadata for `{issuer}`"
1326        ))
1327    }))
1328}
1329
1330async fn fetch_json<T: serde::de::DeserializeOwned>(
1331    http: &dyn OAuthHttpClient,
1332    url: &str,
1333) -> Result<T, OAuthClientError> {
1334    let response = http.execute(OAuthHttpRequest::get(url)).await?;
1335    if !response.is_success() {
1336        return Err(OAuthClientError::Discovery(format!(
1337            "GET `{url}` returned HTTP {}: {}",
1338            response.status,
1339            response.error_body()
1340        )));
1341    }
1342    response.json()
1343}
1344
1345fn protected_resource_metadata_urls(resource_url: &str) -> Result<Vec<String>, OAuthClientError> {
1346    let parsed = reqwest::Url::parse(resource_url)
1347        .map_err(|error| OAuthClientError::Discovery(error.to_string()))?;
1348    let origin = parsed.origin().ascii_serialization();
1349    let path = parsed.path();
1350    let mut urls = Vec::new();
1351    if !path.is_empty() && path != "/" {
1352        urls.push(format!(
1353            "{origin}/.well-known/oauth-protected-resource{path}"
1354        ));
1355    }
1356    urls.push(format!("{origin}/.well-known/oauth-protected-resource"));
1357    urls.dedup();
1358    Ok(urls)
1359}
1360
1361fn authorization_server_metadata_urls(issuer: &str) -> Result<Vec<String>, OAuthClientError> {
1362    let parsed = reqwest::Url::parse(issuer)
1363        .map_err(|error| OAuthClientError::Discovery(error.to_string()))?;
1364    let origin = parsed.origin().ascii_serialization();
1365    let path = parsed.path();
1366    let trimmed = issuer.trim_end_matches('/');
1367    let mut urls = Vec::new();
1368    if !path.is_empty() && path != "/" {
1369        urls.push(format!(
1370            "{origin}/.well-known/oauth-authorization-server{path}"
1371        ));
1372        urls.push(format!("{origin}/.well-known/openid-configuration{path}"));
1373        urls.push(format!("{trimmed}/.well-known/openid-configuration"));
1374    } else {
1375        urls.push(format!("{origin}/.well-known/oauth-authorization-server"));
1376        urls.push(format!("{origin}/.well-known/openid-configuration"));
1377    }
1378    urls.dedup();
1379    Ok(urls)
1380}
1381
1382fn validate_resource_identifier(expected: &str, actual: &str) -> Result<(), OAuthClientError> {
1383    let expected = reqwest::Url::parse(expected)
1384        .map_err(|error| OAuthClientError::Discovery(error.to_string()))?;
1385    let actual = reqwest::Url::parse(actual)
1386        .map_err(|error| OAuthClientError::Discovery(error.to_string()))?;
1387    // An origin-level canonical resource may cover a more specific MCP
1388    // endpoint on that origin (for example resource `https://example.com`
1389    // with endpoint `https://example.com/mcp`). A non-root resource path must
1390    // be an exact path-segment prefix, never a merely textual prefix.
1391    let expected_path = expected.path();
1392    let actual_path = actual.path();
1393    let path_matches = actual_path == "/"
1394        || expected_path == actual_path
1395        || (actual_path.ends_with('/') && expected_path.starts_with(actual_path))
1396        || expected_path
1397            .strip_prefix(actual_path)
1398            .is_some_and(|suffix| suffix.starts_with('/'));
1399    let query_matches = actual.query().is_none() || actual.query() == expected.query();
1400    let matches = actual.fragment().is_none()
1401        && expected.scheme() == actual.scheme()
1402        && expected.host_str() == actual.host_str()
1403        && expected.port_or_known_default() == actual.port_or_known_default()
1404        && path_matches
1405        && query_matches;
1406    if matches {
1407        Ok(())
1408    } else {
1409        Err(OAuthClientError::Discovery(format!(
1410            "protected resource mismatch: expected `{expected}`, got `{actual}`"
1411        )))
1412    }
1413}
1414
1415fn select_authorization_server(
1416    discovery: &FlowDiscovery,
1417    preferred_issuer: Option<&str>,
1418) -> Result<OAuthAuthorizationServerMetadata, OAuthClientError> {
1419    match preferred_issuer {
1420        Some(issuer) => discovery
1421            .authorization_servers
1422            .iter()
1423            .find(|metadata| metadata.issuer == issuer)
1424            .cloned()
1425            .ok_or_else(|| {
1426                OAuthClientError::Discovery(format!(
1427                    "preferred authorization server `{issuer}` was not advertised"
1428                ))
1429            }),
1430        None => discovery
1431            .authorization_servers
1432            .first()
1433            .cloned()
1434            .ok_or_else(|| OAuthClientError::Discovery("no authorization server found".into())),
1435    }
1436}
1437
1438fn require_s256(metadata: &OAuthAuthorizationServerMetadata) -> Result<(), OAuthClientError> {
1439    if metadata
1440        .code_challenge_methods_supported
1441        .iter()
1442        .any(|method| method == "S256")
1443    {
1444        Ok(())
1445    } else {
1446        Err(OAuthClientError::Discovery(format!(
1447            "authorization server `{}` does not advertise PKCE S256",
1448            metadata.issuer
1449        )))
1450    }
1451}
1452
1453fn select_scopes(
1454    explicit: &[String],
1455    discovery: &FlowDiscovery,
1456    metadata: &OAuthAuthorizationServerMetadata,
1457) -> Vec<String> {
1458    let selected = if !explicit.is_empty() {
1459        explicit.to_vec()
1460    } else if let Some(challenge) = &discovery.challenge
1461        && !challenge.scopes.is_empty()
1462    {
1463        challenge.scopes.clone()
1464    } else if !discovery.protected_resource.scopes_supported.is_empty() {
1465        discovery.protected_resource.scopes_supported.clone()
1466    } else {
1467        metadata.scopes_supported.clone()
1468    };
1469    let mut selected = unique_scopes(selected);
1470    let refresh_supported = metadata.grant_types_supported.is_empty()
1471        || metadata
1472            .grant_types_supported
1473            .iter()
1474            .any(|grant| grant == "refresh_token");
1475    if refresh_supported
1476        && metadata
1477            .scopes_supported
1478            .iter()
1479            .any(|scope| scope == "offline_access")
1480        && !selected.iter().any(|scope| scope == "offline_access")
1481    {
1482        selected.push("offline_access".to_string());
1483    }
1484    selected
1485}
1486
1487fn preferred_registration_auth_method(
1488    advertised: &[String],
1489    has_private_key_signer: bool,
1490) -> &'static str {
1491    if has_private_key_signer && advertised.iter().any(|method| method == "private_key_jwt") {
1492        "private_key_jwt"
1493    } else if advertised
1494        .iter()
1495        .any(|method| method == "client_secret_basic")
1496    {
1497        "client_secret_basic"
1498    } else if advertised
1499        .iter()
1500        .any(|method| method == "client_secret_post")
1501    {
1502        "client_secret_post"
1503    } else {
1504        "none"
1505    }
1506}
1507
1508async fn resolve_registration_with_http(
1509    http: &dyn OAuthHttpClient,
1510    metadata: &OAuthAuthorizationServerMetadata,
1511    options: &OAuthClientRegistrationOptions,
1512    store: &dyn OAuthClientRegistrationStore,
1513) -> Result<OAuthClientRegistration, OAuthClientError> {
1514    if let Some(registration) = &options.pre_registered {
1515        if registration.method() != OAuthClientRegistrationMethod::PreRegistered {
1516            return Err(OAuthClientError::BuildError(
1517                "pre_registered must contain pre-registered credentials".to_string(),
1518            ));
1519        }
1520        if registration.bound_issuer() != Some(metadata.issuer.as_str()) {
1521            return Err(OAuthClientError::BuildError(format!(
1522                "pre-registered credentials are bound to issuer {:?}, not `{}`",
1523                registration.bound_issuer(),
1524                metadata.issuer
1525            )));
1526        }
1527        return Ok(registration.clone());
1528    }
1529
1530    if metadata.client_id_metadata_document_supported
1531        && let Some(client_id) = &options.client_id_metadata_document
1532    {
1533        validate_cimd_url(client_id)?;
1534        return Ok(OAuthClientRegistration::client_id_metadata_document(
1535            client_id.clone(),
1536        ));
1537    }
1538
1539    if options.dynamic_registration.is_some()
1540        && let Some(registration) = store.load(&metadata.issuer).await?
1541    {
1542        if registration.method() != OAuthClientRegistrationMethod::Dynamic
1543            || registration.bound_issuer() != Some(metadata.issuer.as_str())
1544        {
1545            return Err(OAuthClientError::CredentialStore(format!(
1546                "stored registration is not dynamically bound to `{}`",
1547                metadata.issuer
1548            )));
1549        }
1550        return Ok(registration);
1551    }
1552
1553    if let (Some(endpoint), Some(request)) = (
1554        metadata.registration_endpoint.as_deref(),
1555        options.dynamic_registration.as_ref(),
1556    ) {
1557        if request.redirect_uris.is_empty() {
1558            return Err(OAuthClientError::BuildError(
1559                "dynamic registration requires a redirect URI".to_string(),
1560            ));
1561        }
1562        let value = serde_json::to_value(request)
1563            .map_err(|error| OAuthClientError::Registration(error.to_string()))?;
1564        let response = http
1565            .execute(OAuthHttpRequest::post_json(endpoint, value))
1566            .await?;
1567        if !response.is_success() {
1568            return Err(OAuthClientError::Registration(format!(
1569                "dynamic registration returned HTTP {}: {}",
1570                response.status,
1571                response.error_body()
1572            )));
1573        }
1574        #[derive(serde::Deserialize)]
1575        struct RegistrationResponse {
1576            client_id: String,
1577            client_secret: Option<String>,
1578        }
1579        let registered: RegistrationResponse = response.json()?;
1580        let registration = OAuthClientRegistration::dynamically_registered(
1581            metadata.issuer.clone(),
1582            registered.client_id,
1583            registered.client_secret,
1584        );
1585        store.save(&metadata.issuer, &registration).await?;
1586        return Ok(registration);
1587    }
1588
1589    Err(OAuthClientError::BuildError(
1590        "authorization server supports none of the configured client registration mechanisms"
1591            .to_string(),
1592    ))
1593}
1594
1595fn validate_cimd_url(client_id: &str) -> Result<(), OAuthClientError> {
1596    let url = reqwest::Url::parse(client_id).map_err(|error| {
1597        OAuthClientError::BuildError(format!("invalid CIMD client ID `{client_id}`: {error}"))
1598    })?;
1599    if url.scheme() != "https" || url.path() == "/" {
1600        return Err(OAuthClientError::BuildError(format!(
1601            "CIMD client ID `{client_id}` must use HTTPS and contain a path"
1602        )));
1603    }
1604    Ok(())
1605}
1606
1607async fn send_token_request(
1608    http: &dyn OAuthHttpClient,
1609    token_endpoint: &str,
1610    issuer: &str,
1611    registration: &OAuthClientRegistration,
1612    method: OAuthTokenEndpointAuthMethod,
1613    mut fields: Vec<(String, String)>,
1614    assertion_signer: Option<&dyn OAuthClientAssertionSigner>,
1615) -> Result<OAuthHttpResponse, OAuthClientError> {
1616    let mut request = OAuthHttpRequest::post_form(token_endpoint, Vec::new());
1617    match method {
1618        OAuthTokenEndpointAuthMethod::None => {
1619            fields.push((
1620                "client_id".to_string(),
1621                registration.client_id().to_string(),
1622            ));
1623        }
1624        OAuthTokenEndpointAuthMethod::ClientSecretBasic => {
1625            let secret = registration.client_secret().ok_or_else(|| {
1626                OAuthClientError::BuildError(
1627                    "client_secret_basic selected without a client secret".to_string(),
1628                )
1629            })?;
1630            request = request.basic_auth(registration.client_id(), secret);
1631        }
1632        OAuthTokenEndpointAuthMethod::ClientSecretPost => {
1633            let secret = registration.client_secret().ok_or_else(|| {
1634                OAuthClientError::BuildError(
1635                    "client_secret_post selected without a client secret".to_string(),
1636                )
1637            })?;
1638            fields.push((
1639                "client_id".to_string(),
1640                registration.client_id().to_string(),
1641            ));
1642            fields.push(("client_secret".to_string(), secret.to_string()));
1643        }
1644        OAuthTokenEndpointAuthMethod::PrivateKeyJwt => {
1645            let signer = assertion_signer.ok_or_else(|| {
1646                OAuthClientError::BuildError(
1647                    "private_key_jwt selected without a client assertion signer".to_string(),
1648                )
1649            })?;
1650            let assertion = signer
1651                .sign_client_assertion(OAuthClientAssertionRequest {
1652                    client_id: registration.client_id().to_string(),
1653                    token_endpoint: token_endpoint.to_string(),
1654                    authorization_server_issuer: issuer.to_string(),
1655                })
1656                .await?;
1657            fields.push((
1658                "client_id".to_string(),
1659                registration.client_id().to_string(),
1660            ));
1661            fields.push((
1662                "client_assertion_type".to_string(),
1663                "urn:ietf:params:oauth:client-assertion-type:jwt-bearer".to_string(),
1664            ));
1665            fields.push(("client_assertion".to_string(), assertion));
1666        }
1667    }
1668    request.body = OAuthHttpBody::Form(fields);
1669    let response = http.execute(request).await?;
1670    if !response.is_success() {
1671        return Err(OAuthClientError::TokenRequest(format!(
1672            "token endpoint returned HTTP {}: {}",
1673            response.status,
1674            response.error_body()
1675        )));
1676    }
1677    Ok(response)
1678}
1679
1680fn token_from_response(
1681    response: OAuthHttpResponse,
1682    requested_scopes: &[String],
1683    previous_refresh_token: Option<String>,
1684) -> Result<OAuthStoredToken, OAuthClientError> {
1685    #[derive(serde::Deserialize)]
1686    struct TokenResponse {
1687        access_token: String,
1688        token_type: String,
1689        expires_in: Option<u64>,
1690        refresh_token: Option<String>,
1691        scope: Option<String>,
1692    }
1693    let response: TokenResponse = response.json()?;
1694    if !response.token_type.eq_ignore_ascii_case("bearer") {
1695        return Err(OAuthClientError::InvalidResponse(format!(
1696            "token endpoint returned unsupported token type `{}`",
1697            response.token_type
1698        )));
1699    }
1700    let scopes = response
1701        .scope
1702        .as_deref()
1703        .map(|scope| unique_scopes(scope.split_ascii_whitespace()))
1704        .unwrap_or_else(|| requested_scopes.to_vec());
1705    Ok(OAuthStoredToken {
1706        access_token: response.access_token,
1707        refresh_token: response.refresh_token.or(previous_refresh_token),
1708        expires_at: response
1709            .expires_in
1710            .map(|lifetime| unix_time().saturating_add(lifetime))
1711            .unwrap_or(u64::MAX),
1712        scopes,
1713    })
1714}
1715
1716fn token_is_valid(token: &OAuthStoredToken, buffer: Duration) -> bool {
1717    unix_time().saturating_add(buffer.as_secs()) < token.expires_at
1718}
1719
1720fn scopes_are_covered(requested: &[String], granted: &[String]) -> bool {
1721    requested
1722        .iter()
1723        .all(|scope| granted.iter().any(|granted| granted == scope))
1724}
1725
1726fn unix_time() -> u64 {
1727    SystemTime::now()
1728        .duration_since(UNIX_EPOCH)
1729        .unwrap_or_default()
1730        .as_secs()
1731}
1732
1733fn unique_scopes<I, S>(scopes: I) -> Vec<String>
1734where
1735    I: IntoIterator<Item = S>,
1736    S: AsRef<str>,
1737{
1738    let mut unique = Vec::new();
1739    for scope in scopes {
1740        for scope in scope.as_ref().split_ascii_whitespace() {
1741            if !scope.is_empty() && !unique.iter().any(|existing| existing == scope) {
1742                unique.push(scope.to_string());
1743            }
1744        }
1745    }
1746    unique
1747}
1748
1749fn random_urlsafe(bytes: usize) -> String {
1750    let mut value = vec![0_u8; bytes];
1751    getrandom::fill(&mut value).expect("getrandom failed");
1752    base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(value)
1753}
1754
1755fn pkce_challenge(verifier: &str) -> String {
1756    use sha2::{Digest, Sha256};
1757    base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(Sha256::digest(verifier.as_bytes()))
1758}
1759
1760async fn prepare_redirect(
1761    policy: &OAuthRedirectPolicy,
1762    _state: &str,
1763) -> Result<(String, Option<oneshot::Receiver<String>>), OAuthClientError> {
1764    match policy {
1765        OAuthRedirectPolicy::Fixed { redirect_uri } => Ok((redirect_uri.clone(), None)),
1766        OAuthRedirectPolicy::Loopback {
1767            port,
1768            callback_path,
1769        } => {
1770            let listener = tokio::net::TcpListener::bind(("127.0.0.1", port.unwrap_or(0)))
1771                .await
1772                .map_err(|error| OAuthClientError::Redirect(error.to_string()))?;
1773            let actual_port = listener
1774                .local_addr()
1775                .map_err(|error| OAuthClientError::Redirect(error.to_string()))?
1776                .port();
1777            let redirect_uri = format!("http://127.0.0.1:{actual_port}{callback_path}");
1778            let (sender, receiver) = oneshot::channel();
1779            let callback_base = format!("http://127.0.0.1:{actual_port}");
1780            tokio::spawn(run_loopback_callback(listener, sender, callback_base));
1781            Ok((redirect_uri, Some(receiver)))
1782        }
1783    }
1784}
1785
1786async fn run_loopback_callback(
1787    listener: tokio::net::TcpListener,
1788    sender: oneshot::Sender<String>,
1789    callback_base: String,
1790) {
1791    use tokio::io::{AsyncReadExt, AsyncWriteExt};
1792
1793    let Ok((mut stream, _)) = listener.accept().await else {
1794        return;
1795    };
1796    let mut bytes = vec![0_u8; 8192];
1797    let Ok(read) = stream.read(&mut bytes).await else {
1798        return;
1799    };
1800    let request = String::from_utf8_lossy(&bytes[..read]);
1801    let target = request
1802        .lines()
1803        .next()
1804        .and_then(|line| line.split_ascii_whitespace().nth(1));
1805    let (status, body) = if target.is_some() {
1806        ("200 OK", "Authorization received. You can close this tab.")
1807    } else {
1808        ("400 Bad Request", "Invalid OAuth callback.")
1809    };
1810    let response = format!(
1811        "HTTP/1.1 {status}\r\nContent-Type: text/plain\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
1812        body.len()
1813    );
1814    let _ = stream.write_all(response.as_bytes()).await;
1815    let _ = stream.flush().await;
1816    if let Some(target) = target {
1817        let _ = sender.send(format!("{callback_base}{target}"));
1818    }
1819}
1820
1821struct ParsedCallback {
1822    code: String,
1823    state: String,
1824    issuer: Option<String>,
1825}
1826
1827fn parse_callback_url(callback_url: &str) -> Result<ParsedCallback, OAuthClientError> {
1828    let url = reqwest::Url::parse(callback_url)
1829        .map_err(|error| OAuthClientError::InvalidResponse(error.to_string()))?;
1830    let mut code = None;
1831    let mut state = None;
1832    let mut issuer = None;
1833    let mut error = None;
1834    let mut error_description = None;
1835    for (name, value) in url.query_pairs() {
1836        match name.as_ref() {
1837            "code" => code = Some(value.into_owned()),
1838            "state" => state = Some(value.into_owned()),
1839            "iss" => issuer = Some(value.into_owned()),
1840            "error" => error = Some(value.into_owned()),
1841            "error_description" => error_description = Some(value.into_owned()),
1842            _ => {}
1843        }
1844    }
1845    if let Some(error) = error {
1846        return Err(OAuthClientError::InvalidResponse(format!(
1847            "authorization server returned `{error}`{}",
1848            error_description
1849                .map(|description| format!(": {description}"))
1850                .unwrap_or_default()
1851        )));
1852    }
1853    Ok(ParsedCallback {
1854        code: code.ok_or_else(|| {
1855            OAuthClientError::InvalidResponse("callback omitted authorization code".to_string())
1856        })?,
1857        state: state.ok_or_else(|| {
1858            OAuthClientError::InvalidResponse("callback omitted state".to_string())
1859        })?,
1860        issuer,
1861    })
1862}
1863
1864fn validate_callback_target(
1865    callback_url: &str,
1866    redirect_uri: &str,
1867) -> Result<(), OAuthClientError> {
1868    let callback = reqwest::Url::parse(callback_url)
1869        .map_err(|error| OAuthClientError::InvalidResponse(error.to_string()))?;
1870    let expected = reqwest::Url::parse(redirect_uri)
1871        .map_err(|error| OAuthClientError::InvalidResponse(error.to_string()))?;
1872    if callback.scheme() == expected.scheme()
1873        && callback.host_str() == expected.host_str()
1874        && callback.port_or_known_default() == expected.port_or_known_default()
1875        && callback.path() == expected.path()
1876    {
1877        Ok(())
1878    } else {
1879        Err(OAuthClientError::InvalidResponse(
1880            "callback URL does not match the configured redirect URI".to_string(),
1881        ))
1882    }
1883}
1884
1885fn validate_callback_issuer(
1886    actual: Option<&str>,
1887    expected: &str,
1888    required: bool,
1889) -> Result<(), OAuthClientError> {
1890    match actual {
1891        Some(actual) if actual == expected => Ok(()),
1892        Some(actual) => Err(OAuthClientError::InvalidResponse(format!(
1893            "authorization response issuer mismatch: expected `{expected}`, got `{actual}`"
1894        ))),
1895        None if required => Err(OAuthClientError::InvalidResponse(
1896            "authorization response omitted required `iss`".to_string(),
1897        )),
1898        None => Ok(()),
1899    }
1900}
1901
1902#[cfg(test)]
1903mod tests {
1904    use std::sync::atomic::{AtomicUsize, Ordering};
1905
1906    use super::*;
1907    use crate::client::OAuthScopeChallenge;
1908
1909    #[derive(Debug, Clone, Copy)]
1910    enum RegistrationMode {
1911        PreRegistered,
1912        Cimd,
1913        Dynamic,
1914        PrivateKeyJwt,
1915    }
1916
1917    #[derive(Clone)]
1918    struct MockOAuthHttp {
1919        mode: RegistrationMode,
1920        requests: Arc<Mutex<Vec<OAuthHttpRequest>>>,
1921        token_requests: Arc<AtomicUsize>,
1922        expire_initial_token: bool,
1923    }
1924
1925    impl MockOAuthHttp {
1926        fn new(mode: RegistrationMode) -> Self {
1927            Self {
1928                mode,
1929                requests: Arc::new(Mutex::new(Vec::new())),
1930                token_requests: Arc::new(AtomicUsize::new(0)),
1931                expire_initial_token: false,
1932            }
1933        }
1934
1935        fn expiring(mut self) -> Self {
1936            self.expire_initial_token = true;
1937            self
1938        }
1939
1940        async fn requests(&self) -> Vec<OAuthHttpRequest> {
1941            self.requests.lock().await.clone()
1942        }
1943
1944        fn response(status: u16, body: serde_json::Value) -> OAuthHttpResponse {
1945            OAuthHttpResponse {
1946                status,
1947                headers: Vec::new(),
1948                body: serde_json::to_vec(&body).unwrap(),
1949            }
1950        }
1951    }
1952
1953    #[async_trait]
1954    impl OAuthHttpClient for MockOAuthHttp {
1955        async fn execute(
1956            &self,
1957            request: OAuthHttpRequest,
1958        ) -> Result<OAuthHttpResponse, OAuthClientError> {
1959            self.requests.lock().await.push(request.clone());
1960            let path = reqwest::Url::parse(&request.url)
1961                .unwrap()
1962                .path()
1963                .to_string();
1964            match path.as_str() {
1965                "/mcp" => Ok(OAuthHttpResponse {
1966                    status: 401,
1967                    headers: vec![(
1968                        "www-authenticate".to_string(),
1969                        "Bearer resource_metadata=\"https://mcp.example.com/prm\", scope=\"challenge.scope\""
1970                            .to_string(),
1971                    )],
1972                    body: Vec::new(),
1973                }),
1974                "/prm" => Ok(Self::response(
1975                    200,
1976                    serde_json::json!({
1977                        "resource": "https://mcp.example.com/mcp",
1978                        "authorization_servers": ["https://auth.example.com/issuer"],
1979                        "scopes_supported": ["prm.scope"]
1980                    }),
1981                )),
1982                "/.well-known/oauth-authorization-server/issuer" => {
1983                    let (cimd, methods) = match self.mode {
1984                        RegistrationMode::PreRegistered => (false, vec!["client_secret_basic"]),
1985                        RegistrationMode::Cimd => (true, vec!["none"]),
1986                        RegistrationMode::Dynamic => (false, vec!["none"]),
1987                        RegistrationMode::PrivateKeyJwt => (false, vec!["private_key_jwt"]),
1988                    };
1989                    Ok(Self::response(
1990                        200,
1991                        serde_json::json!({
1992                            "issuer": "https://auth.example.com/issuer",
1993                            "authorization_endpoint": "https://auth.example.com/authorize",
1994                            "token_endpoint": "https://auth.example.com/token",
1995                            "registration_endpoint": "https://auth.example.com/register",
1996                            "client_id_metadata_document_supported": cimd,
1997                            "authorization_response_iss_parameter_supported": true,
1998                            "code_challenge_methods_supported": ["S256"],
1999                            "token_endpoint_auth_methods_supported": methods,
2000                            "grant_types_supported": ["authorization_code", "refresh_token"],
2001                            "scopes_supported": ["challenge.scope", "prm.scope", "extra.scope", "offline_access"]
2002                        }),
2003                    ))
2004                }
2005                "/register" => Ok(Self::response(
2006                    201,
2007                    serde_json::json!({ "client_id": "dynamic-client" }),
2008                )),
2009                "/token" => {
2010                    let request_number = self.token_requests.fetch_add(1, Ordering::SeqCst);
2011                    let fields = match &request.body {
2012                        OAuthHttpBody::Form(fields) => fields,
2013                        body => panic!("expected form token request, got {body:?}"),
2014                    };
2015                    let grant = fields
2016                        .iter()
2017                        .find(|(name, _)| name == "grant_type")
2018                        .map(|(_, value)| value.as_str())
2019                        .unwrap();
2020                    let scope = fields
2021                        .iter()
2022                        .find(|(name, _)| name == "scope")
2023                        .map(|(_, value)| value.clone())
2024                        .unwrap_or_else(|| {
2025                            if request_number == 0 {
2026                                "challenge.scope offline_access".to_string()
2027                            } else {
2028                                "challenge.scope offline_access extra.scope".to_string()
2029                            }
2030                        });
2031                    if grant == "refresh_token" {
2032                        Ok(Self::response(
2033                            200,
2034                            serde_json::json!({
2035                                "access_token": "refreshed-token",
2036                                "token_type": "Bearer",
2037                                "expires_in": 3600,
2038                                "scope": scope
2039                            }),
2040                        ))
2041                    } else {
2042                        Ok(Self::response(
2043                            200,
2044                            serde_json::json!({
2045                                "access_token": format!("access-token-{request_number}"),
2046                                "token_type": "Bearer",
2047                                "expires_in": if self.expire_initial_token { 0 } else { 3600 },
2048                                "refresh_token": "refresh-token",
2049                                "scope": scope
2050                            }),
2051                        ))
2052                    }
2053                }
2054                other => Err(OAuthClientError::Http(format!(
2055                    "unexpected mock request path `{other}`"
2056                ))),
2057            }
2058        }
2059    }
2060
2061    #[derive(Clone, Default)]
2062    struct AutomaticAuthorizationHandler {
2063        calls: Arc<AtomicUsize>,
2064    }
2065
2066    #[derive(Clone, Default)]
2067    struct TestAssertionSigner;
2068
2069    #[async_trait]
2070    impl OAuthClientAssertionSigner for TestAssertionSigner {
2071        async fn sign_client_assertion(
2072            &self,
2073            request: OAuthClientAssertionRequest,
2074        ) -> Result<String, OAuthClientError> {
2075            assert_eq!(request.client_id, "signed-client");
2076            assert_eq!(request.token_endpoint, "https://auth.example.com/token");
2077            assert_eq!(
2078                request.authorization_server_issuer,
2079                "https://auth.example.com/issuer"
2080            );
2081            Ok("signed-client-assertion".to_string())
2082        }
2083    }
2084
2085    #[async_trait]
2086    impl OAuthAuthorizationHandler for AutomaticAuthorizationHandler {
2087        async fn authorize(
2088            &self,
2089            request: OAuthAuthorizationRequest,
2090        ) -> Result<OAuthAuthorizationAction, OAuthClientError> {
2091            self.calls.fetch_add(1, Ordering::SeqCst);
2092            let authorization_url = reqwest::Url::parse(&request.authorization_url).unwrap();
2093            let state = authorization_url
2094                .query_pairs()
2095                .find(|(name, _)| name == "state")
2096                .unwrap()
2097                .1
2098                .into_owned();
2099            let mut callback = reqwest::Url::parse(&request.redirect_uri).unwrap();
2100            callback
2101                .query_pairs_mut()
2102                .append_pair("code", "authorization-code")
2103                .append_pair("state", &state)
2104                .append_pair("iss", &request.issuer);
2105            Ok(OAuthAuthorizationAction::CallbackUrl(callback.to_string()))
2106        }
2107    }
2108
2109    fn dynamic_options() -> OAuthClientRegistrationOptions {
2110        OAuthClientRegistrationOptions::new()
2111            .with_client_id_metadata_document("https://client.example.com/metadata.json")
2112            .with_dynamic_registration(
2113                super::super::oauth_authcode::OAuthDynamicClientRegistration::native(
2114                    "test-client",
2115                    std::iter::empty::<String>(),
2116                ),
2117            )
2118    }
2119
2120    fn flow_builder(
2121        http: MockOAuthHttp,
2122        options: OAuthClientRegistrationOptions,
2123        handler: AutomaticAuthorizationHandler,
2124    ) -> OAuthAuthorizationFlowBuilder {
2125        OAuthAuthorizationFlow::builder("https://mcp.example.com/mcp")
2126            .http_client(http)
2127            .redirect_policy(OAuthRedirectPolicy::fixed(
2128                "http://127.0.0.1:23456/callback",
2129            ))
2130            .registration_options(options)
2131            .authorization_handler(handler)
2132    }
2133
2134    #[tokio::test]
2135    async fn preregistered_flow_binds_resource_and_uses_basic_auth() {
2136        let http = MockOAuthHttp::new(RegistrationMode::PreRegistered);
2137        let flow = flow_builder(
2138            http.clone(),
2139            OAuthClientRegistrationOptions::new(),
2140            AutomaticAuthorizationHandler::default(),
2141        )
2142        .pre_registered_client("pre:client", Some("pre secret".to_string()))
2143        .build()
2144        .unwrap();
2145
2146        flow.authorize(std::iter::empty::<&str>()).await.unwrap();
2147        assert_eq!(flow.get_token().await.unwrap(), "access-token-0");
2148        assert_eq!(
2149            flow.authorized_scopes().await.unwrap(),
2150            vec!["challenge.scope", "offline_access"]
2151        );
2152
2153        let requests = http.requests().await;
2154        let probe = requests.first().unwrap();
2155        assert_eq!(probe.method, OAuthHttpMethod::Post);
2156        let OAuthHttpBody::Json(probe_body) = &probe.body else {
2157            panic!("expected JSON MCP probe")
2158        };
2159        assert_eq!(probe_body["method"], "tools/list");
2160        assert!(
2161            !requests
2162                .iter()
2163                .any(|request| request.url.ends_with("/register"))
2164        );
2165        let token_request = requests
2166            .iter()
2167            .find(|request| request.url.ends_with("/token"))
2168            .unwrap();
2169        assert!(token_request.headers.iter().any(|(name, value)| {
2170            name == "authorization"
2171                && value
2172                    == &format!(
2173                        "Basic {}",
2174                        base64::engine::general_purpose::STANDARD
2175                            .encode("pre%3Aclient:pre%20secret")
2176                    )
2177        }));
2178        let OAuthHttpBody::Form(fields) = &token_request.body else {
2179            panic!("expected token form")
2180        };
2181        assert!(fields.iter().any(|field| field
2182            == &(
2183                "resource".to_string(),
2184                "https://mcp.example.com/mcp".to_string()
2185            )));
2186    }
2187
2188    #[tokio::test]
2189    async fn private_key_jwt_is_used_when_advertised() {
2190        let http = MockOAuthHttp::new(RegistrationMode::PrivateKeyJwt);
2191        let options = OAuthClientRegistrationOptions::new().with_pre_registered(
2192            OAuthClientRegistration::pre_registered(
2193                "https://auth.example.com/issuer",
2194                "signed-client",
2195                None,
2196            ),
2197        );
2198        let flow = flow_builder(
2199            http.clone(),
2200            options,
2201            AutomaticAuthorizationHandler::default(),
2202        )
2203        .client_assertion_signer(TestAssertionSigner)
2204        .build()
2205        .unwrap();
2206
2207        flow.authorize(["challenge.scope"]).await.unwrap();
2208        let requests = http.requests().await;
2209        let token_request = requests
2210            .iter()
2211            .find(|request| request.url.ends_with("/token"))
2212            .unwrap();
2213        let OAuthHttpBody::Form(fields) = &token_request.body else {
2214            panic!("expected token form")
2215        };
2216        assert!(fields.iter().any(|field| field
2217            == &(
2218                "client_assertion_type".to_string(),
2219                "urn:ietf:params:oauth:client-assertion-type:jwt-bearer".to_string()
2220            )));
2221        assert!(fields.iter().any(|field| field
2222            == &(
2223                "client_assertion".to_string(),
2224                "signed-client-assertion".to_string()
2225            )));
2226    }
2227
2228    #[tokio::test]
2229    async fn cimd_takes_priority_over_dynamic_registration() {
2230        let http = MockOAuthHttp::new(RegistrationMode::Cimd);
2231        let flow = flow_builder(
2232            http.clone(),
2233            dynamic_options(),
2234            AutomaticAuthorizationHandler::default(),
2235        )
2236        .build()
2237        .unwrap();
2238
2239        flow.authorize(["explicit.scope"]).await.unwrap();
2240        let requests = http.requests().await;
2241        assert!(
2242            !requests
2243                .iter()
2244                .any(|request| request.url.ends_with("/register"))
2245        );
2246        let authorization = requests
2247            .iter()
2248            .find(|request| request.url.ends_with("/token"))
2249            .unwrap();
2250        let OAuthHttpBody::Form(fields) = &authorization.body else {
2251            panic!("expected form")
2252        };
2253        assert!(fields.iter().any(|field| field
2254            == &(
2255                "client_id".to_string(),
2256                "https://client.example.com/metadata.json".to_string()
2257            )));
2258        assert!(fields.iter().any(|field| field
2259            == &(
2260                "resource".to_string(),
2261                "https://mcp.example.com/mcp".to_string()
2262            )));
2263    }
2264
2265    #[tokio::test]
2266    async fn dynamic_registration_is_persisted_and_reused() {
2267        let http = MockOAuthHttp::new(RegistrationMode::Dynamic);
2268        let registrations = super::super::oauth_authcode::MemoryOAuthClientRegistrationStore::new();
2269
2270        for _ in 0..2 {
2271            let flow = flow_builder(
2272                http.clone(),
2273                dynamic_options(),
2274                AutomaticAuthorizationHandler::default(),
2275            )
2276            .registration_store(registrations.clone())
2277            .build()
2278            .unwrap();
2279            flow.authorize(["challenge.scope"]).await.unwrap();
2280        }
2281
2282        let requests = http.requests().await;
2283        assert_eq!(
2284            requests
2285                .iter()
2286                .filter(|request| request.url.ends_with("/register"))
2287                .count(),
2288            1
2289        );
2290        let registration_request = requests
2291            .iter()
2292            .find(|request| request.url.ends_with("/register"))
2293            .unwrap();
2294        let OAuthHttpBody::Json(value) = &registration_request.body else {
2295            panic!("expected registration JSON")
2296        };
2297        assert_eq!(value["redirect_uris"][0], "http://127.0.0.1:23456/callback");
2298    }
2299
2300    #[tokio::test]
2301    async fn expired_token_refreshes_and_preserves_binding() {
2302        let http = MockOAuthHttp::new(RegistrationMode::Dynamic).expiring();
2303        let flow = flow_builder(
2304            http.clone(),
2305            dynamic_options(),
2306            AutomaticAuthorizationHandler::default(),
2307        )
2308        .refresh_buffer(Duration::ZERO)
2309        .build()
2310        .unwrap();
2311
2312        flow.authorize(["challenge.scope"]).await.unwrap();
2313        assert_eq!(flow.get_token().await.unwrap(), "refreshed-token");
2314        let requests = http.requests().await;
2315        let refresh = requests
2316            .iter()
2317            .rfind(|request| request.url.ends_with("/token"))
2318            .unwrap();
2319        let OAuthHttpBody::Form(fields) = &refresh.body else {
2320            panic!("expected refresh form")
2321        };
2322        assert!(
2323            fields
2324                .iter()
2325                .any(|field| field == &("grant_type".to_string(), "refresh_token".to_string()))
2326        );
2327        assert!(fields.iter().any(|field| field
2328            == &(
2329                "resource".to_string(),
2330                "https://mcp.example.com/mcp".to_string()
2331            )));
2332    }
2333
2334    #[tokio::test]
2335    async fn scope_escalation_reauthorizes_same_provider() {
2336        let http = MockOAuthHttp::new(RegistrationMode::Dynamic);
2337        let handler = AutomaticAuthorizationHandler::default();
2338        let calls = handler.calls.clone();
2339        let flow = flow_builder(http, dynamic_options(), handler)
2340            .build()
2341            .unwrap();
2342        flow.authorize(std::iter::empty::<&str>()).await.unwrap();
2343
2344        flow.reauthorize(OAuthScopeEscalationRequest {
2345            resource: "https://mcp.example.com/mcp".to_string(),
2346            operation: "tools/call:admin".to_string(),
2347            challenge: OAuthScopeChallenge {
2348                required_scopes: vec!["extra.scope".to_string()],
2349                resource_metadata: Some("https://mcp.example.com/prm".to_string()),
2350                error_description: None,
2351            },
2352            previous_scopes: vec!["challenge.scope".to_string(), "offline_access".to_string()],
2353            requested_scopes: vec![
2354                "challenge.scope".to_string(),
2355                "offline_access".to_string(),
2356                "extra.scope".to_string(),
2357            ],
2358            attempt: 1,
2359        })
2360        .await
2361        .unwrap();
2362
2363        assert_eq!(calls.load(Ordering::SeqCst), 2);
2364        assert_eq!(
2365            flow.authorized_scopes().await.unwrap(),
2366            vec!["challenge.scope", "offline_access", "extra.scope"]
2367        );
2368        assert_eq!(flow.get_token().await.unwrap(), "access-token-1");
2369    }
2370
2371    #[tokio::test]
2372    async fn persisted_pkce_state_can_complete_after_flow_rebuild() {
2373        let http = MockOAuthHttp::new(RegistrationMode::Dynamic);
2374        let state_store = MemoryOAuthAuthorizationStateStore::new();
2375        let token_store = MemoryOAuthTokenStore::new();
2376        let registrations = super::super::oauth_authcode::MemoryOAuthClientRegistrationStore::new();
2377        let first = flow_builder(
2378            http.clone(),
2379            dynamic_options(),
2380            AutomaticAuthorizationHandler::default(),
2381        )
2382        .state_store(state_store.clone())
2383        .token_store(token_store.clone())
2384        .registration_store(registrations.clone())
2385        .build()
2386        .unwrap();
2387        let OAuthAuthorizationStart::Pending(pending) = first.begin(["prm.scope"]).await.unwrap()
2388        else {
2389            panic!("expected pending flow")
2390        };
2391        let request = pending.request().clone();
2392        let authorization_url = reqwest::Url::parse(&request.authorization_url).unwrap();
2393        let state = authorization_url
2394            .query_pairs()
2395            .find(|(name, _)| name == "state")
2396            .unwrap()
2397            .1
2398            .into_owned();
2399        drop(pending);
2400
2401        let second = flow_builder(
2402            http,
2403            dynamic_options(),
2404            AutomaticAuthorizationHandler::default(),
2405        )
2406        .state_store(state_store)
2407        .token_store(token_store)
2408        .registration_store(registrations)
2409        .build()
2410        .unwrap();
2411        let mut callback = reqwest::Url::parse(&request.redirect_uri).unwrap();
2412        callback
2413            .query_pairs_mut()
2414            .append_pair("code", "persisted-code")
2415            .append_pair("state", &state)
2416            .append_pair("iss", &request.issuer);
2417        second
2418            .complete_callback_url(callback.as_str())
2419            .await
2420            .unwrap();
2421        assert_eq!(second.get_token().await.unwrap(), "access-token-0");
2422    }
2423
2424    #[test]
2425    fn token_without_lifetime_remains_valid_until_the_server_rejects_it() {
2426        let token = token_from_response(
2427            MockOAuthHttp::response(
2428                200,
2429                serde_json::json!({
2430                    "access_token": "access-token",
2431                    "token_type": "Bearer"
2432                }),
2433            ),
2434            &["read".to_string()],
2435            None,
2436        )
2437        .unwrap();
2438
2439        assert_eq!(token.expires_at, u64::MAX);
2440        assert!(token_is_valid(&token, Duration::from_secs(30)));
2441    }
2442
2443    #[test]
2444    fn token_response_rejects_non_bearer_token_types() {
2445        let error = token_from_response(
2446            MockOAuthHttp::response(
2447                200,
2448                serde_json::json!({
2449                    "access_token": "access-token",
2450                    "token_type": "DPoP"
2451                }),
2452            ),
2453            &[],
2454            None,
2455        )
2456        .unwrap_err();
2457
2458        assert!(error.to_string().contains("unsupported token type `DPoP`"));
2459    }
2460
2461    #[test]
2462    fn scope_selection_falls_back_from_challenge_to_resource_metadata() {
2463        let metadata: OAuthAuthorizationServerMetadata =
2464            serde_json::from_value(serde_json::json!({
2465                "issuer": "https://auth.example.com/issuer",
2466                "authorization_endpoint": "https://auth.example.com/authorize",
2467                "token_endpoint": "https://auth.example.com/token",
2468                "grant_types_supported": ["authorization_code"],
2469                "scopes_supported": ["as.scope"]
2470            }))
2471            .unwrap();
2472        let protected_resource: OAuthProtectedResourceMetadata =
2473            serde_json::from_value(serde_json::json!({
2474                "resource": "https://mcp.example.com/mcp",
2475                "authorization_servers": ["https://auth.example.com/issuer"],
2476                "scopes_supported": ["prm.scope"]
2477            }))
2478            .unwrap();
2479        let mut discovery = FlowDiscovery {
2480            resource: protected_resource.resource.clone(),
2481            protected_resource,
2482            authorization_servers: vec![metadata.clone()],
2483            challenge: None,
2484        };
2485
2486        assert_eq!(select_scopes(&[], &discovery, &metadata), ["prm.scope"]);
2487        discovery.challenge = Some(OAuthBearerChallenge {
2488            error: None,
2489            scopes: vec!["challenge.scope".to_string()],
2490            resource_metadata: None,
2491            error_description: None,
2492        });
2493        assert_eq!(
2494            select_scopes(&[], &discovery, &metadata),
2495            ["challenge.scope"]
2496        );
2497        assert_eq!(
2498            select_scopes(&["explicit.scope".to_string()], &discovery, &metadata),
2499            ["explicit.scope"]
2500        );
2501    }
2502
2503    #[test]
2504    fn resource_identifier_may_be_a_canonical_parent_on_the_same_origin() {
2505        assert!(
2506            validate_resource_identifier("https://mcp.example.com/mcp", "https://mcp.example.com")
2507                .is_ok()
2508        );
2509        assert!(
2510            validate_resource_identifier(
2511                "https://mcp.example.com/tenant/mcp",
2512                "https://mcp.example.com/tenant"
2513            )
2514            .is_ok()
2515        );
2516        assert!(
2517            validate_resource_identifier(
2518                "https://mcp.example.com/tenant-evil/mcp",
2519                "https://mcp.example.com/tenant"
2520            )
2521            .is_err()
2522        );
2523        assert!(
2524            validate_resource_identifier(
2525                "https://other.example.com/mcp",
2526                "https://mcp.example.com"
2527            )
2528            .is_err()
2529        );
2530    }
2531}