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