Skip to main content

scm_sdk/
auth.rs

1//! Explicit authentication and isolated, caller-driven OAuth refresh.
2//!
3//! A provider owns one immutable issuer/client/credential-generation/tenant/scope
4//! identity. Refresh has no detached task: concurrent callers share its future and
5//! the transport future drops when its last waiter drops. Ordinary formatting
6//! exposes no credential, tenant identifier, scope, audience, or issuer URL.
7
8use std::{
9    fmt,
10    sync::{Arc, Mutex},
11    time::Duration,
12};
13
14use base64::{Engine as _, engine::general_purpose::STANDARD};
15use bytes::Bytes;
16use futures_util::{
17    FutureExt,
18    future::{BoxFuture, Shared, WeakShared},
19};
20use hmac::{Hmac, Mac};
21use http::{
22    HeaderMap, HeaderValue, Method,
23    header::{AUTHORIZATION, CONTENT_TYPE},
24};
25use serde::Deserialize;
26use sha2::Sha256;
27use tokio::time::Instant;
28use zeroize::Zeroizing;
29
30use crate::{
31    Error, ErrorKind, Result, Secret,
32    config::{DestinationPolicy, DestinationPurpose, Endpoint, RequestOptions, Session},
33    transport::{Transport, TransportRequest, read_limited},
34};
35
36const MAX_TOKEN_RESPONSE: usize = 64 * 1024;
37const MAX_REFRESH_TIME: Duration = Duration::from_secs(30);
38const MAX_TOKEN_LIFETIME: Duration = Duration::from_secs(31 * 24 * 60 * 60);
39
40/// Immutable early-refresh policy. Fixed buffers preserve the source's absolute window.
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42#[non_exhaustive]
43pub enum RefreshBuffer {
44    /// Cap the chosen maximum at 20% of the issued TTL so short leases remain usable.
45    Adaptive(Duration),
46    /// An absolute window; zero disables early refresh and a window >= TTL refreshes each call.
47    Fixed(Duration),
48}
49impl Default for RefreshBuffer {
50    fn default() -> Self {
51        Self::Adaptive(Duration::from_secs(30))
52    }
53}
54impl RefreshBuffer {
55    fn window(self, ttl: Duration) -> Duration {
56        match self {
57            Self::Adaptive(maximum) => maximum.min(ttl / 5),
58            Self::Fixed(buffer) => buffer,
59        }
60    }
61}
62
63/// Provider policy is fixed before the cache or its subscribers are created.
64#[derive(Clone, Debug)]
65#[non_exhaustive]
66pub struct OAuthProviderOptions {
67    pub refresh_buffer: RefreshBuffer,
68    /// Bounded metadata history. Slow receivers explicitly observe lag instead of blocking refresh.
69    pub event_capacity: usize,
70}
71impl Default for OAuthProviderOptions {
72    fn default() -> Self {
73        Self {
74            refresh_buffer: RefreshBuffer::default(),
75            event_capacity: 16,
76        }
77    }
78}
79impl OAuthProviderOptions {
80    pub fn validate(&self) -> Result<()> {
81        if !(1..=1024).contains(&self.event_capacity) {
82            return Err(Error::new(ErrorKind::Configuration));
83        }
84        Ok(())
85    }
86}
87
88/// Immutable identity used to obtain a service token. Formatting is redacted.
89#[derive(Clone)]
90pub struct AuthContext {
91    issuer: Endpoint,
92    client_id: String,
93    credential_generation: u64,
94    tsg_id: String,
95    scopes: Vec<String>,
96    audience: Option<String>,
97}
98
99impl AuthContext {
100    /// Default scope is exactly `tsg_id:<tenant>`. Each provider isolates its cache.
101    pub fn new(
102        issuer: Endpoint,
103        client_id: impl Into<String>,
104        credential_generation: u64,
105        tsg_id: impl Into<String>,
106    ) -> Result<Self> {
107        let client_id = client_id.into();
108        let tsg_id = tsg_id.into();
109        if client_id.is_empty()
110            || client_id.len() > 1024
111            || client_id.chars().any(char::is_control)
112            || tsg_id.is_empty()
113            || tsg_id.len() > 32
114            || !tsg_id.bytes().all(|value| value.is_ascii_digit())
115        {
116            return Err(Error::new(ErrorKind::Configuration));
117        }
118        let scopes = vec![format!("tsg_id:{tsg_id}")];
119        Ok(Self {
120            issuer,
121            client_id,
122            credential_generation,
123            tsg_id,
124            scopes,
125            audience: None,
126        })
127    }
128
129    /// Supply explicit scopes including this identity's tenant scope.
130    /// Conflicting tenant scopes and duplicate scopes are rejected.
131    pub fn with_scopes(mut self, scopes: Vec<String>) -> Result<Self> {
132        let tenant_scope = format!("tsg_id:{}", self.tsg_id);
133        if scopes.is_empty()
134            || scopes.len() > 64
135            || !scopes.contains(&tenant_scope)
136            || scopes.iter().enumerate().any(|(index, scope)| {
137                scope.is_empty()
138                    || scope.len() > 512
139                    || !scope
140                        .bytes()
141                        .all(|value| matches!(value, 0x21 | 0x23..=0x5b | 0x5d..=0x7e))
142                    || (scope.starts_with("tsg_id:") && scope != &tenant_scope)
143                    || scopes[..index].contains(scope)
144            })
145        {
146            return Err(Error::new(ErrorKind::Configuration));
147        }
148        self.scopes = scopes;
149        Ok(self)
150    }
151
152    /// Optional service audience, sent as an encoded form field.
153    pub fn with_audience(mut self, audience: impl Into<String>) -> Result<Self> {
154        let audience = audience.into();
155        if audience.trim().is_empty()
156            || audience.len() > 2048
157            || audience.chars().any(char::is_control)
158        {
159            return Err(Error::new(ErrorKind::Configuration));
160        }
161        self.audience = Some(audience);
162        Ok(self)
163    }
164
165    pub fn issuer(&self) -> &Endpoint {
166        &self.issuer
167    }
168    pub fn client_id(&self) -> &str {
169        &self.client_id
170    }
171    pub fn credential_generation(&self) -> u64 {
172        self.credential_generation
173    }
174    pub fn tsg_id(&self) -> &str {
175        &self.tsg_id
176    }
177    pub fn scopes(&self) -> &[String] {
178        &self.scopes
179    }
180    pub fn audience(&self) -> Option<&str> {
181        self.audience.as_deref()
182    }
183}
184
185impl fmt::Debug for AuthContext {
186    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187        f.debug_struct("AuthContext")
188            .field("identity", &"[REDACTED]")
189            .field("credential_generation", &self.credential_generation)
190            .field("scope_count", &self.scopes.len())
191            .field("has_audience", &self.audience.is_some())
192            .finish()
193    }
194}
195
196/// A bearer token and the cache generation that supplied it.
197/// Explicit secret access is available; formatting redacts the token.
198#[derive(Clone)]
199pub struct TokenLease {
200    token: Secret,
201    generation: u64,
202    expires_at: Instant,
203    refresh_at: Instant,
204    expires_at_wall: std::time::SystemTime,
205}
206
207impl TokenLease {
208    /// Construct a lease for an application-provided token provider.
209    pub fn new(token: Secret, generation: u64, valid_for: Duration) -> Result<Self> {
210        Self::issued(token, generation, Instant::now(), valid_for)
211    }
212
213    fn issued(
214        token: Secret,
215        generation: u64,
216        issued_at: Instant,
217        valid_for: Duration,
218    ) -> Result<Self> {
219        Self::issued_with_buffer(
220            token,
221            generation,
222            issued_at,
223            valid_for,
224            RefreshBuffer::default(),
225        )
226    }
227
228    fn issued_with_buffer(
229        token: Secret,
230        generation: u64,
231        issued_at: Instant,
232        valid_for: Duration,
233        refresh_buffer: RefreshBuffer,
234    ) -> Result<Self> {
235        validate_bearer(token.expose())?;
236        if valid_for.is_zero() || valid_for > MAX_TOKEN_LIFETIME {
237            return Err(Error::new(ErrorKind::Authentication));
238        }
239        let expires_at = issued_at
240            .checked_add(valid_for)
241            .ok_or_else(|| Error::new(ErrorKind::Authentication))?;
242        if expires_at <= Instant::now() {
243            return Err(Error::new(ErrorKind::Authentication));
244        }
245        let buffer = refresh_buffer.window(valid_for);
246        let expires_at_wall = std::time::SystemTime::now()
247            .checked_add(expires_at.saturating_duration_since(Instant::now()))
248            .ok_or_else(|| Error::new(ErrorKind::Authentication))?;
249        Ok(Self {
250            token,
251            generation,
252            expires_at,
253            refresh_at: issued_at + valid_for.saturating_sub(buffer),
254            expires_at_wall,
255        })
256    }
257
258    pub fn token(&self) -> &Secret {
259        &self.token
260    }
261    pub fn secret(&self) -> &Secret {
262        &self.token
263    }
264    pub fn generation(&self) -> u64 {
265        self.generation
266    }
267    pub fn expires_at(&self) -> std::time::Instant {
268        self.expires_at.into_std()
269    }
270    pub fn remaining(&self) -> Duration {
271        self.expires_at.saturating_duration_since(Instant::now())
272    }
273    fn usable(&self) -> bool {
274        self.refresh_at > Instant::now()
275    }
276}
277
278impl fmt::Debug for TokenLease {
279    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
280        f.debug_struct("TokenLease")
281            .field("token", &"[REDACTED]")
282            .field("generation", &self.generation)
283            .field("remaining", &self.remaining())
284            .finish()
285    }
286}
287
288/// Safe token-cache snapshot, containing no token or identity values.
289#[derive(Clone, Copy, Debug, PartialEq, Eq)]
290#[non_exhaustive]
291pub struct TokenInfo {
292    pub cached: bool,
293    pub generation: Option<u64>,
294    pub remaining: Option<Duration>,
295    pub refresh_in_progress: bool,
296    pub is_valid: bool,
297    pub is_expired: bool,
298    pub is_expiring_soon: bool,
299    /// Wall-clock expiry snapshot; cache validity uses a monotonic clock.
300    pub expires_at: Option<std::time::SystemTime>,
301}
302
303/// Service-token integration point; provider implementations own credential policy.
304pub trait TokenProvider: Send + Sync {
305    fn token<'a>(&'a self, options: &'a RequestOptions) -> BoxFuture<'a, Result<TokenLease>>;
306    fn context(&self) -> &AuthContext;
307    /// Invalidate only the generation observed by the unauthorized request.
308    fn invalidate(&self, generation: u64) -> bool;
309}
310
311type Refresh = BoxFuture<'static, Result<TokenLease>>;
312struct Pending {
313    generation: u64,
314    future: WeakShared<Refresh>,
315}
316#[derive(Default)]
317struct Cache {
318    token: Option<TokenLease>,
319    pending: Option<Pending>,
320    clear_epoch: u64,
321    next_generation: u64,
322}
323
324fn cache_info(cache: &Cache, refresh_in_progress: bool) -> TokenInfo {
325    let now = Instant::now();
326    let is_expired = cache
327        .token
328        .as_ref()
329        .is_none_or(|token| token.expires_at <= now);
330    let is_expiring_soon = cache
331        .token
332        .as_ref()
333        .is_none_or(|token| token.refresh_at <= now);
334    TokenInfo {
335        cached: cache.token.is_some(),
336        generation: cache.token.as_ref().map(TokenLease::generation),
337        remaining: cache
338            .token
339            .as_ref()
340            .map(|token| token.expires_at.saturating_duration_since(now)),
341        refresh_in_progress,
342        is_valid: cache.token.is_some() && !is_expiring_soon,
343        is_expired,
344        is_expiring_soon,
345        expires_at: cache.token.as_ref().map(|token| token.expires_at_wall),
346    }
347}
348
349struct OAuthInner {
350    context: AuthContext,
351    client_secret: Secret,
352    transport: Arc<dyn Transport>,
353    policy: Arc<dyn DestinationPolicy>,
354    session: Session,
355    cache: Mutex<Cache>,
356    options: OAuthProviderOptions,
357    refresh_events: tokio::sync::broadcast::Sender<TokenInfo>,
358}
359
360/// OAuth2 client-credentials provider with an isolated cache and bounded refresh.
361#[derive(Clone)]
362pub struct OAuthProvider {
363    inner: Arc<OAuthInner>,
364}
365
366impl OAuthProvider {
367    pub fn new(
368        context: AuthContext,
369        client_secret: Secret,
370        transport: Arc<dyn Transport>,
371        policy: Arc<dyn DestinationPolicy>,
372        session: Session,
373    ) -> Result<Self> {
374        Self::new_with_options(
375            context,
376            client_secret,
377            transport,
378            policy,
379            session,
380            OAuthProviderOptions::default(),
381        )
382    }
383
384    pub fn new_with_options(
385        context: AuthContext,
386        client_secret: Secret,
387        transport: Arc<dyn Transport>,
388        policy: Arc<dyn DestinationPolicy>,
389        session: Session,
390        options: OAuthProviderOptions,
391    ) -> Result<Self> {
392        if client_secret.expose().chars().any(char::is_control) {
393            return Err(Error::new(ErrorKind::Configuration));
394        }
395        options.validate()?;
396        let (refresh_events, _) = tokio::sync::broadcast::channel(options.event_capacity);
397        Ok(Self {
398            inner: Arc::new(OAuthInner {
399                context,
400                client_secret,
401                transport,
402                policy,
403                session,
404                cache: Mutex::new(Cache::default()),
405                options,
406                refresh_events,
407            }),
408        })
409    }
410
411    /// Subscribe to successful cache-publication snapshots; no credentials or callbacks cross this seam.
412    /// Events are historical metadata, emitted once per generation without workers or backpressure.
413    pub fn subscribe_refresh(&self) -> tokio::sync::broadcast::Receiver<TokenInfo> {
414        self.inner.refresh_events.subscribe()
415    }
416
417    /// Clear the cached token and prevent an older pending refresh from restoring it.
418    /// Existing waiters receive an authentication error if that refresh finishes.
419    pub fn clear(&self) -> Result<()> {
420        let mut cache = self
421            .inner
422            .cache
423            .lock()
424            .map_err(|_| Error::new(ErrorKind::Authentication))?;
425        cache.clear_epoch = cache
426            .clear_epoch
427            .checked_add(1)
428            .ok_or_else(|| Error::new(ErrorKind::Authentication))?;
429        cache.token = None;
430        cache.pending = None;
431        Ok(())
432    }
433
434    /// Fetch or reuse a token lease under the caller's budget (30 seconds by default).
435    pub async fn get_token(&self, options: Option<&RequestOptions>) -> Result<TokenLease> {
436        let options = options
437            .cloned()
438            .map_or_else(|| RequestOptions::with_timeout(MAX_REFRESH_TIME), Ok)?;
439        self.token(&options).await
440    }
441
442    pub fn is_token_expired(&self) -> Result<bool> {
443        Ok(self.token_info()?.is_expired)
444    }
445
446    /// A custom duration is compared directly, avoiding timestamp overflow.
447    pub fn is_token_expiring_soon(&self, buffer: Option<Duration>) -> Result<bool> {
448        let cache = self
449            .inner
450            .cache
451            .lock()
452            .map_err(|_| Error::new(ErrorKind::Authentication))?;
453        Ok(cache.token.as_ref().is_none_or(|token| match buffer {
454            Some(buffer) => token.remaining() <= buffer,
455            None => !token.usable(),
456        }))
457    }
458
459    pub fn token_info(&self) -> Result<TokenInfo> {
460        let cache = self
461            .inner
462            .cache
463            .lock()
464            .map_err(|_| Error::new(ErrorKind::Authentication))?;
465        let pending = cache
466            .pending
467            .as_ref()
468            .and_then(|pending| pending.future.upgrade());
469        let snapshot = cache_info(&cache, pending.is_some());
470        // A concurrent waiter drop can make this temporary upgrade the final owner.
471        // Release it after the lock so a future's drop/subscriber cannot reenter it.
472        drop(cache);
473        drop(pending);
474        Ok(snapshot)
475    }
476
477    fn shared_refresh(&self) -> Result<Shared<Refresh>> {
478        let mut cache = self
479            .inner
480            .cache
481            .lock()
482            .map_err(|_| Error::new(ErrorKind::Authentication))?;
483        if self.inner.session.is_cancelled() {
484            return Err(Error::new(ErrorKind::Cancelled));
485        }
486        if let Some(token) = cache.token.as_ref().filter(|token| token.usable()) {
487            return Ok(futures_util::future::ready(Ok(token.clone()))
488                .boxed()
489                .shared());
490        }
491        if let Some(pending) = cache
492            .pending
493            .as_ref()
494            .and_then(|pending| pending.future.upgrade())
495        {
496            return Ok(pending);
497        }
498        cache.next_generation = cache
499            .next_generation
500            .checked_add(1)
501            .ok_or_else(|| Error::new(ErrorKind::Authentication))?;
502        let generation = cache.next_generation;
503        let epoch = cache.clear_epoch;
504        let inner = Arc::clone(&self.inner);
505        let future: Shared<Refresh> = async move {
506            let result = inner.refresh(generation).await;
507            let mut cache = inner
508                .cache
509                .lock()
510                .map_err(|_| Error::new(ErrorKind::Authentication))?;
511            if cache.clear_epoch != epoch {
512                return Err(Error::new(ErrorKind::Authentication));
513            }
514            if cache
515                .pending
516                .as_ref()
517                .is_some_and(|pending| pending.generation == generation)
518            {
519                cache.pending = None;
520            }
521            let event = if let Ok(token) = &result {
522                cache.token = Some(token.clone());
523                Some(cache_info(&cache, false))
524            } else {
525                None
526            };
527            drop(cache);
528            if let Some(event) = event {
529                crate::observability::published();
530                // Publishing metadata is synchronous and bounded, with no callbacks
531                // or workers. Slow/absent receivers never hold up token delivery.
532                let _ = inner.refresh_events.send(event);
533            }
534            result
535        }
536        .boxed()
537        .shared();
538        cache.pending = Some(Pending {
539            generation,
540            future: future
541                .downgrade()
542                .ok_or_else(|| Error::new(ErrorKind::Authentication))?,
543        });
544        Ok(future)
545    }
546}
547
548impl TokenProvider for OAuthProvider {
549    fn token<'a>(&'a self, options: &'a RequestOptions) -> BoxFuture<'a, Result<TokenLease>> {
550        async move {
551            let refresh = self.shared_refresh()?;
552            // The caller's deadline drops only this waiter. The shared refresh has
553            // its own 30-second budget, and has no strong owner inside the cache.
554            options.run(&self.inner.session, refresh).await
555        }
556        .boxed()
557    }
558    fn context(&self) -> &AuthContext {
559        &self.inner.context
560    }
561    fn invalidate(&self, generation: u64) -> bool {
562        let Ok(mut cache) = self.inner.cache.lock() else {
563            return false;
564        };
565        if cache
566            .token
567            .as_ref()
568            .is_some_and(|token| token.generation == generation)
569        {
570            cache.token = None;
571            // Preserve an already-running newer generation.
572            true
573        } else {
574            false
575        }
576    }
577}
578
579impl fmt::Debug for OAuthProvider {
580    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
581        f.debug_struct("OAuthProvider")
582            .field("context", &self.inner.context)
583            .field("credentials", &"[REDACTED]")
584            .finish_non_exhaustive()
585    }
586}
587
588#[derive(Deserialize)]
589struct WireToken {
590    access_token: String,
591    token_type: String,
592    expires_in: u64,
593}
594
595impl OAuthInner {
596    async fn refresh(&self, generation: u64) -> Result<TokenLease> {
597        let lifecycle =
598            crate::observability::Lifecycle::start(crate::observability::Operation::OAuthRefresh);
599        let result = self.refresh_observed(generation, &lifecycle).await;
600        lifecycle.finish(&result);
601        result
602    }
603    async fn refresh_observed(
604        &self,
605        generation: u64,
606        lifecycle: &crate::observability::Lifecycle,
607    ) -> Result<TokenLease> {
608        let options = RequestOptions::with_timeout(MAX_REFRESH_TIME)?;
609        options
610            .run(&self.session, async {
611                let destination = self.context.issuer.url().clone();
612                self.context.issuer.admit(&destination)?;
613                self.policy
614                    .check(&destination, DestinationPurpose::OAuthToken)?;
615                let form_body = {
616                    let mut form = url::form_urlencoded::Serializer::new(String::new());
617                    form.append_pair("grant_type", "client_credentials")
618                        .append_pair("scope", &self.context.scopes.join(" "));
619                    if let Some(audience) = &self.context.audience {
620                        form.append_pair("audience", audience);
621                    }
622                    form.finish()
623                };
624                let encoded_id: String =
625                    url::form_urlencoded::byte_serialize(self.context.client_id.as_bytes())
626                        .collect();
627                let encoded_secret = Zeroizing::new(
628                    url::form_urlencoded::byte_serialize(self.client_secret.expose().as_bytes())
629                        .collect::<String>(),
630                );
631                let pair = Zeroizing::new(format!("{encoded_id}:{}", encoded_secret.as_str()));
632                let basic = Zeroizing::new(format!("Basic {}", STANDARD.encode(pair.as_bytes())));
633                let mut headers = HeaderMap::new();
634                headers.insert(AUTHORIZATION, sensitive_header(basic.as_str())?);
635                headers.insert(
636                    CONTENT_TYPE,
637                    HeaderValue::from_static("application/x-www-form-urlencoded"),
638                );
639                let issued_at = Instant::now();
640                lifecycle.dispatched();
641                let mut response = self
642                    .transport
643                    .send(TransportRequest::new(
644                        Method::POST,
645                        destination,
646                        headers,
647                        Bytes::from(form_body),
648                        "oauth.token",
649                    ))
650                    .await?;
651                lifecycle.response(
652                    response.status.as_u16(),
653                    crate::engine::error_metadata(&response.headers, None)
654                        .0
655                        .is_some(),
656                );
657                if !response.status.is_success() {
658                    return Err(
659                        Error::new(ErrorKind::Authentication).with_status(response.status.as_u16())
660                    );
661                }
662                let body = read_limited(&mut response.body, MAX_TOKEN_RESPONSE).await?;
663                let wire: WireToken = crate::json_decode::from_slice(&body)
664                    .and_then(crate::json_decode::from_value)
665                    .map_err(|_| Error::new(ErrorKind::Decode))?;
666                let access_token = Zeroizing::new(wire.access_token);
667                if !wire.token_type.eq_ignore_ascii_case("bearer") {
668                    return Err(Error::new(ErrorKind::Authentication));
669                }
670                TokenLease::issued_with_buffer(
671                    Secret::new(access_token.as_str())
672                        .map_err(|_| Error::new(ErrorKind::Authentication))?,
673                    generation,
674                    issued_at,
675                    Duration::from_secs(wire.expires_in),
676                    self.options.refresh_buffer,
677                )
678            })
679            .await
680            .map_err(|error| error.with_operation("oauth.token"))
681    }
682}
683
684/// Per-client authentication. Credentials never enter ordinary formatting.
685#[derive(Clone)]
686#[non_exhaustive]
687pub enum Authentication {
688    None,
689    Scan {
690        api_key: Option<Secret>,
691        api_token: Option<Secret>,
692    },
693    OAuth {
694        provider: Arc<dyn TokenProvider>,
695        tsg_header: Option<String>,
696    },
697    RuntimeKey {
698        api_key: Secret,
699    },
700}
701
702impl Authentication {
703    pub fn scan(api_key: Option<Secret>, api_token: Option<Secret>) -> Result<Self> {
704        if api_key.is_none() && api_token.is_none() {
705            return Err(Error::new(ErrorKind::Configuration));
706        }
707        for secret in [&api_key, &api_token].into_iter().flatten() {
708            validate_header_secret(secret.expose())?;
709        }
710        Ok(Self::Scan { api_key, api_token })
711    }
712    /// Bind the optional tenant header to the provider's immutable context.
713    pub fn oauth(provider: Arc<dyn TokenProvider>, with_tsg_header: bool) -> Self {
714        let tsg_header = with_tsg_header.then(|| provider.context().tsg_id.clone());
715        Self::OAuth {
716            provider,
717            tsg_header,
718        }
719    }
720    pub fn runtime_key(api_key: Secret) -> Result<Self> {
721        validate_header_secret(api_key.expose())?;
722        Ok(Self::RuntimeKey { api_key })
723    }
724
725    /// Attach protected headers after destination admission. Returns the OAuth
726    /// token generation used so a later 401 can invalidate precisely that lease.
727    pub async fn prepare(
728        &self,
729        headers: &mut HeaderMap,
730        body: &Bytes,
731        has_body: bool,
732        options: &RequestOptions,
733    ) -> Result<Option<u64>> {
734        // Reject caller overrides instead of silently mixing credential identities.
735        for name in [
736            "authorization",
737            "x-pan-token",
738            "x-payload-hash",
739            "x-portkey-api-key",
740            "x-tsg-id",
741        ] {
742            if headers.contains_key(name) {
743                return Err(Error::new(ErrorKind::Configuration));
744            }
745        }
746        match self {
747            Self::None => Ok(None),
748            Self::Scan { api_key, api_token } => {
749                if api_key.is_none() && api_token.is_none() {
750                    return Err(Error::new(ErrorKind::Configuration));
751                }
752                if let Some(token) = api_token {
753                    validate_header_secret(token.expose())?;
754                    headers.insert(
755                        AUTHORIZATION,
756                        sensitive_header(&format!("Bearer {}", token.expose()))?,
757                    );
758                }
759                if let Some(key) = api_key {
760                    validate_header_secret(key.expose())?;
761                    headers.insert("x-pan-token", sensitive_header(key.expose())?);
762                    if has_body {
763                        let mut hmac = Hmac::<Sha256>::new_from_slice(key.expose().as_bytes())
764                            .map_err(|_| Error::new(ErrorKind::Authentication))?;
765                        hmac.update(body);
766                        let signature: String = hmac
767                            .finalize()
768                            .into_bytes()
769                            .iter()
770                            .map(|byte| format!("{byte:02x}"))
771                            .collect();
772                        headers.insert("x-payload-hash", sensitive_header(&signature)?);
773                    }
774                }
775                Ok(None)
776            }
777            Self::OAuth {
778                provider,
779                tsg_header,
780            } => {
781                if let Some(tenant) = tsg_header {
782                    if tenant != provider.context().tsg_id() {
783                        return Err(Error::new(ErrorKind::Configuration));
784                    }
785                    validate_header_secret(tenant)?;
786                }
787                let lease = provider.token(options).await?;
788                validate_bearer(lease.token.expose())?;
789                if lease.remaining().is_zero() {
790                    return Err(Error::new(ErrorKind::Authentication));
791                }
792                headers.insert(
793                    AUTHORIZATION,
794                    sensitive_header(&format!("Bearer {}", lease.token.expose()))?,
795                );
796                if let Some(tenant) = tsg_header {
797                    headers.insert("x-tsg-id", sensitive_header(tenant)?);
798                }
799                Ok(Some(lease.generation))
800            }
801            Self::RuntimeKey { api_key } => {
802                validate_header_secret(api_key.expose())?;
803                headers.insert("x-portkey-api-key", sensitive_header(api_key.expose())?);
804                Ok(None)
805            }
806        }
807    }
808
809    pub fn on_unauthorized(&self, generation: u64) -> bool {
810        match self {
811            Self::OAuth { provider, .. } => provider.invalidate(generation),
812            _ => false,
813        }
814    }
815}
816
817impl fmt::Debug for Authentication {
818    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
819        match self {
820            Self::None => f.write_str("Authentication::None"),
821            Self::Scan { api_key, api_token } => f
822                .debug_struct("Authentication::Scan")
823                .field("has_api_key", &api_key.is_some())
824                .field("has_api_token", &api_token.is_some())
825                .finish(),
826            Self::OAuth { tsg_header, .. } => f
827                .debug_struct("Authentication::OAuth")
828                .field("has_tenant_header", &tsg_header.is_some())
829                .finish_non_exhaustive(),
830            Self::RuntimeKey { .. } => f.write_str("Authentication::RuntimeKey([REDACTED])"),
831        }
832    }
833}
834
835fn sensitive_header(value: &str) -> Result<HeaderValue> {
836    let mut header =
837        HeaderValue::from_str(value).map_err(|_| Error::new(ErrorKind::Authentication))?;
838    header.set_sensitive(true);
839    Ok(header)
840}
841fn validate_header_secret(value: &str) -> Result<()> {
842    if value.is_empty()
843        || value.len() > 16 * 1024
844        || !value.bytes().all(|byte| byte.is_ascii_graphic())
845    {
846        Err(Error::new(ErrorKind::Configuration))
847    } else {
848        Ok(())
849    }
850}
851fn validate_bearer(value: &str) -> Result<()> {
852    if value.is_empty() || value.len() > 16 * 1024 {
853        return Err(Error::new(ErrorKind::Authentication));
854    }
855    let stripped = value.trim_end_matches('=');
856    if stripped.is_empty()
857        || !stripped
858            .bytes()
859            .all(|byte| byte.is_ascii_alphanumeric() || b"-._~+/".contains(&byte))
860    {
861        return Err(Error::new(ErrorKind::Authentication));
862    }
863    Ok(())
864}
865
866#[cfg(test)]
867mod tests {
868    use super::*;
869    use crate::{config::OriginPolicy, transport::TransportResponse};
870    use http::StatusCode;
871    use std::{
872        collections::VecDeque,
873        sync::atomic::{AtomicUsize, Ordering},
874    };
875    use tokio::sync::{Notify, Semaphore};
876
877    struct FakeTransport {
878        requests: Mutex<Vec<TransportRequest>>,
879        responses: Mutex<VecDeque<(StatusCode, Bytes)>>,
880        release: Semaphore,
881        started: Notify,
882        active: AtomicUsize,
883        dropped: AtomicUsize,
884        blocked: bool,
885    }
886    impl FakeTransport {
887        fn new(blocked: bool, responses: Vec<Bytes>) -> Arc<Self> {
888            Arc::new(Self {
889                requests: Mutex::new(Vec::new()),
890                responses: Mutex::new(
891                    responses
892                        .into_iter()
893                        .map(|body| (StatusCode::OK, body))
894                        .collect(),
895                ),
896                release: Semaphore::new(0),
897                started: Notify::new(),
898                active: AtomicUsize::new(0),
899                dropped: AtomicUsize::new(0),
900                blocked,
901            })
902        }
903        fn count(&self) -> usize {
904            self.requests.lock().unwrap().len()
905        }
906    }
907    struct InFlight<'a> {
908        transport: &'a FakeTransport,
909        completed: bool,
910    }
911    impl Drop for InFlight<'_> {
912        fn drop(&mut self) {
913            self.transport.active.fetch_sub(1, Ordering::SeqCst);
914            if !self.completed {
915                self.transport.dropped.fetch_add(1, Ordering::SeqCst);
916            }
917        }
918    }
919    impl Transport for FakeTransport {
920        fn send<'a>(
921            &'a self,
922            request: TransportRequest,
923        ) -> BoxFuture<'a, Result<TransportResponse>> {
924            async move {
925                self.requests.lock().unwrap().push(request);
926                self.active.fetch_add(1, Ordering::SeqCst);
927                let mut guard = InFlight {
928                    transport: self,
929                    completed: false,
930                };
931                let (status, body) = self
932                    .responses
933                    .lock()
934                    .unwrap()
935                    .pop_front()
936                    .unwrap_or((StatusCode::OK, token("fallback", 3600)));
937                self.started.notify_one();
938                if self.blocked {
939                    self.release.acquire().await.unwrap().forget();
940                }
941                guard.completed = true;
942                Ok(TransportResponse::from_bytes(
943                    status,
944                    HeaderMap::new(),
945                    body,
946                ))
947            }
948            .boxed()
949        }
950    }
951    fn token(value: &str, ttl: u64) -> Bytes {
952        Bytes::from(
953            serde_json::json!({"access_token":value,"token_type":"Bearer","expires_in":ttl})
954                .to_string(),
955        )
956    }
957    fn context(tenant: &str) -> AuthContext {
958        AuthContext::new(
959            Endpoint::parse("https://issuer.example/private-token-path").unwrap(),
960            "client-sensitive",
961            7,
962            tenant,
963        )
964        .unwrap()
965    }
966    fn provider_with(
967        context: AuthContext,
968        transport: Arc<FakeTransport>,
969        session: Session,
970    ) -> OAuthProvider {
971        let policy = Arc::new(OriginPolicy::new([context.issuer.clone()]));
972        OAuthProvider::new(
973            context,
974            Secret::new("secret-sensitive").unwrap(),
975            transport,
976            policy,
977            session,
978        )
979        .unwrap()
980    }
981    fn provider(transport: Arc<FakeTransport>) -> OAuthProvider {
982        provider_with(context("123"), transport, Session::new())
983    }
984    fn options() -> RequestOptions {
985        RequestOptions::with_timeout(Duration::from_secs(60)).unwrap()
986    }
987
988    #[tokio::test(start_paused = true)]
989    async fn public_lifecycle_reports_expiry_buffer_and_clear_without_secrets() {
990        let transport = FakeTransport::new(false, vec![token("lifecycle-secret", 100)]);
991        let provider = provider(Arc::clone(&transport));
992        assert!(provider.is_token_expired().unwrap());
993        assert!(provider.is_token_expiring_soon(None).unwrap());
994        let absent = provider.token_info().unwrap();
995        assert!(!absent.cached && !absent.is_valid && absent.is_expired);
996        assert!(absent.expires_at.is_none());
997        let lease = provider.get_token(None).await.unwrap();
998        assert_eq!(lease.secret().expose(), "lifecycle-secret");
999        assert!(!provider.is_token_expired().unwrap());
1000        assert!(!provider.is_token_expiring_soon(None).unwrap());
1001        assert!(
1002            provider
1003                .is_token_expiring_soon(Some(Duration::from_secs(101)))
1004                .unwrap()
1005        );
1006        let current = provider.token_info().unwrap();
1007        assert!(current.cached && current.is_valid && !current.is_expired);
1008        assert!(current.expires_at.is_some());
1009        assert!(!format!("{current:?} {lease:?}").contains("lifecycle-secret"));
1010        tokio::time::advance(Duration::from_secs(81)).await;
1011        let soon = provider.token_info().unwrap();
1012        assert!(!soon.is_expired && soon.is_expiring_soon && !soon.is_valid);
1013        tokio::time::advance(Duration::from_secs(19)).await;
1014        assert!(provider.is_token_expired().unwrap());
1015        provider.clear().unwrap();
1016        assert!(!provider.token_info().unwrap().cached);
1017        assert_eq!(transport.count(), 1);
1018    }
1019
1020    #[tokio::test]
1021    async fn rfc4231_signature_uses_exact_bytes_and_protected_headers() {
1022        let auth = Authentication::scan(Some(Secret::new("Jefe").unwrap()), None).unwrap();
1023        let mut headers = HeaderMap::new();
1024        auth.prepare(
1025            &mut headers,
1026            &Bytes::from_static(b"what do ya want for nothing?"),
1027            true,
1028            &options(),
1029        )
1030        .await
1031        .unwrap();
1032        assert_eq!(
1033            headers["x-payload-hash"],
1034            "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843"
1035        );
1036        assert!(headers["x-pan-token"].is_sensitive());
1037        assert!(headers["x-payload-hash"].is_sensitive());
1038        let mut without_body = HeaderMap::new();
1039        auth.prepare(&mut without_body, &Bytes::new(), false, &options())
1040            .await
1041            .unwrap();
1042        assert!(!without_body.contains_key("x-payload-hash"));
1043        let mut empty_body = HeaderMap::new();
1044        auth.prepare(&mut empty_body, &Bytes::new(), true, &options())
1045            .await
1046            .unwrap();
1047        assert!(empty_body.contains_key("x-payload-hash"));
1048        let mut override_headers = HeaderMap::new();
1049        override_headers.insert(AUTHORIZATION, HeaderValue::from_static("caller-token"));
1050        assert_eq!(
1051            auth.prepare(&mut override_headers, &Bytes::new(), false, &options())
1052                .await
1053                .unwrap_err()
1054                .kind(),
1055            ErrorKind::Configuration
1056        );
1057    }
1058
1059    #[tokio::test]
1060    async fn header_injection_and_wrong_auth_domains_are_rejected() {
1061        for invalid in ["key\r\nx-secret: leaked", "key with spaces", ""] {
1062            let result = Secret::new(invalid).and_then(Authentication::runtime_key);
1063            assert!(result.is_err());
1064            assert!(!format!("{:?}", result.err().unwrap()).contains("leaked"));
1065        }
1066        for invalid in ["tok en", "tok\nX", "=", "to=ken", "é"] {
1067            assert!(
1068                TokenLease::new(Secret::new(invalid).unwrap(), 1, Duration::from_secs(60)).is_err()
1069            );
1070        }
1071        assert!(Authentication::scan(None, None).is_err());
1072        // Enum-level non_exhaustive still permits constructing known variants;
1073        // prepare must therefore enforce invariants independently of constructors.
1074        let raw_empty_scan = Authentication::Scan {
1075            api_key: None,
1076            api_token: None,
1077        };
1078        assert_eq!(
1079            raw_empty_scan
1080                .prepare(&mut HeaderMap::new(), &Bytes::new(), false, &options())
1081                .await
1082                .unwrap_err()
1083                .kind(),
1084            ErrorKind::Configuration
1085        );
1086        let transport = FakeTransport::new(false, vec![token("good", 3600)]);
1087        let auth = Authentication::OAuth {
1088            provider: Arc::new(provider(transport.clone())),
1089            tsg_header: Some("456".to_owned()),
1090        };
1091        assert!(
1092            auth.prepare(&mut HeaderMap::new(), &Bytes::new(), false, &options())
1093                .await
1094                .is_err()
1095        );
1096        assert_eq!(transport.count(), 0);
1097    }
1098
1099    #[tokio::test]
1100    async fn token_exchange_uses_encoded_basic_form_and_no_runtime_credentials() {
1101        let transport = FakeTransport::new(false, vec![token("token-1", 3600)]);
1102        let context = AuthContext::new(
1103            Endpoint::parse("https://issuer.example/token").unwrap(),
1104            "client: &+",
1105            1,
1106            "123",
1107        )
1108        .unwrap()
1109        .with_scopes(vec!["tsg_id:123".into(), "read:api".into()])
1110        .unwrap()
1111        .with_audience("service & scope")
1112        .unwrap();
1113        let provider = OAuthProvider::new(
1114            context.clone(),
1115            Secret::new("sec/ +:").unwrap(),
1116            transport.clone(),
1117            Arc::new(OriginPolicy::new([context.issuer.clone()])),
1118            Session::new(),
1119        )
1120        .unwrap();
1121        let lease = provider.token(&options()).await.unwrap();
1122        assert_eq!(lease.secret().expose(), "token-1");
1123        let requests = transport.requests.lock().unwrap();
1124        let request = &requests[0];
1125        assert_eq!(request.method(), Method::POST);
1126        assert_eq!(request.url(), context.issuer.url());
1127        assert_eq!(
1128            request.headers()[CONTENT_TYPE],
1129            "application/x-www-form-urlencoded"
1130        );
1131        let basic = request.headers()[AUTHORIZATION]
1132            .to_str()
1133            .unwrap()
1134            .strip_prefix("Basic ")
1135            .unwrap();
1136        assert_eq!(
1137            STANDARD.decode(basic).unwrap(),
1138            b"client%3A+%26%2B:sec%2F+%2B%3A"
1139        );
1140        let form: std::collections::BTreeMap<_, _> = url::form_urlencoded::parse(request.body())
1141            .into_owned()
1142            .collect();
1143        assert_eq!(form["scope"], "tsg_id:123 read:api");
1144        assert_eq!(form["audience"], "service & scope");
1145        assert_eq!(form["grant_type"], "client_credentials");
1146        assert!(request.headers()[AUTHORIZATION].is_sensitive());
1147        assert!(!request.headers().contains_key("x-portkey-api-key"));
1148        assert!(!request.headers().contains_key("x-pan-token"));
1149    }
1150
1151    #[tokio::test]
1152    async fn token_destination_policy_runs_before_transport() {
1153        struct Reject;
1154        impl DestinationPolicy for Reject {
1155            fn check(&self, _: &url::Url, purpose: DestinationPurpose) -> Result<()> {
1156                assert_eq!(purpose, DestinationPurpose::OAuthToken);
1157                Err(Error::new(ErrorKind::Destination))
1158            }
1159        }
1160        let transport = FakeTransport::new(false, vec![]);
1161        let provider = OAuthProvider::new(
1162            context("123"),
1163            Secret::new("credential").unwrap(),
1164            transport.clone(),
1165            Arc::new(Reject),
1166            Session::new(),
1167        )
1168        .unwrap();
1169        assert_eq!(
1170            provider.token(&options()).await.unwrap_err().kind(),
1171            ErrorKind::Destination
1172        );
1173        assert_eq!(transport.count(), 0);
1174    }
1175
1176    #[tokio::test]
1177    async fn refresh_is_deduplicated_and_one_cancelled_waiter_preserves_the_other() {
1178        let transport = FakeTransport::new(true, vec![token("shared-token", 3600)]);
1179        let provider = provider(transport.clone());
1180        let first_options = options();
1181        let first_cancel = first_options.clone();
1182        let first_provider = provider.clone();
1183        let first = tokio::spawn(async move { first_provider.token(&first_options).await });
1184        transport.started.notified().await;
1185        let second_provider = provider.clone();
1186        let second = tokio::spawn(async move { second_provider.token(&options()).await });
1187        tokio::task::yield_now().await;
1188        assert_eq!(transport.count(), 1);
1189        first_cancel.cancel();
1190        assert_eq!(
1191            first.await.unwrap().unwrap_err().kind(),
1192            ErrorKind::Cancelled
1193        );
1194        assert_eq!(transport.active.load(Ordering::SeqCst), 1);
1195        transport.release.add_permits(1);
1196        assert_eq!(
1197            second.await.unwrap().unwrap().secret().expose(),
1198            "shared-token"
1199        );
1200        assert_eq!(transport.dropped.load(Ordering::SeqCst), 0);
1201        assert_eq!(provider.token(&options()).await.unwrap().generation(), 1);
1202        assert_eq!(transport.count(), 1);
1203    }
1204
1205    #[tokio::test]
1206    async fn last_waiter_drop_releases_http_and_next_call_can_refresh() {
1207        let transport = FakeTransport::new(
1208            true,
1209            vec![token("abandoned", 3600), token("replacement", 3600)],
1210        );
1211        let provider = provider(transport.clone());
1212        let task_provider = provider.clone();
1213        let task = tokio::spawn(async move { task_provider.token(&options()).await });
1214        transport.started.notified().await;
1215        task.abort();
1216        assert!(task.await.is_err());
1217        assert_eq!(transport.active.load(Ordering::SeqCst), 0);
1218        assert_eq!(transport.dropped.load(Ordering::SeqCst), 1);
1219        assert!(!provider.token_info().unwrap().refresh_in_progress);
1220        transport.release.add_permits(1);
1221        let lease = provider.token(&options()).await.unwrap();
1222        assert_eq!(lease.secret().expose(), "replacement");
1223        assert_eq!(transport.count(), 2);
1224    }
1225
1226    #[tokio::test]
1227    async fn explicit_session_cancellation_interrupts_shared_refresh() {
1228        let transport = FakeTransport::new(true, vec![token("pending", 3600)]);
1229        let session = Session::new();
1230        let provider = provider_with(context("123"), transport.clone(), session.clone());
1231        let task_provider = provider.clone();
1232        let task = tokio::spawn(async move { task_provider.token(&options()).await });
1233        transport.started.notified().await;
1234        session.cancel();
1235        assert_eq!(
1236            task.await.unwrap().unwrap_err().kind(),
1237            ErrorKind::Cancelled
1238        );
1239        assert_eq!(transport.active.load(Ordering::SeqCst), 0);
1240        assert_eq!(
1241            provider.token(&options()).await.unwrap_err().kind(),
1242            ErrorKind::Cancelled
1243        );
1244        assert_eq!(transport.count(), 1);
1245    }
1246
1247    #[tokio::test]
1248    async fn already_cancelled_waiter_never_starts_token_exchange() {
1249        let transport = FakeTransport::new(false, vec![token("unused", 3600)]);
1250        let provider = provider(transport.clone());
1251        let options = options();
1252        options.cancel();
1253        assert_eq!(
1254            provider.token(&options).await.unwrap_err().kind(),
1255            ErrorKind::Cancelled
1256        );
1257        assert_eq!(transport.count(), 0);
1258        assert!(!provider.token_info().unwrap().refresh_in_progress);
1259    }
1260
1261    #[tokio::test(start_paused = true)]
1262    async fn refresh_has_its_own_thirty_second_deadline() {
1263        let transport = FakeTransport::new(true, vec![token("pending", 3600)]);
1264        let provider = provider(transport.clone());
1265        let task = tokio::spawn(async move { provider.token(&options()).await });
1266        transport.started.notified().await;
1267        tokio::time::advance(Duration::from_secs(31)).await;
1268        assert_eq!(task.await.unwrap().unwrap_err().kind(), ErrorKind::Deadline);
1269        assert_eq!(transport.active.load(Ordering::SeqCst), 0);
1270        assert_eq!(transport.dropped.load(Ordering::SeqCst), 1);
1271    }
1272
1273    #[tokio::test(start_paused = true)]
1274    async fn one_waiter_deadline_does_not_expire_other_waiters() {
1275        let transport = FakeTransport::new(true, vec![token("shared-after-deadline", 3600)]);
1276        let provider = provider(transport.clone());
1277        let first_provider = provider.clone();
1278        let first = tokio::spawn(async move {
1279            first_provider
1280                .token(&RequestOptions::with_timeout(Duration::from_secs(1)).unwrap())
1281                .await
1282        });
1283        transport.started.notified().await;
1284        let second_provider = provider.clone();
1285        let second = tokio::spawn(async move { second_provider.token(&options()).await });
1286        tokio::task::yield_now().await;
1287        tokio::time::advance(Duration::from_secs(2)).await;
1288        assert_eq!(
1289            first.await.unwrap().unwrap_err().kind(),
1290            ErrorKind::Deadline
1291        );
1292        assert_eq!(transport.active.load(Ordering::SeqCst), 1);
1293        transport.release.add_permits(1);
1294        assert_eq!(
1295            second.await.unwrap().unwrap().secret().expose(),
1296            "shared-after-deadline"
1297        );
1298        assert_eq!(transport.count(), 1);
1299        assert_eq!(transport.dropped.load(Ordering::SeqCst), 0);
1300    }
1301
1302    #[tokio::test(start_paused = true)]
1303    async fn late_unauthorized_response_cannot_evict_new_generation() {
1304        let transport = FakeTransport::new(false, vec![token("old", 2), token("new", 3600)]);
1305        let provider = provider(transport.clone());
1306        let old = provider.token(&options()).await.unwrap();
1307        tokio::time::advance(Duration::from_secs(2)).await;
1308        let new = provider.token(&options()).await.unwrap();
1309        assert!(new.generation() > old.generation());
1310        assert!(!provider.invalidate(old.generation()));
1311        assert_eq!(
1312            provider.token(&options()).await.unwrap().secret().expose(),
1313            "new"
1314        );
1315        assert_eq!(transport.count(), 2);
1316        assert!(provider.invalidate(new.generation()));
1317        assert!(!provider.token_info().unwrap().cached);
1318    }
1319
1320    #[tokio::test(start_paused = true)]
1321    async fn token_lifetime_starts_before_exchange_latency() {
1322        let transport = FakeTransport::new(true, vec![token("already-expired", 1)]);
1323        let provider = provider(transport.clone());
1324        let task_provider = provider.clone();
1325        let task = tokio::spawn(async move { task_provider.token(&options()).await });
1326        transport.started.notified().await;
1327        tokio::time::advance(Duration::from_secs(2)).await;
1328        transport.release.add_permits(1);
1329        assert_eq!(
1330            task.await.unwrap().unwrap_err().kind(),
1331            ErrorKind::Authentication
1332        );
1333        assert!(!provider.token_info().unwrap().cached);
1334    }
1335
1336    #[tokio::test(start_paused = true)]
1337    async fn invalidating_old_cached_token_preserves_new_pending_refresh() {
1338        let transport = FakeTransport::new(true, vec![token("old", 2), token("new", 3600)]);
1339        let provider = provider(transport.clone());
1340        transport.release.add_permits(1);
1341        let old = provider.token(&options()).await.unwrap();
1342        tokio::time::advance(Duration::from_secs(2)).await;
1343        let next_provider = provider.clone();
1344        let next = tokio::spawn(async move { next_provider.token(&options()).await });
1345        while transport.count() < 2 {
1346            tokio::task::yield_now().await;
1347        }
1348        assert!(provider.invalidate(old.generation()));
1349        transport.release.add_permits(1);
1350        let new = next.await.unwrap().unwrap();
1351        assert_eq!(
1352            provider.token(&options()).await.unwrap().generation(),
1353            new.generation()
1354        );
1355        assert_eq!(transport.count(), 2);
1356    }
1357
1358    #[tokio::test]
1359    async fn clear_prevents_pending_old_refresh_from_restoring_cache() {
1360        let transport = FakeTransport::new(
1361            true,
1362            vec![token("old-pending", 3600), token("new-pending", 3600)],
1363        );
1364        let provider = provider(transport.clone());
1365        let old_provider = provider.clone();
1366        let old = tokio::spawn(async move { old_provider.token(&options()).await });
1367        transport.started.notified().await;
1368        provider.clear().unwrap();
1369        let next_provider = provider.clone();
1370        let next = tokio::spawn(async move { next_provider.token(&options()).await });
1371        while transport.count() < 2 {
1372            tokio::task::yield_now().await;
1373        }
1374        transport.release.add_permits(2);
1375        assert_eq!(
1376            old.await.unwrap().unwrap_err().kind(),
1377            ErrorKind::Authentication
1378        );
1379        assert_eq!(
1380            next.await.unwrap().unwrap().secret().expose(),
1381            "new-pending"
1382        );
1383        assert_eq!(
1384            provider.token(&options()).await.unwrap().secret().expose(),
1385            "new-pending"
1386        );
1387    }
1388
1389    #[tokio::test]
1390    async fn identities_caches_and_formatting_are_isolated() {
1391        let transport = FakeTransport::new(
1392            false,
1393            vec![
1394                token("tenant-one-token", 3600),
1395                token("tenant-two-token", 3600),
1396            ],
1397        );
1398        let first = provider_with(context("123"), transport.clone(), Session::new());
1399        let second = provider_with(context("456"), transport.clone(), Session::new());
1400        let first_token = first.token(&options()).await.unwrap();
1401        let second_token = second.token(&options()).await.unwrap();
1402        assert_ne!(
1403            first_token.secret().expose(),
1404            second_token.secret().expose()
1405        );
1406        assert_eq!(transport.count(), 2);
1407        assert!(first.invalidate(first_token.generation()));
1408        assert!(second.token_info().unwrap().cached);
1409        let auth = Authentication::oauth(Arc::new(second.clone()), true);
1410        let output = format!(
1411            "{first:?} {second:?} {auth:?} {first_token:?} {:?}",
1412            first.token_info().unwrap()
1413        );
1414        for secret in [
1415            "issuer.example",
1416            "private-token-path",
1417            "client-sensitive",
1418            "secret-sensitive",
1419            "tenant-one-token",
1420            "tenant-two-token",
1421            "tsg_id:123",
1422            "tsg_id:456",
1423        ] {
1424            assert!(!output.contains(secret));
1425        }
1426        let mut headers = HeaderMap::new();
1427        assert_eq!(
1428            auth.prepare(&mut headers, &Bytes::new(), false, &options())
1429                .await
1430                .unwrap(),
1431            Some(second_token.generation())
1432        );
1433        assert_eq!(headers["x-tsg-id"], "456");
1434        assert_eq!(headers[AUTHORIZATION], "Bearer tenant-two-token");
1435        assert!(headers[AUTHORIZATION].is_sensitive());
1436        assert!(headers["x-tsg-id"].is_sensitive());
1437    }
1438
1439    #[tokio::test]
1440    async fn malformed_or_unbounded_token_responses_fail_without_values() {
1441        let cases = [
1442            (
1443                Bytes::from_static(br#"{"access_token":"secret\r\nvalue","token_type":"Bearer","expires_in":3600}"#),
1444                ErrorKind::Authentication,
1445            ),
1446            (
1447                Bytes::from_static(br#"{"access_token":"secret","token_type":"MAC","expires_in":3600}"#),
1448                ErrorKind::Authentication,
1449            ),
1450            (
1451                Bytes::from_static(br#"{"access_token":"secret","token_type":"Bearer","expires_in":0}"#),
1452                ErrorKind::Authentication,
1453            ),
1454            (
1455                Bytes::from_static(br#"{"access_token":"secret","token_type":"Bearer","expires_in":999999999999}"#),
1456                ErrorKind::Authentication,
1457            ),
1458            (
1459                Bytes::from_static(br#"{"access_token":"secret","token_type":"Bearer","expires_in":"3600"}"#),
1460                ErrorKind::Decode,
1461            ),
1462            (Bytes::from_static(b"secret malformed upstream response"), ErrorKind::Decode),
1463            (Bytes::from(vec![b'x'; MAX_TOKEN_RESPONSE + 1]), ErrorKind::Limit),
1464        ];
1465        for (body, kind) in cases {
1466            let transport = FakeTransport::new(false, vec![body]);
1467            let error = provider(transport).token(&options()).await.unwrap_err();
1468            assert_eq!(error.kind(), kind);
1469            assert!(!format!("{error:?} {error}").contains("secret"));
1470        }
1471        let transport =
1472            FakeTransport::new(false, vec![Bytes::from_static(b"secret upstream error")]);
1473        transport.responses.lock().unwrap()[0].0 = StatusCode::UNAUTHORIZED;
1474        let error = provider(transport).token(&options()).await.unwrap_err();
1475        assert_eq!(error.status(), Some(401));
1476        assert!(!format!("{error:?} {error}").contains("secret"));
1477    }
1478
1479    #[test]
1480    fn immutable_context_rejects_cross_tenant_or_unsafe_scope_configuration() {
1481        assert!(
1482            context("123")
1483                .with_scopes(vec!["tsg_id:456".into()])
1484                .is_err()
1485        );
1486        assert!(
1487            context("123")
1488                .with_scopes(vec!["tsg_id:123".into(), "two scopes".into()])
1489                .is_err()
1490        );
1491        assert!(
1492            context("123")
1493                .with_scopes(vec!["tsg_id:123".into(), "tsg_id:123".into()])
1494                .is_err()
1495        );
1496        assert!(context("123").with_audience("audience\nInjected").is_err());
1497        assert!(
1498            AuthContext::new(
1499                Endpoint::parse("https://issuer.example/token").unwrap(),
1500                "client",
1501                1,
1502                "123\n"
1503            )
1504            .is_err()
1505        );
1506    }
1507    fn configured_provider(
1508        transport: Arc<FakeTransport>,
1509        buffer: RefreshBuffer,
1510        capacity: usize,
1511    ) -> OAuthProvider {
1512        let context = context("123");
1513        let policy = Arc::new(OriginPolicy::new([context.issuer.clone()]));
1514        OAuthProvider::new_with_options(
1515            context,
1516            Secret::new("secret-sensitive").unwrap(),
1517            transport,
1518            policy,
1519            Session::new(),
1520            OAuthProviderOptions {
1521                refresh_buffer: buffer,
1522                event_capacity: capacity,
1523            },
1524        )
1525        .unwrap()
1526    }
1527    #[tokio::test(start_paused = true)]
1528    async fn configurable_fixed_and_adaptive_windows_preserve_source_and_safe_defaults() {
1529        let fixed_transport =
1530            FakeTransport::new(false, vec![token("fixed", 100), token("fixed2", 100)]);
1531        let adaptive_transport = FakeTransport::new(false, vec![token("adaptive", 100)]);
1532        let fixed = configured_provider(
1533            fixed_transport.clone(),
1534            RefreshBuffer::Fixed(Duration::from_secs(30)),
1535            16,
1536        );
1537        let adaptive = configured_provider(
1538            adaptive_transport.clone(),
1539            RefreshBuffer::Adaptive(Duration::from_secs(30)),
1540            16,
1541        );
1542        fixed.get_token(None).await.unwrap();
1543        adaptive.get_token(None).await.unwrap();
1544        tokio::time::advance(Duration::from_secs(71)).await;
1545        assert!(fixed.token_info().unwrap().is_expiring_soon);
1546        assert!(!adaptive.token_info().unwrap().is_expiring_soon);
1547        assert_eq!(
1548            fixed.get_token(None).await.unwrap().secret().expose(),
1549            "fixed2"
1550        );
1551        tokio::time::advance(Duration::from_secs(10)).await;
1552        assert!(adaptive.token_info().unwrap().is_expiring_soon);
1553        let transport = FakeTransport::new(false, vec![token("zero", 10)]);
1554        let zero = configured_provider(transport.clone(), RefreshBuffer::Fixed(Duration::ZERO), 16);
1555        zero.get_token(None).await.unwrap();
1556        tokio::time::advance(Duration::from_secs(9)).await;
1557        assert!(zero.token_info().unwrap().is_valid);
1558        zero.get_token(None).await.unwrap();
1559        assert_eq!(transport.count(), 1);
1560        tokio::time::advance(Duration::from_secs(1)).await;
1561        assert!(zero.is_token_expired().unwrap());
1562        let transport = FakeTransport::new(false, vec![token("first", 10), token("second", 10)]);
1563        let eager = configured_provider(transport.clone(), RefreshBuffer::Fixed(Duration::MAX), 16);
1564        assert_eq!(
1565            eager.get_token(None).await.unwrap().secret().expose(),
1566            "first"
1567        );
1568        assert!(eager.token_info().unwrap().is_expiring_soon);
1569        assert_eq!(
1570            eager.get_token(None).await.unwrap().secret().expose(),
1571            "second"
1572        );
1573        assert_eq!(transport.count(), 2);
1574    }
1575    #[tokio::test]
1576    async fn refresh_metadata_is_once_per_published_generation_and_never_contains_credentials() {
1577        let transport = FakeTransport::new(true, vec![token("event-secret", 3600)]);
1578        let provider = configured_provider(transport.clone(), RefreshBuffer::default(), 16);
1579        let mut events = provider.subscribe_refresh();
1580        let one = tokio::spawn({
1581            let provider = provider.clone();
1582            async move { provider.get_token(None).await }
1583        });
1584        transport.started.notified().await;
1585        let two = tokio::spawn({
1586            let provider = provider.clone();
1587            async move { provider.get_token(None).await }
1588        });
1589        tokio::task::yield_now().await;
1590        transport.release.add_permits(1);
1591        let first = one.await.unwrap().unwrap();
1592        let second = two.await.unwrap().unwrap();
1593        assert_eq!(first.generation(), second.generation());
1594        assert_eq!(transport.count(), 1);
1595        let event = events.recv().await.unwrap();
1596        assert_eq!(event.generation, Some(first.generation()));
1597        assert!(event.cached && event.is_valid && !event.refresh_in_progress);
1598        assert!(!format!("{event:?}").contains("event-secret"));
1599        assert!(matches!(
1600            events.try_recv(),
1601            Err(tokio::sync::broadcast::error::TryRecvError::Empty)
1602        ));
1603        provider.get_token(None).await.unwrap();
1604        assert!(matches!(
1605            events.try_recv(),
1606            Err(tokio::sync::broadcast::error::TryRecvError::Empty)
1607        ));
1608        // Observers own their work and may inspect/clear without lock reentrancy.
1609        assert!(provider.token_info().unwrap().cached);
1610        provider.clear().unwrap();
1611        assert!(!provider.token_info().unwrap().cached);
1612    }
1613    #[tokio::test]
1614    async fn failed_and_cleared_pending_refreshes_publish_no_success_event() {
1615        let transport = FakeTransport::new(false, vec![Bytes::from_static(b"invalid-secret-json")]);
1616        let provider = configured_provider(transport, RefreshBuffer::default(), 16);
1617        let mut events = provider.subscribe_refresh();
1618        assert!(provider.get_token(None).await.is_err());
1619        assert!(matches!(
1620            events.try_recv(),
1621            Err(tokio::sync::broadcast::error::TryRecvError::Empty)
1622        ));
1623        let transport = FakeTransport::new(true, vec![token("stale-secret", 3600)]);
1624        let provider = configured_provider(transport.clone(), RefreshBuffer::default(), 16);
1625        let mut events = provider.subscribe_refresh();
1626        let pending = tokio::spawn({
1627            let provider = provider.clone();
1628            async move { provider.get_token(None).await }
1629        });
1630        transport.started.notified().await;
1631        provider.clear().unwrap();
1632        transport.release.add_permits(1);
1633        assert_eq!(
1634            pending.await.unwrap().unwrap_err().kind(),
1635            ErrorKind::Authentication
1636        );
1637        assert!(matches!(
1638            events.try_recv(),
1639            Err(tokio::sync::broadcast::error::TryRecvError::Empty)
1640        ));
1641        assert!(!provider.token_info().unwrap().cached);
1642    }
1643    #[tokio::test]
1644    async fn lagged_dropped_and_absent_subscribers_never_block_token_delivery() {
1645        let transport = FakeTransport::new(false, vec![]);
1646        let provider =
1647            configured_provider(transport.clone(), RefreshBuffer::Fixed(Duration::MAX), 1);
1648        let mut slow = provider.subscribe_refresh();
1649        for _ in 0..5 {
1650            provider.get_token(None).await.unwrap();
1651        }
1652        assert!(matches!(
1653            slow.try_recv(),
1654            Err(tokio::sync::broadcast::error::TryRecvError::Lagged(4))
1655        ));
1656        assert_eq!(slow.try_recv().unwrap().generation, Some(5));
1657        drop(slow);
1658        provider.get_token(None).await.unwrap();
1659        assert_eq!(transport.count(), 6);
1660        let mut last = provider.subscribe_refresh();
1661        drop(provider);
1662        assert!(matches!(
1663            last.recv().await,
1664            Err(tokio::sync::broadcast::error::RecvError::Closed)
1665        ));
1666    }
1667    #[test]
1668    fn refresh_event_capacity_is_validated_before_provider_creation() {
1669        for capacity in [0, 1025, usize::MAX] {
1670            let context = context("123");
1671            let policy = Arc::new(OriginPolicy::new([context.issuer.clone()]));
1672            let transport = FakeTransport::new(false, vec![]);
1673            let result = OAuthProvider::new_with_options(
1674                context,
1675                Secret::new("secret").unwrap(),
1676                transport.clone(),
1677                policy,
1678                Session::new(),
1679                OAuthProviderOptions {
1680                    event_capacity: capacity,
1681                    ..OAuthProviderOptions::default()
1682                },
1683            );
1684            assert_eq!(result.unwrap_err().kind(), ErrorKind::Configuration);
1685            assert_eq!(transport.count(), 0);
1686        }
1687    }
1688}