fix: decrypt token cache before extracting refresh token (#11)

This commit is contained in:
Justin Poehnelt
2026-03-03 12:49:55 -07:00
committed by GitHub
parent a73ff26522
commit adb2cfa8dc
2 changed files with 44 additions and 11 deletions
@@ -0,0 +1,5 @@
---
"@googleworkspace/cli": patch
---
Fix OAuth login failing with "no refresh token" error by decrypting the token cache before parsing and supporting the HashMap token format used by EncryptedTokenStorage
+39 -11
View File
@@ -173,7 +173,12 @@ async fn handle_login(args: &[String]) -> Result<(), GwsError> {
if token.token().is_some() {
// Read yup-oauth2's token cache to extract the refresh_token.
let token_data = std::fs::read_to_string(&temp_path).unwrap_or_default();
// EncryptedTokenStorage stores data encrypted, so we must decrypt first.
let token_data = std::fs::read(&temp_path)
.ok()
.and_then(|bytes| crate::credential_store::decrypt(&bytes).ok())
.and_then(|decrypted| String::from_utf8(decrypted).ok())
.unwrap_or_default();
let refresh_token = extract_refresh_token(&token_data).ok_or_else(|| {
GwsError::Auth(
"OAuth flow completed but no refresh token was returned. \
@@ -891,16 +896,39 @@ fn handle_logout() -> Result<(), GwsError> {
}
/// Extract refresh_token from yup-oauth2 v12 token cache.
/// Format: [{"scopes":[...], "token":{"access_token":..., "refresh_token":...}}]
///
/// Supports two formats:
/// 1. Array format (yup-oauth2 default file storage):
/// [{"scopes":[...], "token":{"access_token":..., "refresh_token":...}}]
/// 2. Object/HashMap format (EncryptedTokenStorage serialization):
/// {"scope_key": {"access_token":..., "refresh_token":..., ...}}
pub fn extract_refresh_token(token_data: &str) -> Option<String> {
let cache: serde_json::Value = serde_json::from_str(token_data).ok()?;
cache.as_array()?.iter().find_map(|entry| {
entry
.get("token")
.and_then(|t| t.get("refresh_token"))
.and_then(|v| v.as_str())
.map(|s| s.to_string())
})
// Format 1: array of {scopes, token} entries
if let Some(arr) = cache.as_array() {
let result = arr.iter().find_map(|entry| {
entry
.get("token")
.and_then(|t| t.get("refresh_token"))
.and_then(|v| v.as_str())
.map(|s| s.to_string())
});
if result.is_some() {
return result;
}
}
// Format 2: HashMap<String, TokenInfo> — values are TokenInfo structs
if let Some(obj) = cache.as_object() {
for value in obj.values() {
if let Some(rt) = value.get("refresh_token").and_then(|v| v.as_str()) {
return Some(rt.to_string());
}
}
}
None
}
/// Parse --scopes or --readonly from args, falling back to DEFAULT_SCOPES.
@@ -1223,8 +1251,8 @@ mod tests {
#[test]
fn extract_refresh_token_object_format() {
// Should return None — we only support array format
// HashMap<String, TokenInfo> format from EncryptedTokenStorage
let data = r#"{"key":{"access_token":"ya29","refresh_token":"1//tok"}}"#;
assert_eq!(extract_refresh_token(data), None);
assert_eq!(extract_refresh_token(data), Some("1//tok".to_string()));
}
}