Skip to content

Commit 6f26305

Browse files
author
Justin Poehnelt
authored
fix: decrypt token cache before extracting refresh token (googleworkspace#11)
1 parent 5312bc5 commit 6f26305

2 files changed

Lines changed: 44 additions & 11 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@googleworkspace/cli": patch
3+
---
4+
5+
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

src/auth_commands.rs

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,12 @@ async fn handle_login(args: &[String]) -> Result<(), GwsError> {
173173

174174
if token.token().is_some() {
175175
// Read yup-oauth2's token cache to extract the refresh_token.
176-
let token_data = std::fs::read_to_string(&temp_path).unwrap_or_default();
176+
// EncryptedTokenStorage stores data encrypted, so we must decrypt first.
177+
let token_data = std::fs::read(&temp_path)
178+
.ok()
179+
.and_then(|bytes| crate::credential_store::decrypt(&bytes).ok())
180+
.and_then(|decrypted| String::from_utf8(decrypted).ok())
181+
.unwrap_or_default();
177182
let refresh_token = extract_refresh_token(&token_data).ok_or_else(|| {
178183
GwsError::Auth(
179184
"OAuth flow completed but no refresh token was returned. \
@@ -891,16 +896,39 @@ fn handle_logout() -> Result<(), GwsError> {
891896
}
892897

893898
/// Extract refresh_token from yup-oauth2 v12 token cache.
894-
/// Format: [{"scopes":[...], "token":{"access_token":..., "refresh_token":...}}]
899+
///
900+
/// Supports two formats:
901+
/// 1. Array format (yup-oauth2 default file storage):
902+
/// [{"scopes":[...], "token":{"access_token":..., "refresh_token":...}}]
903+
/// 2. Object/HashMap format (EncryptedTokenStorage serialization):
904+
/// {"scope_key": {"access_token":..., "refresh_token":..., ...}}
895905
pub fn extract_refresh_token(token_data: &str) -> Option<String> {
896906
let cache: serde_json::Value = serde_json::from_str(token_data).ok()?;
897-
cache.as_array()?.iter().find_map(|entry| {
898-
entry
899-
.get("token")
900-
.and_then(|t| t.get("refresh_token"))
901-
.and_then(|v| v.as_str())
902-
.map(|s| s.to_string())
903-
})
907+
908+
// Format 1: array of {scopes, token} entries
909+
if let Some(arr) = cache.as_array() {
910+
let result = arr.iter().find_map(|entry| {
911+
entry
912+
.get("token")
913+
.and_then(|t| t.get("refresh_token"))
914+
.and_then(|v| v.as_str())
915+
.map(|s| s.to_string())
916+
});
917+
if result.is_some() {
918+
return result;
919+
}
920+
}
921+
922+
// Format 2: HashMap<String, TokenInfo> — values are TokenInfo structs
923+
if let Some(obj) = cache.as_object() {
924+
for value in obj.values() {
925+
if let Some(rt) = value.get("refresh_token").and_then(|v| v.as_str()) {
926+
return Some(rt.to_string());
927+
}
928+
}
929+
}
930+
931+
None
904932
}
905933

906934
/// Parse --scopes or --readonly from args, falling back to DEFAULT_SCOPES.
@@ -1223,8 +1251,8 @@ mod tests {
12231251

12241252
#[test]
12251253
fn extract_refresh_token_object_format() {
1226-
// Should return None — we only support array format
1254+
// HashMap<String, TokenInfo> format from EncryptedTokenStorage
12271255
let data = r#"{"key":{"access_token":"ya29","refresh_token":"1//tok"}}"#;
1228-
assert_eq!(extract_refresh_token(data), None);
1256+
assert_eq!(extract_refresh_token(data), Some("1//tok".to_string()));
12291257
}
12301258
}

0 commit comments

Comments
 (0)