1use std::{
15 fmt, fs,
16 future::Future,
17 io,
18 path::{Path, PathBuf},
19 pin::Pin,
20 sync::Arc,
21 time::{Duration, SystemTime, UNIX_EPOCH},
22};
23
24use base64::{
25 Engine as _,
26 engine::general_purpose::{STANDARD as B64, URL_SAFE_NO_PAD},
27};
28use pkcs8::der::pem;
29use ring::{
30 rand::SystemRandom,
31 signature::{self, RsaKeyPair},
32};
33use tokio::{
34 io::{AsyncRead, AsyncReadExt as _, AsyncWrite, AsyncWriteExt as _},
35 sync::Mutex,
36 task::JoinHandle,
37 time::{Instant, sleep, timeout},
38};
39
40use crate::{
41 backoff,
42 security::{Password, TlsConnectorConfig},
43 transport::{SocketOptions, dial},
44};
45
46pub type TokenFuture<'a> = Pin<Box<dyn Future<Output = Result<String, String>> + Send + 'a>>;
48
49pub type RefreshFuture<'a> = Pin<Box<dyn Future<Output = Option<SystemTime>> + Send + 'a>>;
51
52pub trait OAuthBearerTokenProvider: fmt::Debug + Send + Sync {
57 fn token(&self) -> TokenFuture<'_>;
62
63 fn refresh(&self) -> TokenFuture<'_> {
72 self.token()
73 }
74
75 fn refresh_at(&self) -> RefreshFuture<'_> {
81 Box::pin(async { None })
82 }
83
84 fn background_refresh_enabled(&self) -> bool {
95 true
96 }
97}
98
99#[must_use]
114pub fn spawn_background_refresh(
115 provider: Arc<dyn OAuthBearerTokenProvider>,
116) -> Option<JoinHandle<()>> {
117 if !provider.background_refresh_enabled() {
118 return None;
119 }
120 Some(tokio::spawn(async move {
121 let mut result = provider.token().await;
122 loop {
123 if let Err(error) = result {
124 tracing::warn!(error = %error, "background OAUTHBEARER token refresh failed");
125 }
126 let wait = provider
127 .refresh_at()
128 .await
129 .and_then(|refresh_at| refresh_at.duration_since(SystemTime::now()).ok())
130 .unwrap_or(DEFAULT_LOGIN_RETRY_BACKOFF_MAX);
131 sleep(wait).await;
132 result = provider.refresh().await;
133 }
134 }))
135}
136
137pub const DEFAULT_LOGIN_RETRY_BACKOFF: Duration = Duration::from_millis(100);
139pub const DEFAULT_LOGIN_RETRY_BACKOFF_MAX: Duration = Duration::from_secs(10);
141pub const DEFAULT_LOGIN_REFRESH_WINDOW_FACTOR: f64 = 0.80;
143pub const DEFAULT_LOGIN_REFRESH_WINDOW_JITTER: f64 = 0.05;
145pub const DEFAULT_LOGIN_REFRESH_MIN_PERIOD: Duration = Duration::from_mins(1);
147pub const DEFAULT_LOGIN_REFRESH_BUFFER: Duration = Duration::from_mins(5);
149const MAX_RESPONSE_SNIPPET: usize = 1000;
151const MAX_RESPONSE_BYTES: usize = 1024 * 1024;
153
154#[derive(Clone, Debug)]
157pub struct ClientCredentialsConfig {
158 pub token_endpoint_url: String,
160 pub client_id: String,
162 pub client_secret: Password,
164 pub scope: Option<String>,
166 pub url_encode: bool,
169 pub tls: TlsConnectorConfig,
171 pub connect_timeout: Option<Duration>,
173 pub read_timeout: Option<Duration>,
175 pub retry_backoff: Duration,
177 pub retry_backoff_max: Duration,
179 pub refresh_window_factor: f64,
181 pub refresh_window_jitter: f64,
183 pub refresh_min_period: Duration,
185 pub refresh_buffer: Duration,
187 pub background_refresh: bool,
189}
190
191impl ClientCredentialsConfig {
192 #[must_use]
195 pub fn new(
196 endpoint: impl Into<String>,
197 client_id: impl Into<String>,
198 client_secret: impl Into<Password>,
199 ) -> Self {
200 Self {
201 token_endpoint_url: endpoint.into(),
202 client_id: client_id.into(),
203 client_secret: client_secret.into(),
204 scope: None,
205 url_encode: false,
206 tls: TlsConnectorConfig::default(),
207 connect_timeout: None,
208 read_timeout: None,
209 retry_backoff: DEFAULT_LOGIN_RETRY_BACKOFF,
210 retry_backoff_max: DEFAULT_LOGIN_RETRY_BACKOFF_MAX,
211 refresh_window_factor: DEFAULT_LOGIN_REFRESH_WINDOW_FACTOR,
212 refresh_window_jitter: DEFAULT_LOGIN_REFRESH_WINDOW_JITTER,
213 refresh_min_period: DEFAULT_LOGIN_REFRESH_MIN_PERIOD,
214 refresh_buffer: DEFAULT_LOGIN_REFRESH_BUFFER,
215 background_refresh: true,
216 }
217 }
218}
219
220#[derive(Debug)]
222struct Cached {
223 token: String,
224 refresh_at: SystemTime,
225}
226
227#[derive(Debug)]
234pub struct ClientCredentialsTokenProvider {
235 config: ClientCredentialsConfig,
236 cached: Mutex<Option<Cached>>,
237}
238
239impl ClientCredentialsTokenProvider {
240 pub fn new(config: ClientCredentialsConfig) -> Result<Arc<Self>, String> {
246 if config.client_id.trim().is_empty() {
247 return Err("sasl.oauthbearer.client.credentials.client.id is blank".into());
248 }
249 if config.client_secret.value().trim().is_empty() {
250 return Err("sasl.oauthbearer.client.credentials.client.secret is blank".into());
251 }
252 Endpoint::parse(&config.token_endpoint_url)?;
253 for (name, value, range) in [
257 (
258 "sasl.login.refresh.window.factor",
259 config.refresh_window_factor,
260 (0.5, 1.0),
261 ),
262 (
263 "sasl.login.refresh.window.jitter",
264 config.refresh_window_jitter,
265 (0.0, 0.25),
266 ),
267 ] {
268 if !value.is_finite() || value < range.0 || value > range.1 {
269 return Err(format!(
270 "{name} must be between {} and {}, and it is {value}",
271 range.0, range.1
272 ));
273 }
274 }
275 Ok(Arc::new(Self {
276 config,
277 cached: Mutex::new(None),
278 }))
279 }
280
281 async fn fetch_into(
284 &self,
285 cached: &mut Option<Cached>,
286 now: SystemTime,
287 ) -> Result<String, String> {
288 let token = self.fetch().await?;
289 let claims = token_times(&token)?;
290 let refresh_at = self.refresh_time(
291 now,
292 claims.0.unwrap_or(now),
293 claims.1,
294 backoff::random_unit(),
295 );
296 *cached = Some(Cached {
297 token: token.clone(),
298 refresh_at,
299 });
300 Ok(token)
301 }
302
303 async fn fetch(&self) -> Result<String, String> {
304 let request = self.request()?;
305 retry_token_request(
306 self.config.retry_backoff,
307 self.config.retry_backoff_max,
308 || Box::pin(self.post(&request)),
309 )
310 .await
311 }
312
313 fn request(&self) -> Result<TokenRequest, String> {
315 let encode = |value: &str| {
316 if self.config.url_encode {
317 form_urlencode(value)
318 } else {
319 value.to_owned()
320 }
321 };
322 let client_id = encode(self.config.client_id.trim());
323 let client_secret = encode(self.config.client_secret.value().trim());
324 let scope = self
325 .config
326 .scope
327 .as_deref()
328 .map(str::trim)
329 .filter(|scope| !scope.is_empty())
330 .map(encode);
331 let mut body = "grant_type=client_credentials".to_owned();
332 if let Some(scope) = scope {
333 body.push_str("&scope=");
334 body.push_str(&scope);
335 }
336 Ok(TokenRequest {
337 endpoint: Endpoint::parse(&self.config.token_endpoint_url)?,
338 authorization: Some(format!(
339 "Basic {}",
340 B64.encode(format!("{client_id}:{client_secret}"))
341 )),
342 body,
343 })
344 }
345
346 async fn post(&self, request: &TokenRequest) -> Result<String, Failure> {
347 post_to_endpoint(
348 &self.config.tls,
349 self.config.connect_timeout,
350 self.config.read_timeout,
351 request,
352 )
353 .await
354 }
355
356 fn refresh_time(
360 &self,
361 now: SystemTime,
362 start: SystemTime,
363 expire: SystemTime,
364 unit: f64,
365 ) -> SystemTime {
366 expiring_credential_refresh_at(
367 now,
368 start,
369 expire,
370 unit,
371 &RefreshWindow {
372 factor: self.config.refresh_window_factor,
373 jitter: self.config.refresh_window_jitter,
374 min_period: self.config.refresh_min_period,
375 buffer: self.config.refresh_buffer,
376 },
377 )
378 }
379}
380
381impl OAuthBearerTokenProvider for ClientCredentialsTokenProvider {
382 fn token(&self) -> TokenFuture<'_> {
383 Box::pin(async move {
384 let mut cached = self.cached.lock().await;
385 let now = SystemTime::now();
386 if let Some(current) = cached.as_ref()
387 && now < current.refresh_at
388 {
389 return Ok(current.token.clone());
390 }
391 let token = self.fetch_into(&mut cached, now).await;
392 drop(cached);
393 token
394 })
395 }
396
397 fn refresh(&self) -> TokenFuture<'_> {
398 Box::pin(async move {
399 let mut cached = self.cached.lock().await;
400 self.fetch_into(&mut cached, SystemTime::now()).await
401 })
402 }
403
404 fn refresh_at(&self) -> RefreshFuture<'_> {
405 Box::pin(async move {
406 self.cached
407 .lock()
408 .await
409 .as_ref()
410 .map(|cached| cached.refresh_at)
411 })
412 }
413
414 fn background_refresh_enabled(&self) -> bool {
415 self.config.background_refresh
416 }
417}
418
419#[derive(Clone, Copy, Debug)]
423struct RefreshWindow {
424 factor: f64,
425 jitter: f64,
426 min_period: Duration,
427 buffer: Duration,
428}
429
430fn expiring_credential_refresh_at(
435 now: SystemTime,
436 start: SystemTime,
437 expire: SystemTime,
438 unit: f64,
439 window: &RefreshWindow,
440) -> SystemTime {
441 let factor = window.jitter.mul_add(unit, window.factor);
442 if now + window.min_period + window.buffer > expire {
443 return now
444 + expire
445 .duration_since(now)
446 .unwrap_or_default()
447 .mul_f64(factor);
448 }
449 let proposed = start
450 + expire
451 .duration_since(start)
452 .unwrap_or_default()
453 .mul_f64(factor);
454 let buffer_start = expire - window.buffer;
455 if proposed > buffer_start {
456 return buffer_start;
457 }
458 proposed.max(now + window.min_period)
459}
460
461const JWT_BEARER_GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:jwt-bearer";
463
464#[derive(Clone, Debug)]
467pub struct JwtBearerConfig {
468 pub token_endpoint_url: String,
470 pub algorithm: String,
472 pub exp_seconds: u64,
475 pub nbf_seconds: u64,
478 pub include_jti: bool,
481 pub iss: Option<String>,
483 pub sub: Option<String>,
485 pub aud: Option<String>,
487 pub scope: Option<String>,
489 pub private_key_file: PathBuf,
494 pub private_key_passphrase: Option<Password>,
496 pub template_file: Option<PathBuf>,
504 pub tls: TlsConnectorConfig,
506 pub connect_timeout: Option<Duration>,
508 pub read_timeout: Option<Duration>,
510 pub retry_backoff: Duration,
512 pub retry_backoff_max: Duration,
514 pub refresh_window_factor: f64,
516 pub refresh_window_jitter: f64,
518 pub refresh_min_period: Duration,
520 pub refresh_buffer: Duration,
522 pub background_refresh: bool,
524}
525
526impl JwtBearerConfig {
527 #[must_use]
531 pub fn new(endpoint: impl Into<String>, private_key_file: impl Into<PathBuf>) -> Self {
532 Self {
533 token_endpoint_url: endpoint.into(),
534 algorithm: "RS256".to_owned(),
535 exp_seconds: 300,
536 nbf_seconds: 60,
537 include_jti: false,
538 iss: None,
539 sub: None,
540 aud: None,
541 scope: None,
542 private_key_file: private_key_file.into(),
543 private_key_passphrase: None,
544 template_file: None,
545 tls: TlsConnectorConfig::default(),
546 connect_timeout: None,
547 read_timeout: None,
548 retry_backoff: DEFAULT_LOGIN_RETRY_BACKOFF,
549 retry_backoff_max: DEFAULT_LOGIN_RETRY_BACKOFF_MAX,
550 refresh_window_factor: DEFAULT_LOGIN_REFRESH_WINDOW_FACTOR,
551 refresh_window_jitter: DEFAULT_LOGIN_REFRESH_WINDOW_JITTER,
552 refresh_min_period: DEFAULT_LOGIN_REFRESH_MIN_PERIOD,
553 refresh_buffer: DEFAULT_LOGIN_REFRESH_BUFFER,
554 background_refresh: true,
555 }
556 }
557}
558
559type ClaimMap = serde_json::Map<String, serde_json::Value>;
562
563pub struct JwtBearerTokenProvider {
570 config: JwtBearerConfig,
571 key_pair: RsaKeyPair,
572 template_header: ClaimMap,
573 template_payload: ClaimMap,
574 cached: Mutex<Option<Cached>>,
575}
576
577impl fmt::Debug for JwtBearerTokenProvider {
578 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
579 formatter
580 .debug_struct("JwtBearerTokenProvider")
581 .field("config", &self.config)
582 .finish_non_exhaustive()
583 }
584}
585
586impl JwtBearerTokenProvider {
587 pub fn new(config: JwtBearerConfig) -> Result<Arc<Self>, String> {
595 if config.algorithm != "RS256" {
596 return Err(format!(
597 "sasl.oauthbearer.assertion.algorithm {:?} is not supported; only RS256 is \
598 implemented",
599 config.algorithm
600 ));
601 }
602 Endpoint::parse(&config.token_endpoint_url)?;
603 for (name, value, range) in [
607 (
608 "sasl.login.refresh.window.factor",
609 config.refresh_window_factor,
610 (0.5, 1.0),
611 ),
612 (
613 "sasl.login.refresh.window.jitter",
614 config.refresh_window_jitter,
615 (0.0, 0.25),
616 ),
617 ] {
618 if !value.is_finite() || value < range.0 || value > range.1 {
619 return Err(format!(
620 "{name} must be between {} and {}, and it is {value}",
621 range.0, range.1
622 ));
623 }
624 }
625 let key_pair = load_rsa_private_key(
626 &config.private_key_file,
627 config.private_key_passphrase.as_ref(),
628 )
629 .map_err(|error| {
630 format!(
631 "sasl.oauthbearer.assertion.private.key.file {}: {error}",
632 config.private_key_file.display()
633 )
634 })?;
635 let (template_header, template_payload) = match &config.template_file {
636 Some(path) => load_assertion_template(path)?,
637 None => (ClaimMap::new(), ClaimMap::new()),
638 };
639 Ok(Arc::new(Self {
640 config,
641 key_pair,
642 template_header,
643 template_payload,
644 cached: Mutex::new(None),
645 }))
646 }
647
648 async fn fetch_into(
651 &self,
652 cached: &mut Option<Cached>,
653 now: SystemTime,
654 ) -> Result<String, String> {
655 let token = self.fetch().await?;
656 let claims = token_times(&token)?;
657 let refresh_at = self.refresh_time(
658 now,
659 claims.0.unwrap_or(now),
660 claims.1,
661 backoff::random_unit(),
662 );
663 *cached = Some(Cached {
664 token: token.clone(),
665 refresh_at,
666 });
667 Ok(token)
668 }
669
670 async fn fetch(&self) -> Result<String, String> {
671 let request = self.request()?;
672 retry_token_request(
673 self.config.retry_backoff,
674 self.config.retry_backoff_max,
675 || Box::pin(self.post(&request)),
676 )
677 .await
678 }
679
680 fn claims(&self, now: SystemTime) -> (ClaimMap, ClaimMap) {
687 let mut header = self.template_header.clone();
688 header.insert("typ".to_owned(), "JWT".into());
689 header.insert("alg".to_owned(), self.config.algorithm.clone().into());
690
691 let mut payload = ClaimMap::new();
692 if let Some(iss) = &self.config.iss {
693 payload.insert("iss".to_owned(), iss.clone().into());
694 }
695 if let Some(sub) = &self.config.sub {
696 payload.insert("sub".to_owned(), sub.clone().into());
697 }
698 if let Some(aud) = &self.config.aud {
699 payload.insert("aud".to_owned(), aud.clone().into());
700 }
701 for (claim, value) in &self.template_payload {
702 payload.insert(claim.clone(), value.clone());
703 }
704 let now_secs = now.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs();
705 payload.insert("iat".to_owned(), now_secs.into());
706 payload.insert(
707 "exp".to_owned(),
708 (now_secs + self.config.exp_seconds).into(),
709 );
710 payload.insert(
711 "nbf".to_owned(),
712 now_secs.saturating_sub(self.config.nbf_seconds).into(),
713 );
714 if self.config.include_jti {
715 payload.insert("jti".to_owned(), uuid::Uuid::new_v4().to_string().into());
716 }
717 (header, payload)
718 }
719
720 fn assertion(&self) -> Result<String, String> {
722 let (header, payload) = self.claims(SystemTime::now());
723 let signing_input = format!(
724 "{}.{}",
725 URL_SAFE_NO_PAD.encode(serde_json::Value::Object(header).to_string()),
726 URL_SAFE_NO_PAD.encode(serde_json::Value::Object(payload).to_string()),
727 );
728 let signature = sign_rs256(&self.key_pair, signing_input.as_bytes())?;
729 Ok(format!(
730 "{signing_input}.{}",
731 URL_SAFE_NO_PAD.encode(signature)
732 ))
733 }
734
735 fn request(&self) -> Result<TokenRequest, String> {
739 let assertion = self.assertion()?;
740 let mut body = format!(
741 "grant_type={}&assertion={}",
742 form_urlencode(JWT_BEARER_GRANT_TYPE),
743 form_urlencode(&assertion)
744 );
745 if let Some(scope) = self
746 .config
747 .scope
748 .as_deref()
749 .map(str::trim)
750 .filter(|scope| !scope.is_empty())
751 {
752 body.push_str("&scope=");
753 body.push_str(&form_urlencode(scope));
754 }
755 Ok(TokenRequest {
756 endpoint: Endpoint::parse(&self.config.token_endpoint_url)?,
757 authorization: None,
758 body,
759 })
760 }
761
762 async fn post(&self, request: &TokenRequest) -> Result<String, Failure> {
763 post_to_endpoint(
764 &self.config.tls,
765 self.config.connect_timeout,
766 self.config.read_timeout,
767 request,
768 )
769 .await
770 }
771
772 fn refresh_time(
776 &self,
777 now: SystemTime,
778 start: SystemTime,
779 expire: SystemTime,
780 unit: f64,
781 ) -> SystemTime {
782 expiring_credential_refresh_at(
783 now,
784 start,
785 expire,
786 unit,
787 &RefreshWindow {
788 factor: self.config.refresh_window_factor,
789 jitter: self.config.refresh_window_jitter,
790 min_period: self.config.refresh_min_period,
791 buffer: self.config.refresh_buffer,
792 },
793 )
794 }
795}
796
797impl OAuthBearerTokenProvider for JwtBearerTokenProvider {
798 fn token(&self) -> TokenFuture<'_> {
799 Box::pin(async move {
800 let mut cached = self.cached.lock().await;
801 let now = SystemTime::now();
802 if let Some(current) = cached.as_ref()
803 && now < current.refresh_at
804 {
805 return Ok(current.token.clone());
806 }
807 let token = self.fetch_into(&mut cached, now).await;
808 drop(cached);
809 token
810 })
811 }
812
813 fn refresh(&self) -> TokenFuture<'_> {
814 Box::pin(async move {
815 let mut cached = self.cached.lock().await;
816 self.fetch_into(&mut cached, SystemTime::now()).await
817 })
818 }
819
820 fn refresh_at(&self) -> RefreshFuture<'_> {
821 Box::pin(async move {
822 self.cached
823 .lock()
824 .await
825 .as_ref()
826 .map(|cached| cached.refresh_at)
827 })
828 }
829
830 fn background_refresh_enabled(&self) -> bool {
831 self.config.background_refresh
832 }
833}
834
835fn sign_rs256(key_pair: &RsaKeyPair, message: &[u8]) -> Result<Vec<u8>, String> {
838 let rng = SystemRandom::new();
839 let mut signature = vec![0_u8; key_pair.public().modulus_len()];
840 key_pair
841 .sign(&signature::RSA_PKCS1_SHA256, &rng, message, &mut signature)
842 .map_err(|_| "RSA signing failed".to_owned())?;
843 Ok(signature)
844}
845
846fn pem_block(text: &str) -> Result<(String, Vec<u8>), String> {
848 const BEGIN: &str = "-----BEGIN ";
849 let (_, after_begin) = text
850 .split_once(BEGIN)
851 .ok_or_else(|| "no PEM block found".to_owned())?;
852 let (label, _) = after_begin
853 .split_once("-----")
854 .ok_or_else(|| "unterminated BEGIN line".to_owned())?;
855 let label = label.to_owned();
856 let end_line = format!("-----END {label}-----");
857 let (inside, _) = after_begin
858 .split_once(&end_line)
859 .ok_or_else(|| format!("no END line for {label}"))?;
860 let block = format!("{BEGIN}{inside}{end_line}");
861 let (_, der) =
862 pem::decode_vec(block.as_bytes()).map_err(|error| format!("{label}: {error}"))?;
863 Ok((label, der))
864}
865
866fn decrypt_pkcs8(der: &[u8], passphrase: &Password) -> Result<Vec<u8>, String> {
869 let encrypted = pkcs8::EncryptedPrivateKeyInfoRef::try_from(der)
870 .map_err(|error| format!("invalid encrypted private key: {error}"))?;
871 let document = encrypted
872 .decrypt(passphrase.value())
873 .map_err(|error| format!("private key decryption failed: {error}"))?;
874 Ok(document.as_bytes().to_vec())
875}
876
877fn load_rsa_private_key(path: &Path, passphrase: Option<&Password>) -> Result<RsaKeyPair, String> {
883 let text = fs::read_to_string(path).map_err(|error| error.to_string())?;
884 let (label, der) = pem_block(&text)?;
885 let der = match (label.as_str(), passphrase) {
886 ("ENCRYPTED PRIVATE KEY", Some(passphrase)) => decrypt_pkcs8(&der, passphrase)?,
887 ("ENCRYPTED PRIVATE KEY", None) => {
888 return Err(
889 "the private key is encrypted, but sasl.oauthbearer.assertion.private.key.\
890 passphrase is not set"
891 .to_owned(),
892 );
893 }
894 (_, Some(_)) => {
895 return Err(
896 "sasl.oauthbearer.assertion.private.key.passphrase is set, but the private key \
897 is not encrypted"
898 .to_owned(),
899 );
900 }
901 ("PRIVATE KEY" | "RSA PRIVATE KEY", None) => der,
902 (other, None) => return Err(format!("unsupported private key format {other:?}")),
903 };
904 match label.as_str() {
905 "RSA PRIVATE KEY" => RsaKeyPair::from_der(&der),
906 _ => RsaKeyPair::from_pkcs8(&der),
907 }
908 .map_err(|error| format!("invalid RSA private key: {error}"))
909}
910
911fn load_assertion_template(path: &Path) -> Result<(ClaimMap, ClaimMap), String> {
916 let shown = path.display();
917 let text = fs::read_to_string(path)
918 .map_err(|error| format!("sasl.oauthbearer.assertion.template.file {shown}: {error}"))?;
919 let json: serde_json::Value = serde_json::from_str(&text)
920 .map_err(|error| format!("sasl.oauthbearer.assertion.template.file {shown}: {error}"))?;
921 let map = |key: &str| match json.get(key) {
922 None => Ok(ClaimMap::new()),
923 Some(serde_json::Value::Object(map)) => Ok(map.clone()),
924 Some(_) => Err(format!(
925 "sasl.oauthbearer.assertion.template.file {shown}: {key:?} is not a JSON object"
926 )),
927 };
928 Ok((map("header")?, map("payload")?))
929}
930
931enum Failure {
933 Retriable(String),
935 Final(String),
937}
938
939struct TokenRequest {
940 endpoint: Endpoint,
941 authorization: Option<String>,
945 body: String,
946}
947
948async fn retry_token_request<'a>(
953 retry_backoff: Duration,
954 retry_backoff_max: Duration,
955 mut attempt: impl FnMut() -> Pin<Box<dyn Future<Output = Result<String, Failure>> + Send + 'a>>,
956) -> Result<String, String> {
957 let started = Instant::now();
958 let end = started + retry_backoff_max;
959 let mut attempt_number = 0_u32;
960 loop {
961 attempt_number += 1;
962 match attempt().await {
963 Ok(body) => return parse_token(&body),
964 Err(Failure::Final(error)) => return Err(error),
965 Err(Failure::Retriable(error)) => {
966 let wait = retry_backoff
967 .saturating_mul(2_u32.saturating_pow(attempt_number - 1))
968 .min(end.saturating_duration_since(Instant::now()));
969 if wait.is_zero() {
970 return Err(error);
971 }
972 tracing::warn!(
973 attempt = attempt_number,
974 error = %error,
975 "token endpoint request failed"
976 );
977 sleep(wait).await;
978 }
979 }
980 }
981}
982
983async fn post_to_endpoint(
986 tls: &TlsConnectorConfig,
987 connect_timeout: Option<Duration>,
988 read_timeout: Option<Duration>,
989 request: &TokenRequest,
990) -> Result<String, Failure> {
991 let endpoint = &request.endpoint;
992 let connect = dial(&endpoint.host, endpoint.port, SocketOptions::default());
993 let tcp = within(connect_timeout, connect)
994 .await
995 .map_err(|()| Failure::Retriable("token endpoint connect timed out".into()))?
996 .map_err(|error| Failure::Retriable(format!("token endpoint connect: {error}")))?;
997 let exchange = async {
998 if endpoint.https {
999 let connector = tls
1000 .connector()
1001 .map_err(|error| Failure::Final(error.to_string()))?;
1002 let server_name = if tls.server_name.is_empty() {
1005 endpoint.host.clone()
1006 } else {
1007 tls.server_name.clone()
1008 };
1009 let name = rustls::pki_types::ServerName::try_from(server_name)
1010 .map_err(|error| Failure::Final(format!("invalid endpoint host: {error}")))?;
1011 let stream = connector
1012 .connect(name, tcp)
1013 .await
1014 .map_err(|error| Failure::Retriable(format!("token endpoint TLS: {error}")))?;
1015 http_post(stream, request).await
1016 } else {
1017 http_post(tcp, request).await
1018 }
1019 };
1020 within(read_timeout, exchange)
1021 .await
1022 .map_err(|()| Failure::Retriable("token endpoint read timed out".into()))?
1023}
1024
1025#[derive(Debug, PartialEq, Eq)]
1027struct Endpoint {
1028 https: bool,
1029 host: String,
1030 port: u16,
1031 target: String,
1033}
1034
1035impl Endpoint {
1036 fn parse(url: &str) -> Result<Self, String> {
1037 let invalid =
1038 || format!("sasl.oauthbearer.token.endpoint.url {url:?} is not an http or https URL");
1039 let (https, rest) = if let Some(rest) = url.strip_prefix("https://") {
1040 (true, rest)
1041 } else if let Some(rest) = url.strip_prefix("http://") {
1042 (false, rest)
1043 } else {
1044 return Err(invalid());
1045 };
1046 let authority_end = rest.find(['/', '?', '#']).unwrap_or(rest.len());
1050 let (authority, after) = rest.split_at(authority_end);
1051 let after = after.split('#').next().unwrap_or("");
1052 let target = match after.as_bytes().first() {
1053 None => "/".to_owned(),
1054 Some(b'?') => format!("/{after}"),
1055 Some(_) => after.to_owned(),
1056 };
1057 let default_port = if https { 443 } else { 80 };
1058 let (host, port) = if let Some(bracketed) = authority.strip_prefix('[') {
1059 let (host, after) = bracketed.split_once(']').ok_or_else(invalid)?;
1060 let port = match after.strip_prefix(':') {
1061 Some(port) => port.parse().map_err(|_| invalid())?,
1062 None => default_port,
1063 };
1064 (host.to_owned(), port)
1065 } else {
1066 match authority.rsplit_once(':') {
1067 Some((host, port)) => (host.to_owned(), port.parse().map_err(|_| invalid())?),
1068 None => (authority.to_owned(), default_port),
1069 }
1070 };
1071 if host.is_empty() {
1072 return Err(invalid());
1073 }
1074 Ok(Self {
1075 https,
1076 host,
1077 port,
1078 target,
1079 })
1080 }
1081}
1082
1083async fn within<F: Future>(limit: Option<Duration>, future: F) -> Result<F::Output, ()> {
1084 match limit {
1085 Some(limit) => timeout(limit, future).await.map_err(|_| ()),
1086 None => Ok(future.await),
1087 }
1088}
1089
1090async fn http_post<S>(mut stream: S, request: &TokenRequest) -> Result<String, Failure>
1092where
1093 S: AsyncRead + AsyncWrite + Unpin,
1094{
1095 let endpoint = &request.endpoint;
1096 let host = if endpoint.host.contains(':') {
1097 format!("[{}]", endpoint.host)
1098 } else {
1099 endpoint.host.clone()
1100 };
1101 let authorization = request
1102 .authorization
1103 .as_deref()
1104 .map_or_else(String::new, |value| format!("Authorization: {value}\r\n"));
1105 let head = format!(
1106 "POST {} HTTP/1.1\r\nHost: {host}:{}\r\nAccept: application/json\r\n{authorization}\
1107 Cache-Control: no-cache\r\nContent-Type: application/x-www-form-urlencoded\r\n\
1108 Content-Length: {}\r\nConnection: close\r\n\r\n",
1109 endpoint.target,
1110 endpoint.port,
1111 request.body.len()
1112 );
1113 let io = |error: io::Error| Failure::Retriable(format!("token endpoint I/O: {error}"));
1114 stream.write_all(head.as_bytes()).await.map_err(io)?;
1115 stream
1116 .write_all(request.body.as_bytes())
1117 .await
1118 .map_err(io)?;
1119 stream.flush().await.map_err(io)?;
1120 let mut response = Vec::new();
1121 (&mut stream)
1122 .take(u64::try_from(MAX_RESPONSE_BYTES).unwrap_or(u64::MAX))
1123 .read_to_end(&mut response)
1124 .await
1125 .map_err(io)?;
1126 let (status, body) = parse_http_response(&response).map_err(Failure::Retriable)?;
1127 match status {
1128 200 | 201 => Ok(body),
1129 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 409 | 410 | 411 | 412 | 413 | 414 | 415
1131 | 501 | 505 => Err(Failure::Final(format!(
1132 "the token endpoint answered {status}: {}",
1133 snippet(&body)
1134 ))),
1135 status => Err(Failure::Retriable(format!(
1136 "the token endpoint answered {status}: {}",
1137 snippet(&body)
1138 ))),
1139 }
1140}
1141
1142fn parse_http_response(response: &[u8]) -> Result<(u16, String), String> {
1148 let (head, body) = split_once_bytes(response, b"\r\n\r\n")
1149 .ok_or_else(|| "incomplete token endpoint response".to_owned())?;
1150 let head = String::from_utf8_lossy(head).into_owned();
1151 let mut lines = head.split("\r\n");
1152 let status = lines
1153 .next()
1154 .and_then(|line| line.split(' ').nth(1))
1155 .and_then(|code| code.parse::<u16>().ok())
1156 .ok_or_else(|| "invalid token endpoint status line".to_owned())?;
1157 let chunked = lines.any(|line| {
1158 line.split_once(':').is_some_and(|(name, value)| {
1159 name.trim().eq_ignore_ascii_case("transfer-encoding")
1160 && value.trim().eq_ignore_ascii_case("chunked")
1161 })
1162 });
1163 let body = if chunked {
1164 dechunk(body)?
1165 } else {
1166 body.to_vec()
1167 };
1168 let body = String::from_utf8(body)
1169 .map_err(|_| "the token endpoint response is not UTF-8".to_owned())?;
1170 Ok((status, body))
1171}
1172
1173fn dechunk(mut body: &[u8]) -> Result<Vec<u8>, String> {
1175 let mut out = Vec::new();
1176 loop {
1177 let (size_line, rest) = split_once_bytes(body, b"\r\n")
1178 .ok_or_else(|| "invalid chunked token endpoint response".to_owned())?;
1179 let size = String::from_utf8_lossy(size_line);
1180 let size = usize::from_str_radix(size.split(';').next().unwrap_or("").trim(), 16)
1181 .map_err(|_| "invalid chunk size in token endpoint response".to_owned())?;
1182 if size == 0 {
1183 return Ok(out);
1184 }
1185 let chunk = rest
1186 .get(..size)
1187 .ok_or_else(|| "truncated chunk in token endpoint response".to_owned())?;
1188 out.extend_from_slice(chunk);
1189 body = rest.get(size + 2..).unwrap_or(&[]);
1190 }
1191}
1192
1193fn split_once_bytes<'a>(haystack: &'a [u8], needle: &[u8]) -> Option<(&'a [u8], &'a [u8])> {
1195 let at = haystack
1196 .windows(needle.len())
1197 .position(|window| window == needle)?;
1198 let (head, rest) = haystack.split_at_checked(at)?;
1199 Some((head, rest.get(needle.len()..)?))
1200}
1201
1202fn snippet(body: &str) -> String {
1203 if body.len() <= MAX_RESPONSE_SNIPPET {
1204 return body.to_owned();
1205 }
1206 let head: String = body
1207 .char_indices()
1208 .take_while(|(index, character)| index + character.len_utf8() <= MAX_RESPONSE_SNIPPET)
1209 .map(|(_, character)| character)
1210 .collect();
1211 format!(
1212 "{head} (trimmed to first {MAX_RESPONSE_SNIPPET} characters out of {} total)",
1213 body.len()
1214 )
1215}
1216
1217fn parse_token(body: &str) -> Result<String, String> {
1219 let json: serde_json::Value = serde_json::from_str(body)
1220 .map_err(|error| format!("invalid token endpoint response: {error}"))?;
1221 ["access_token", "id_token"]
1222 .into_iter()
1223 .filter_map(|name| json.get(name).and_then(serde_json::Value::as_str))
1224 .map(str::trim)
1225 .find(|value| !value.is_empty())
1226 .map(ToOwned::to_owned)
1227 .ok_or_else(|| {
1228 format!(
1229 "The token endpoint response did not contain a valid JWT. Response: ({})",
1230 snippet(body)
1231 )
1232 })
1233}
1234
1235fn token_times(token: &str) -> Result<(Option<SystemTime>, SystemTime), String> {
1238 let payload = token
1239 .split('.')
1240 .nth(1)
1241 .ok_or_else(|| "the token is not a JWT".to_owned())?;
1242 let json: serde_json::Value = URL_SAFE_NO_PAD
1243 .decode(payload.trim_end_matches('='))
1244 .ok()
1245 .and_then(|bytes| serde_json::from_slice(&bytes).ok())
1246 .ok_or_else(|| "the JWT payload is not base64url JSON".to_owned())?;
1247 let time = |name: &str| {
1250 json.get(name)
1251 .and_then(serde_json::Value::as_f64)
1252 .filter(|seconds| seconds.is_finite() && *seconds >= 0.0)
1253 .map(|seconds| UNIX_EPOCH + Duration::from_secs_f64(seconds))
1254 };
1255 let expire = time("exp").ok_or_else(|| "the JWT has no exp claim".to_owned())?;
1256 Ok((time("iat"), expire))
1257}
1258
1259fn form_urlencode(value: &str) -> String {
1261 value
1262 .bytes()
1263 .map(|byte| match byte {
1264 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'.' | b'-' | b'*' | b'_' => {
1265 char::from(byte).to_string()
1266 }
1267 b' ' => "+".to_owned(),
1268 other => format!("%{other:02X}"),
1269 })
1270 .collect()
1271}
1272
1273#[cfg(test)]
1274mod tests;