gcloud_identity_token/
cache.rs1use crate::config::SavedToken;
10use anyhow::Result;
11use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
12use keyring::Entry;
13use serde::Deserialize;
14use std::{fs, path::PathBuf};
15
16const SERVICE: &str = env!("CARGO_PKG_NAME");
17
18#[derive(Debug, Deserialize)]
20struct IdTokenClaims {
21 email: String,
22}
23
24fn extract_email_from_id_token(id_token: &str) -> Option<String> {
28 let parts: Vec<&str> = id_token.split('.').collect();
29 if parts.len() != 3 {
30 return None;
31 }
32
33 let payload = URL_SAFE_NO_PAD.decode(parts[1]).ok()?;
34 let claims: IdTokenClaims = serde_json::from_slice(&payload).ok()?;
35 Some(claims.email)
36}
37
38pub fn load_cached_token() -> Option<SavedToken> {
48 if let Ok(env_path) = std::env::var("GCLOUD_IDENTITY_TOKEN_PATH") {
49 let path = PathBuf::from(env_path);
50 let data = fs::read_to_string(path).ok()?;
51 return serde_json::from_str(&data).ok();
52 }
53
54 let user = fs::read_to_string(email_hint_path()).unwrap_or_else(|_| "default".to_string());
55 let entry = Entry::new(SERVICE, &user).ok()?;
56 let json = entry.get_password().ok()?;
57 serde_json::from_str(&json).ok()
58}
59
60pub fn save_token(token: &SavedToken) -> Result<()> {
70 if let Ok(env_path) = std::env::var("GCLOUD_IDENTITY_TOKEN_PATH") {
71 let path = PathBuf::from(env_path);
72 fs::create_dir_all(path.parent().unwrap())?;
73 fs::write(path, serde_json::to_string_pretty(token)?)?;
74 return Ok(());
75 }
76
77 let user =
78 extract_email_from_id_token(&token.id_token).unwrap_or_else(|| "default".to_string());
79 fs::write(email_hint_path(), &user)?;
80
81 let json = serde_json::to_string(token)?;
82 let entry = Entry::new(SERVICE, &user)?;
83 entry.set_password(&json)?;
84 Ok(())
85}
86
87pub fn delete_token() -> Result<()> {
92 let user = fs::read_to_string(email_hint_path()).unwrap_or_else(|_| "default".to_string());
93 let entry = Entry::new(SERVICE, &user)?;
94 entry.delete_password()?;
95 Ok(())
96}
97
98fn email_hint_path() -> PathBuf {
99 dirs::home_dir()
100 .expect("no home dir")
101 .join(".cache")
102 .join(format!("{}.email", env!("CARGO_PKG_NAME")))
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 #[test]
110 fn test_load_save_with_file_cache() {
111 unsafe {
112 std::env::set_var("GCLOUD_IDENTITY_TOKEN_PATH", "/tmp/test_token.json");
113 }
114
115 let token = SavedToken {
116 refresh_token: "r".into(),
117 access_token: "a".into(),
118 id_token: encode_dummy_id_token_with_email("test@example.com"),
119 token_expiry: chrono::DateTime::parse_from_rfc3339("2025-01-01T00:00:00Z").unwrap().with_timezone(&chrono::Utc),
120 };
121
122 save_token(&token).unwrap();
123 let loaded = load_cached_token().unwrap();
124 assert_eq!(loaded.refresh_token, "r");
125 assert_eq!(loaded.id_token, token.id_token);
126 }
127
128 fn encode_dummy_id_token_with_email(email: &str) -> String {
130 let header = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(r#"{"alg":"none"}"#);
131 let payload = base64::engine::general_purpose::URL_SAFE_NO_PAD
132 .encode(format!(r#"{{"email":"{}"}}"#, email));
133 let signature = "";
134 format!("{}.{}.{}", header, payload, signature)
135 }
136}