use crate::config::SavedToken;
use anyhow::Result;
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use keyring::Entry;
use serde::Deserialize;
use std::{fs, path::PathBuf};
const SERVICE: &str = env!("CARGO_PKG_NAME");
#[derive(Debug, Deserialize)]
struct IdTokenClaims {
email: String,
}
fn extract_email_from_id_token(id_token: &str) -> Option<String> {
let parts: Vec<&str> = id_token.split('.').collect();
if parts.len() != 3 {
return None;
}
let payload = URL_SAFE_NO_PAD.decode(parts[1]).ok()?;
let claims: IdTokenClaims = serde_json::from_slice(&payload).ok()?;
Some(claims.email)
}
pub fn load_cached_token() -> Option<SavedToken> {
if let Ok(env_path) = std::env::var("GCLOUD_IDENTITY_TOKEN_PATH") {
let path = PathBuf::from(env_path);
let data = fs::read_to_string(path).ok()?;
return serde_json::from_str(&data).ok();
}
let user = fs::read_to_string(email_hint_path()).unwrap_or_else(|_| "default".to_string());
let entry = Entry::new(SERVICE, &user).ok()?;
let json = entry.get_password().ok()?;
serde_json::from_str(&json).ok()
}
pub fn save_token(token: &SavedToken) -> Result<()> {
if let Ok(env_path) = std::env::var("GCLOUD_IDENTITY_TOKEN_PATH") {
let path = PathBuf::from(env_path);
fs::create_dir_all(path.parent().unwrap())?;
fs::write(path, serde_json::to_string_pretty(token)?)?;
return Ok(());
}
let user =
extract_email_from_id_token(&token.id_token).unwrap_or_else(|| "default".to_string());
fs::write(email_hint_path(), &user)?;
let json = serde_json::to_string(token)?;
let entry = Entry::new(SERVICE, &user)?;
entry.set_password(&json)?;
Ok(())
}
pub fn delete_token() -> Result<()> {
let user = fs::read_to_string(email_hint_path()).unwrap_or_else(|_| "default".to_string());
let entry = Entry::new(SERVICE, &user)?;
entry.delete_password()?;
Ok(())
}
fn email_hint_path() -> PathBuf {
dirs::home_dir()
.expect("no home dir")
.join(".cache")
.join(format!("{}.email", env!("CARGO_PKG_NAME")))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_load_save_with_file_cache() {
unsafe {
std::env::set_var("GCLOUD_IDENTITY_TOKEN_PATH", "/tmp/test_token.json");
}
let token = SavedToken {
refresh_token: "r".into(),
access_token: "a".into(),
id_token: encode_dummy_id_token_with_email("test@example.com"),
token_expiry: chrono::DateTime::parse_from_rfc3339("2025-01-01T00:00:00Z").unwrap().with_timezone(&chrono::Utc),
};
save_token(&token).unwrap();
let loaded = load_cached_token().unwrap();
assert_eq!(loaded.refresh_token, "r");
assert_eq!(loaded.id_token, token.id_token);
}
fn encode_dummy_id_token_with_email(email: &str) -> String {
let header = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(r#"{"alg":"none"}"#);
let payload = base64::engine::general_purpose::URL_SAFE_NO_PAD
.encode(format!(r#"{{"email":"{}"}}"#, email));
let signature = "";
format!("{}.{}.{}", header, payload, signature)
}
}