grr_cli/core/auth/
token.rs1use std::time::{SystemTime, UNIX_EPOCH};
4
5use serde::{Deserialize, Serialize};
6use tracing::debug;
7
8use crate::core::error::Result;
9
10#[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, #[serde(default = "default_token_type")]
26 pub token_type: String,
27 #[serde(default)]
28 pub scope: String,
29}
30
31fn default_token_type() -> String {
34 "Bearer".to_string()
35}
36
37impl TokenStorage {
38 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 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 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 #[test]
73 fn minimal_old_shape_token_json_still_deserializes()
74 -> std::result::Result<(), Box<dyn std::error::Error>> {
75 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 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}