Skip to main content

krabka_client_core/
oauth.rs

1//! OAUTHBEARER tokens: the token provider seam, an OAuth 2 client credentials
2//! provider and a jwt-bearer client assertion provider (KIP-768), and a
3//! background refresh task for either.
4//!
5//! Kafka's `OAuthBearerLoginCallbackHandler` gets a token from a
6//! `JwtRetriever`. `ClientCredentialsJwtRetriever` posts a
7//! `client_credentials` grant to `sasl.oauthbearer.token.endpoint.url`,
8//! `JwtBearerJwtRetriever` instead posts the
9//! `urn:ietf:params:oauth:grant-type:jwt-bearer` grant with a signed client
10//! assertion, and `ExpiringCredentialRefreshingLogin` fetches a new token
11//! before the old one expires. [`spawn_background_refresh`] is this crate's
12//! equivalent of that last part.
13
14use 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
46/// The future of [`OAuthBearerTokenProvider::token`].
47pub type TokenFuture<'a> = Pin<Box<dyn Future<Output = Result<String, String>> + Send + 'a>>;
48
49/// The future of [`OAuthBearerTokenProvider::refresh_at`].
50pub type RefreshFuture<'a> = Pin<Box<dyn Future<Output = Option<SystemTime>> + Send + 'a>>;
51
52/// A source of OAUTHBEARER tokens, as Kafka's `sasl.login.callback.handler.class`
53/// with an `OAuthBearerTokenCallback` is.
54///
55/// Each SASL exchange, including a re-authentication, asks for a token.
56pub trait OAuthBearerTokenProvider: fmt::Debug + Send + Sync {
57    /// The compact serialized token to send.
58    ///
59    /// # Errors
60    /// Returns a description when no token is available.
61    fn token(&self) -> TokenFuture<'_>;
62
63    /// Fetch a new token even when a cached one has not reached its refresh
64    /// time, as Kafka's `ExpiringCredentialRefreshingLogin` re-logs in once
65    /// its refresh timer fires. [`spawn_background_refresh`] calls this at
66    /// each [`Self::refresh_at`]; a provider that caches nothing can leave
67    /// the default, which is [`Self::token`].
68    ///
69    /// # Errors
70    /// Returns a description when no token is available.
71    fn refresh(&self) -> TokenFuture<'_> {
72        self.token()
73    }
74
75    /// The time a cached token should next be refreshed, once
76    /// [`Self::token`] has fetched one. [`spawn_background_refresh`] polls
77    /// this to know when to wake up and fetch proactively; a provider that
78    /// caches nothing can leave the default, which never schedules a
79    /// background fetch.
80    fn refresh_at(&self) -> RefreshFuture<'_> {
81        Box::pin(async { None })
82    }
83
84    /// Whether [`spawn_background_refresh`] should run a background task for
85    /// this provider.
86    ///
87    /// Kafka's `ExpiringCredentialRefreshingLogin` background thread always
88    /// runs once a login completes. krabka instead makes starting the
89    /// equivalent task an explicit call, so building a provider outside a
90    /// Tokio runtime never panics; this flag is each provider's own opt-in
91    /// for that call, and it defaults to `true` so a caller that does invoke
92    /// [`spawn_background_refresh`] gets Kafka's normal, always-on behavior
93    /// unless it explicitly turns the flag off.
94    fn background_refresh_enabled(&self) -> bool {
95        true
96    }
97}
98
99/// Starts a background task that keeps `provider`'s token fresh.
100///
101/// The task calls
102/// [`OAuthBearerTokenProvider::refresh`] at the time
103/// [`OAuthBearerTokenProvider::refresh_at`] reports, rather than waiting for
104/// the next SASL exchange to notice the cached token needs a refresh. This is
105/// krabka's equivalent of Kafka's `ExpiringCredentialRefreshingLogin`
106/// background thread.
107///
108/// Returns `None` when
109/// [`background_refresh_enabled`](OAuthBearerTokenProvider::background_refresh_enabled)
110/// says not to. Otherwise the task runs until its `JoinHandle` is dropped or
111/// aborted, so the caller should abort it (dropping the last reference to
112/// `provider`) once the client using it shuts down.
113#[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
137/// Kafka's `sasl.login.retry.backoff.ms` default.
138pub const DEFAULT_LOGIN_RETRY_BACKOFF: Duration = Duration::from_millis(100);
139/// Kafka's `sasl.login.retry.backoff.max.ms` default.
140pub const DEFAULT_LOGIN_RETRY_BACKOFF_MAX: Duration = Duration::from_secs(10);
141/// Kafka's `sasl.login.refresh.window.factor` default.
142pub const DEFAULT_LOGIN_REFRESH_WINDOW_FACTOR: f64 = 0.80;
143/// Kafka's `sasl.login.refresh.window.jitter` default.
144pub const DEFAULT_LOGIN_REFRESH_WINDOW_JITTER: f64 = 0.05;
145/// Kafka's `sasl.login.refresh.min.period.seconds` default.
146pub const DEFAULT_LOGIN_REFRESH_MIN_PERIOD: Duration = Duration::from_mins(1);
147/// Kafka's `sasl.login.refresh.buffer.seconds` default.
148pub const DEFAULT_LOGIN_REFRESH_BUFFER: Duration = Duration::from_mins(5);
149/// Kafka's `JwtResponseParser.MAX_RESPONSE_BODY_LENGTH`.
150const MAX_RESPONSE_SNIPPET: usize = 1000;
151/// The largest token endpoint response that the client reads.
152const MAX_RESPONSE_BYTES: usize = 1024 * 1024;
153
154/// The settings of [`ClientCredentialsTokenProvider`], as Kafka's
155/// `SaslConfigs` names them.
156#[derive(Clone, Debug)]
157pub struct ClientCredentialsConfig {
158    /// `sasl.oauthbearer.token.endpoint.url`, an `http` or `https` URL.
159    pub token_endpoint_url: String,
160    /// `sasl.oauthbearer.client.credentials.client.id`.
161    pub client_id: String,
162    /// `sasl.oauthbearer.client.credentials.client.secret`.
163    pub client_secret: Password,
164    /// `sasl.oauthbearer.scope`. `None` requests the default scope.
165    pub scope: Option<String>,
166    /// `sasl.oauthbearer.header.urlencode`: URL-encode the client id, secret
167    /// and scope (RFC 6749 section 2.3.1). Kafka's default is `false`.
168    pub url_encode: bool,
169    /// The TLS settings of an `https` endpoint.
170    pub tls: TlsConnectorConfig,
171    /// `sasl.login.connect.timeout.ms`. `None` has no limit.
172    pub connect_timeout: Option<Duration>,
173    /// `sasl.login.read.timeout.ms`. `None` has no limit.
174    pub read_timeout: Option<Duration>,
175    /// `sasl.login.retry.backoff.ms`.
176    pub retry_backoff: Duration,
177    /// `sasl.login.retry.backoff.max.ms`.
178    pub retry_backoff_max: Duration,
179    /// `sasl.login.refresh.window.factor`.
180    pub refresh_window_factor: f64,
181    /// `sasl.login.refresh.window.jitter`.
182    pub refresh_window_jitter: f64,
183    /// `sasl.login.refresh.min.period.seconds`.
184    pub refresh_min_period: Duration,
185    /// `sasl.login.refresh.buffer.seconds`.
186    pub refresh_buffer: Duration,
187    /// [`OAuthBearerTokenProvider::background_refresh_enabled`].
188    pub background_refresh: bool,
189}
190
191impl ClientCredentialsConfig {
192    /// Settings with Kafka's defaults for `endpoint`, `client_id` and
193    /// `client_secret`.
194    #[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/// A token and the time to fetch the next one.
221#[derive(Debug)]
222struct Cached {
223    token: String,
224    refresh_at: SystemTime,
225}
226
227/// An OAUTHBEARER token provider that uses the OAuth 2 client credentials
228/// grant, as Kafka's `ClientCredentialsJwtRetriever` with
229/// `ClientSecretRequestFormatter` does.
230///
231/// It keeps the token until the refresh
232/// time of Kafka's `ExpiringCredentialRefreshingLogin`, then fetches a new one.
233#[derive(Debug)]
234pub struct ClientCredentialsTokenProvider {
235    config: ClientCredentialsConfig,
236    cached: Mutex<Option<Cached>>,
237}
238
239impl ClientCredentialsTokenProvider {
240    /// A provider for `config`.
241    ///
242    /// # Errors
243    /// Returns a description when the client id or secret is blank, or the
244    /// endpoint is not an `http` or `https` URL, as Kafka's `ConfigException`.
245    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        // Kafka's `ExpiringCredentialRefreshConfig` refuses a window factor or
254        // jitter outside its range. A bad one would reach
255        // `Duration::mul_f64` and panic at the first token.
256        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    /// Fetch a new token as of `now` and store it with its refresh time in
282    /// `cached`.
283    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    /// The headers and body of `ClientSecretRequestFormatter`.
314    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    /// The refresh time of Kafka's
357    /// `ExpiringCredentialRefreshingLogin.refreshMs` for a token issued at
358    /// `start` and expiring at `expire`, now being `now`.
359    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/// The window settings of Kafka's `ExpiringCredentialRefreshConfig`:
420/// `sasl.login.refresh.window.factor`, `.jitter`, `sasl.login.refresh.min.period.seconds`
421/// and `sasl.login.refresh.buffer.seconds`.
422#[derive(Clone, Copy, Debug)]
423struct RefreshWindow {
424    factor: f64,
425    jitter: f64,
426    min_period: Duration,
427    buffer: Duration,
428}
429
430/// Kafka's `ExpiringCredentialRefreshingLogin.refreshMs` for a token issued at
431/// `start` and expiring at `expire`, now being `now`, with `window`'s factor
432/// plus `window.jitter * unit`, no sooner than `window.min_period` from `now`
433/// and no later than `window.buffer` before `expire`.
434fn 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
461/// Kafka's `urn:ietf:params:oauth:grant-type:jwt-bearer` grant type.
462const JWT_BEARER_GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:jwt-bearer";
463
464/// The settings of [`JwtBearerTokenProvider`], as Kafka's `SaslConfigs` names
465/// them.
466#[derive(Clone, Debug)]
467pub struct JwtBearerConfig {
468    /// `sasl.oauthbearer.token.endpoint.url`, an `http` or `https` URL.
469    pub token_endpoint_url: String,
470    /// `sasl.oauthbearer.assertion.algorithm`. Only `RS256` is implemented.
471    pub algorithm: String,
472    /// `sasl.oauthbearer.assertion.claim.exp.seconds`: how far past `iat` the
473    /// assertion's `exp` claim is set.
474    pub exp_seconds: u64,
475    /// `sasl.oauthbearer.assertion.claim.nbf.seconds`: how far before `iat`
476    /// the assertion's `nbf` claim is set.
477    pub nbf_seconds: u64,
478    /// `sasl.oauthbearer.assertion.claim.jti.include`: add a random `jti`
479    /// claim to the assertion.
480    pub include_jti: bool,
481    /// `sasl.oauthbearer.assertion.claim.iss`.
482    pub iss: Option<String>,
483    /// `sasl.oauthbearer.assertion.claim.sub`.
484    pub sub: Option<String>,
485    /// `sasl.oauthbearer.assertion.claim.aud`.
486    pub aud: Option<String>,
487    /// `sasl.oauthbearer.scope`. `None` requests the default scope.
488    pub scope: Option<String>,
489    /// `sasl.oauthbearer.assertion.private.key.file`: a PEM file with one RSA
490    /// private key, PKCS#8 (`PRIVATE KEY`), PKCS#8 encrypted with PBES2
491    /// (`ENCRYPTED PRIVATE KEY`, decrypted with
492    /// [`Self::private_key_passphrase`]), or PKCS#1 (`RSA PRIVATE KEY`).
493    pub private_key_file: PathBuf,
494    /// `sasl.oauthbearer.assertion.private.key.passphrase`.
495    pub private_key_passphrase: Option<Password>,
496    /// `sasl.oauthbearer.assertion.template.file`: a JSON file of the shape
497    /// `{"header": {...}, "payload": {...}}`, either key optional. Its
498    /// entries seed the assertion's header and payload, and the generated
499    /// `alg`, `typ`, `iat`, `exp`, `nbf` and (if configured) `jti` then
500    /// overwrite same-named entries, as `LayeredAssertionJwtTemplate` layers
501    /// the static claims, the template file and the dynamic claims, each on
502    /// top of the last.
503    pub template_file: Option<PathBuf>,
504    /// The TLS settings of an `https` endpoint.
505    pub tls: TlsConnectorConfig,
506    /// `sasl.login.connect.timeout.ms`. `None` has no limit.
507    pub connect_timeout: Option<Duration>,
508    /// `sasl.login.read.timeout.ms`. `None` has no limit.
509    pub read_timeout: Option<Duration>,
510    /// `sasl.login.retry.backoff.ms`.
511    pub retry_backoff: Duration,
512    /// `sasl.login.retry.backoff.max.ms`.
513    pub retry_backoff_max: Duration,
514    /// `sasl.login.refresh.window.factor`.
515    pub refresh_window_factor: f64,
516    /// `sasl.login.refresh.window.jitter`.
517    pub refresh_window_jitter: f64,
518    /// `sasl.login.refresh.min.period.seconds`.
519    pub refresh_min_period: Duration,
520    /// `sasl.login.refresh.buffer.seconds`.
521    pub refresh_buffer: Duration,
522    /// [`OAuthBearerTokenProvider::background_refresh_enabled`].
523    pub background_refresh: bool,
524}
525
526impl JwtBearerConfig {
527    /// Settings with Kafka's defaults for `endpoint` and `private_key_file`:
528    /// `RS256`, a 300 s `exp` claim, a 60 s `nbf` claim, and no `jti`, `iss`,
529    /// `sub`, `aud`, scope or template file.
530    #[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
559/// A signed JSON claim map, kept in the order the file or config gave it so a
560/// deterministic assertion is easy to test.
561type ClaimMap = serde_json::Map<String, serde_json::Value>;
562
563/// An OAUTHBEARER token provider that uses the OAuth 2 jwt-bearer grant with a
564/// signed client assertion (RFC 7523), as Kafka's `JwtBearerJwtRetriever` with
565/// `JwtBearerRequestFormatter` does.
566///
567/// It keeps the token until the refresh time
568/// of Kafka's `ExpiringCredentialRefreshingLogin`, then fetches a new one.
569pub 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    /// A provider for `config`.
588    ///
589    /// # Errors
590    /// Returns a description when the algorithm is not `RS256`, the endpoint
591    /// is not an `http` or `https` URL, the private key file or template file
592    /// cannot be read or parsed, or a refresh window setting is out of range,
593    /// as Kafka's `ConfigException`.
594    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        // Kafka's `ExpiringCredentialRefreshConfig` refuses a window factor or
604        // jitter outside its range. A bad one would reach `Duration::mul_f64`
605        // and panic at the first token.
606        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    /// Fetch a new token as of `now` and store it with its refresh time in
649    /// `cached`.
650    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    /// The header and payload of `LayeredAssertionJwtTemplate`: the template
681    /// file's entries (if any) sit under the static `iss`/`sub`/`aud` claims,
682    /// and the dynamic `alg`, `typ`, `iat`, `exp`, `nbf` and `jti` claims are
683    /// generated fresh and win any conflict, as
684    /// `AssertionSupplierFactory.layeredAssertionJwtTemplate` orders its
685    /// layers.
686    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    /// The signed compact JWT assertion of `DefaultAssertionCreator`.
721    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    /// The headers and body of `JwtBearerRequestFormatter.formatBody`: the
736    /// jwt-bearer grant with the signed assertion, and the optional scope. No
737    /// `Authorization` header is sent.
738    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    /// The refresh time of Kafka's
773    /// `ExpiringCredentialRefreshingLogin.refreshMs` for a token issued at
774    /// `start` and expiring at `expire`, now being `now`.
775    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
835/// Sign `message` with `key_pair`, as `DefaultAssertionCreator` does with
836/// Kafka's `sasl.oauthbearer.assertion.algorithm` `RS256`.
837fn 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
846/// The one PEM block of `text`, as `(label, DER)`.
847fn 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
866/// Decrypt a PKCS#8 `EncryptedPrivateKeyInfo` with PBES2, as Kafka's
867/// `PemStore.privateKey` does with `sasl.oauthbearer.assertion.private.key.passphrase`.
868fn 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
877/// Load the one RSA private key of a PEM file, as `DefaultAssertionCreator`
878/// does for `sasl.oauthbearer.assertion.private.key.file`. Accepts a PKCS#8
879/// key (`PRIVATE KEY`), a PKCS#8 key encrypted with PBES2
880/// (`ENCRYPTED PRIVATE KEY`, decrypted with `passphrase`), or a PKCS#1 RSA
881/// key (`RSA PRIVATE KEY`).
882fn 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
911/// Load an assertion template file's `header` and `payload` maps, as
912/// `FileAssertionJwtTemplate` does for
913/// `sasl.oauthbearer.assertion.template.file`. Either key may be absent,
914/// which is the same as an empty map.
915fn 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
931/// How a token endpoint request failed.
932enum Failure {
933    /// Kafka's `HttpJwtRetriever` retries it.
934    Retriable(String),
935    /// An `UnretryableException`: fail at once.
936    Final(String),
937}
938
939struct TokenRequest {
940    endpoint: Endpoint,
941    /// `None` for the jwt-bearer grant: `JwtBearerRequestFormatter` sends no
942    /// `Authorization` header, since the assertion itself authenticates the
943    /// client.
944    authorization: Option<String>,
945    body: String,
946}
947
948/// Kafka's `Retry.execute`: post `request` with `attempt`, retrying a
949/// [`Failure::Retriable`] until `retry_backoff_max` has passed since the
950/// first attempt, doubling the wait each time, and returning the parsed
951/// access or id token of a success.
952async 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
983/// Connect to `request`'s endpoint, over TLS with `tls` when it is `https`,
984/// and post it, as `HttpJwtRetriever.doPost` does.
985async 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            // `tls.server_name` overrides the SNI name, as it does on a
1003            // broker connection.
1004            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/// A parsed token endpoint URL.
1026#[derive(Debug, PartialEq, Eq)]
1027struct Endpoint {
1028    https: bool,
1029    host: String,
1030    port: u16,
1031    /// The path and query, starting with `/`.
1032    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        // The authority ends at the first `/`, `?` or `#`, as RFC 3986 says.
1047        // A query with no path still posts to `/?...`, and a fragment never
1048        // goes on the wire.
1049        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
1090/// Send the POST of `HttpJwtRetriever.handleInput` and read the response.
1091async 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        // Kafka's `HttpJwtRetriever.UNRETRYABLE_HTTP_CODES`.
1130        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
1142/// The status code and body of an HTTP/1.1 response that ends at EOF.
1143///
1144/// The body stays bytes until the chunk framing is off it. A chunk boundary
1145/// may cut a multibyte character in two, so a decode before the dechunk would
1146/// move every later byte offset.
1147fn 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
1173/// Strip the chunk framing of a chunked body, in bytes.
1174fn 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
1193/// The bytes of `haystack` before and after the first `needle`.
1194fn 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
1217/// Kafka's `JwtResponseParser.parseJwt`: `access_token`, else `id_token`.
1218fn 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
1235/// The `iat` and `exp` times of a compact JWT. The client does not verify the
1236/// signature: the broker validates the token.
1237fn 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    // RFC 7519 NumericDate is a JSON number, so a provider may send a
1248    // fractional second. A negative or unreal value has no time.
1249    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
1259/// `application/x-www-form-urlencoded`, as Java's `URLEncoder.encode` writes it.
1260fn 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;