Skip to main content

grr_cli/core/auth/
token.rs

1//! Token type and in-memory expiry tracking.
2
3use std::time::{SystemTime, UNIX_EPOCH};
4
5use serde::{Deserialize, Serialize};
6use tracing::debug;
7
8use crate::core::error::Result;
9
10/// Token storage with automatic refresh.
11///
12/// Every field carries `#[serde(default)]`: a token JSON written by an
13/// older (or newer) grr must still deserialize. A field added later
14/// defaults on old files; a field removed later is ignored (serde's
15/// default behavior for unknown keys). Without the defaults a minimal
16/// old-shape file — just `access_token` + `expires_at` — would fail.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct TokenStorage {
19    #[serde(default)]
20    pub access_token: String,
21    #[serde(default)]
22    pub refresh_token: Option<String>,
23    #[serde(default)]
24    pub expires_at: u64, // Unix timestamp
25    #[serde(default = "default_token_type")]
26    pub token_type: String,
27    #[serde(default)]
28    pub scope: String,
29}
30
31/// The OAuth token type every endpoint returns; used when an old token
32/// file predates the field.
33fn default_token_type() -> String {
34    "Bearer".to_string()
35}
36
37impl TokenStorage {
38    /// Check if token is expired or about to expire (within 60 seconds)
39    pub fn is_expired(&self) -> bool {
40        let now = SystemTime::now()
41            .duration_since(UNIX_EPOCH)
42            .unwrap_or_default()
43            .as_secs();
44        now + 60 >= self.expires_at
45    }
46
47    /// Get remaining lifetime in seconds
48    pub fn remaining_secs(&self) -> u64 {
49        let now = SystemTime::now()
50            .duration_since(UNIX_EPOCH)
51            .unwrap_or_default()
52            .as_secs();
53        self.expires_at.saturating_sub(now)
54    }
55}
56
57impl super::GoogleAuth {
58    /// Save token via the active store (OS keyring, file fallback).
59    pub(crate) async fn save_token(&self, storage: &TokenStorage) -> Result<()> {
60        self.store.save(storage).await?;
61        debug!("Token persisted via {} store", self.store.backend());
62        Ok(())
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    // The core module denies `unwrap`/`expect`/`panic`, so these tests
71    // return a `Result` and let `?` fail loudly.
72    #[test]
73    fn minimal_old_shape_token_json_still_deserializes()
74    -> std::result::Result<(), Box<dyn std::error::Error>> {
75        // A token file written before token_type / scope / refresh_token
76        // were recorded: only access_token + expires_at are present.
77        let old = r#"{"access_token":"old-token","expires_at":4102444800}"#;
78        let storage: TokenStorage = serde_json::from_str(old)?;
79        assert_eq!(storage.access_token, "old-token");
80        assert_eq!(storage.expires_at, 4_102_444_800);
81        assert_eq!(storage.refresh_token, None);
82        assert_eq!(storage.token_type, "Bearer");
83        assert_eq!(storage.scope, "");
84        Ok(())
85    }
86
87    #[test]
88    fn token_json_with_unknown_future_fields_is_ignored()
89    -> std::result::Result<(), Box<dyn std::error::Error>> {
90        // Forward compatibility the other way: a token written by a newer
91        // grr with an extra field must not fail deserialization.
92        let newer = r#"{"access_token":"t","expires_at":1,"token_type":"Bearer","scope":"s","future_field":42}"#;
93        let storage: TokenStorage = serde_json::from_str(newer)?;
94        assert_eq!(storage.access_token, "t");
95        Ok(())
96    }
97
98    #[test]
99    fn current_shape_round_trips() -> std::result::Result<(), Box<dyn std::error::Error>> {
100        let storage = TokenStorage {
101            access_token: "a".into(),
102            refresh_token: Some("r".into()),
103            expires_at: 5,
104            token_type: "Bearer".into(),
105            scope: "s".into(),
106        };
107        let encoded = serde_json::to_string(&storage)?;
108        let decoded: TokenStorage = serde_json::from_str(&encoded)?;
109        assert_eq!(decoded.access_token, "a");
110        assert_eq!(decoded.refresh_token.as_deref(), Some("r"));
111        assert_eq!(decoded.expires_at, 5);
112        assert_eq!(decoded.token_type, "Bearer");
113        assert_eq!(decoded.scope, "s");
114        Ok(())
115    }
116}