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 && 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 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 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 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 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, ®istration).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) = ®istration_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}