Skip to main content

cli_engine/auth/
credential.rs

1use chrono::{DateTime, Duration, Utc};
2use serde::{Deserialize, Serialize};
3
4/// Cache TTL used when a credential has `cached_at`.
5pub const CACHE_TTL: Duration = Duration::minutes(30);
6
7/// Credential returned by an auth provider.
8///
9/// Field names and omission behavior match the provider JSON contract. Empty
10/// strings are accepted because some providers omit optional values.
11#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
12pub struct Credential {
13    /// Access token used by transport injectors.
14    #[serde(default)]
15    pub token: String,
16    /// Explicit expiration timestamp.
17    #[serde(default)]
18    pub expires_at: String,
19    /// Cache creation timestamp. When present, [`CACHE_TTL`] determines expiry.
20    #[serde(default, skip_serializing_if = "String::is_empty")]
21    pub cached_at: String,
22    /// Provider that produced this credential.
23    #[serde(default, skip_serializing_if = "String::is_empty")]
24    pub provider: String,
25    /// Environment this credential targets.
26    #[serde(default, skip_serializing_if = "String::is_empty")]
27    pub env: String,
28    /// Environment alias accepted from provider responses.
29    #[serde(default, skip_serializing_if = "String::is_empty")]
30    pub realm: String,
31    /// Human-readable identity.
32    #[serde(default, skip_serializing_if = "String::is_empty")]
33    pub identity: String,
34    /// Subject identifier.
35    #[serde(default, skip_serializing_if = "String::is_empty")]
36    pub sub: String,
37    /// Account type associated with the credential.
38    #[serde(default, skip_serializing_if = "String::is_empty")]
39    pub account_type: String,
40    /// OAuth scopes granted to this credential's token. Empty for providers
41    /// that don't expose scope data (e.g. PATs, whose scopes are opaque and
42    /// enforced server-side).
43    #[serde(default, skip_serializing_if = "Vec::is_empty")]
44    pub scopes: Vec<String>,
45    /// Whether a renewal mechanism (e.g. an OAuth refresh token) is present
46    /// for this credential, so an expired access token is *expected* to
47    /// renew without an interactive re-login. This reflects presence, not
48    /// verified validity — the mechanism itself may have been revoked or
49    /// expired server-side, which is only discoverable by attempting a
50    /// renewal. `false` for providers with no such mechanism (e.g. PATs) or
51    /// when the credential has none, even if the access token itself is
52    /// still valid.
53    #[serde(default)]
54    pub refreshable: bool,
55}
56
57impl Credential {
58    /// Returns the timestamp used for status display.
59    #[must_use]
60    pub fn effective_expiry(&self) -> String {
61        if let Ok(cached_at) = DateTime::parse_from_rfc3339(&self.cached_at) {
62            return (cached_at.with_timezone(&Utc) + CACHE_TTL)
63                .to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
64        }
65        self.expires_at.clone()
66    }
67
68    /// Reports whether the credential is expired.
69    ///
70    /// Invalid `expires_at` values are treated as expired. Credentials without
71    /// either `expires_at` or `cached_at` are treated as not expired.
72    #[must_use]
73    pub fn is_expired(&self) -> bool {
74        if let Ok(cached_at) = DateTime::parse_from_rfc3339(&self.cached_at) {
75            return Utc::now() > cached_at.with_timezone(&Utc) + CACHE_TTL;
76        }
77        if self.expires_at.is_empty() {
78            return false;
79        }
80        match DateTime::parse_from_rfc3339(&self.expires_at) {
81            Ok(expires_at) => Utc::now() > expires_at.with_timezone(&Utc),
82            Err(_) => true,
83        }
84    }
85}