From 7c1910b27af9e3703efcfce69271427413e9882c Mon Sep 17 00:00:00 2001 From: dumko2001 Date: Wed, 18 Mar 2026 13:42:32 +0530 Subject: [PATCH 1/5] fix(auth): report token info in status when using GOOGLE_WORKSPACE_CLI_TOKEN --- src/auth_commands.rs | 160 ++++++++++++++++++++++--------------------- 1 file changed, 82 insertions(+), 78 deletions(-) diff --git a/src/auth_commands.rs b/src/auth_commands.rs index f51ba6dd..96dccc42 100644 --- a/src/auth_commands.rs +++ b/src/auth_commands.rs @@ -1067,91 +1067,95 @@ async fn handle_status() -> Result<(), GwsError> { } } // end !cfg!(test) - // If we have credentials, try to get live info (user, scopes, APIs) + // If we have credentials or a direct token, try to get live info (user, scopes, APIs) // Skip all network calls and subprocess spawning in test builds if !cfg!(test) { - let creds_json_str = if has_encrypted { - credential_store::load_encrypted().ok() - } else if has_plain { - tokio::fs::read_to_string(&plain_path).await.ok() + let direct_token = std::env::var("GOOGLE_WORKSPACE_CLI_TOKEN") + .ok() + .filter(|t| !t.is_empty()); + + let access_token = if let Some(token) = direct_token { + Some(token) } else { - None - }; + let creds_json_str = if has_encrypted { + credential_store::load_encrypted().ok() + } else if has_plain { + tokio::fs::read_to_string(&plain_path).await.ok() + } else { + None + }; - if let Some(creds_str) = creds_json_str { - if let Ok(creds) = serde_json::from_str::(&creds_str) { - let client_id = creds.get("client_id").and_then(|v| v.as_str()); - let client_secret = creds.get("client_secret").and_then(|v| v.as_str()); - let refresh_token = creds.get("refresh_token").and_then(|v| v.as_str()); - - if let (Some(cid), Some(csec), Some(rt)) = (client_id, client_secret, refresh_token) - { - // Exchange refresh token for access token - let http_client = reqwest::Client::new(); - let token_resp = http_client - .post("https://oauth2.googleapis.com/token") - .form(&[ - ("client_id", cid), - ("client_secret", csec), - ("refresh_token", rt), - ("grant_type", "refresh_token"), - ]) - .send() - .await; - - if let Ok(resp) = token_resp { - if let Ok(token_json) = resp.json::().await { - if let Some(access_token) = - token_json.get("access_token").and_then(|v| v.as_str()) - { - output["token_valid"] = json!(true); - - // Get user info - if let Ok(user_resp) = http_client - .get("https://www.googleapis.com/oauth2/v1/userinfo") - .bearer_auth(access_token) - .send() - .await - { - if let Ok(user_json) = - user_resp.json::().await - { - if let Some(email) = - user_json.get("email").and_then(|v| v.as_str()) - { - output["user"] = json!(email); - } - } - } - - // Get granted scopes via tokeninfo - let tokeninfo_url = format!( - "https://oauth2.googleapis.com/tokeninfo?access_token={}", - access_token - ); - if let Ok(info_resp) = http_client.get(&tokeninfo_url).send().await - { - if let Ok(info_json) = - info_resp.json::().await - { - if let Some(scope_str) = - info_json.get("scope").and_then(|v| v.as_str()) - { - let scopes: Vec<&str> = scope_str.split(' ').collect(); - output["scopes"] = json!(scopes); - output["scope_count"] = json!(scopes.len()); - } - } - } + if let Some(creds_str) = creds_json_str { + if let Ok(creds) = serde_json::from_str::(&creds_str) { + let client_id = creds.get("client_id").and_then(|v| v.as_str()); + let client_secret = creds.get("client_secret").and_then(|v| v.as_str()); + let refresh_token = creds.get("refresh_token").and_then(|v| v.as_str()); + + if let (Some(cid), Some(csec), Some(rt)) = + (client_id, client_secret, refresh_token) + { + // Exchange refresh token for access token + let http_client = reqwest::Client::new(); + let token_resp = http_client + .post("https://oauth2.googleapis.com/token") + .form(&[ + ("client_id", cid), + ("client_secret", csec), + ("refresh_token", rt), + ("grant_type", "refresh_token"), + ]) + .send() + .await; + + if let Ok(resp) = token_resp { + if let Ok(token_json) = resp.json::().await { + token_json + .get("access_token") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) } else { - output["token_valid"] = json!(false); - if let Some(err) = - token_json.get("error_description").and_then(|v| v.as_str()) - { - output["token_error"] = json!(err); - } + None } + } else { + None } + } else { + None + } + } else { + None + } + } else { + None + } + }; + + if let Some(at) = access_token { + output["token_valid"] = json!(true); + let http_client = reqwest::Client::new(); + + // Get user info + if let Ok(user_resp) = http_client + .get("https://www.googleapis.com/oauth2/v1/userinfo") + .bearer_auth(&at) + .send() + .await + { + if let Ok(user_json) = user_resp.json::().await { + if let Some(email) = user_json.get("email").and_then(|v| v.as_str()) { + output["user"] = json!(email); + } + } + } + + // Get granted scopes via tokeninfo + let tokeninfo_url = format!("https://oauth2.googleapis.com/tokeninfo?access_token={}", at); + if let Ok(info_resp) = http_client.get(&tokeninfo_url).send().await { + if let Ok(info_json) = info_resp.json::().await { + if let Some(scope_str) = info_json.get("scope").and_then(|v| v.as_str()) { + let scopes: Vec<&str> = scope_str.split(' ').collect(); + output["scopes"] = json!(scopes); + output["scope_count"] = json!(scopes.len()); } } } From 26696c9e3ca8e7616149c50855604d577c5983e2 Mon Sep 17 00:00:00 2001 From: dumko2001 Date: Wed, 18 Mar 2026 15:04:21 +0530 Subject: [PATCH 2/5] chore: add changeset for auth status fix --- .changeset/fix-auth-status-token.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 .changeset/fix-auth-status-token.md diff --git a/.changeset/fix-auth-status-token.md b/.changeset/fix-auth-status-token.md new file mode 100644 index 00000000..23899cd2 --- /dev/null +++ b/.changeset/fix-auth-status-token.md @@ -0,0 +1 @@ +---\n"gws": patch\n---\n\nfix(auth): report token info in status when using GOOGLE_WORKSPACE_CLI_TOKEN to improve clarity From 4fdac2bcea359ab5502b2380313918c2c6d9e1cb Mon Sep 17 00:00:00 2001 From: dumko2001 Date: Wed, 18 Mar 2026 15:12:59 +0530 Subject: [PATCH 3/5] chore: correct changeset package name --- .changeset/fix-auth-status-token.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/fix-auth-status-token.md b/.changeset/fix-auth-status-token.md index 23899cd2..84c6cf24 100644 --- a/.changeset/fix-auth-status-token.md +++ b/.changeset/fix-auth-status-token.md @@ -1 +1 @@ ----\n"gws": patch\n---\n\nfix(auth): report token info in status when using GOOGLE_WORKSPACE_CLI_TOKEN to improve clarity +---\n"@googleworkspace/cli": patch\n---\n\nfix(auth): report token info in status when using GOOGLE_WORKSPACE_CLI_TOKEN to improve clarity From a8c203ee044f9d08a0c9357e4dd61d66c725bad5 Mon Sep 17 00:00:00 2001 From: dumko2001 Date: Wed, 18 Mar 2026 15:57:23 +0530 Subject: [PATCH 4/5] fix(auth): restore error reporting for failed token refresh in status --- src/auth_commands.rs | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/src/auth_commands.rs b/src/auth_commands.rs index 96dccc42..54940f37 100644 --- a/src/auth_commands.rs +++ b/src/auth_commands.rs @@ -1107,17 +1107,32 @@ async fn handle_status() -> Result<(), GwsError> { .send() .await; - if let Ok(resp) = token_resp { - if let Ok(token_json) = resp.json::().await { - token_json - .get("access_token") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()) - } else { + match token_resp { + Ok(resp) => { + if let Ok(token_json) = resp.json::().await { + if let Some(access_token) = + token_json.get("access_token").and_then(|v| v.as_str()) + { + Some(access_token.to_string()) + } else { + output["token_valid"] = json!(false); + if let Some(err) = token_json + .get("error_description") + .and_then(|v| v.as_str()) + { + output["token_error"] = json!(err); + } + None + } + } else { + None + } + } + Err(e) => { + output["token_valid"] = json!(false); + output["token_error"] = json!(e.to_string()); None } - } else { - None } } else { None From 07beab6373169a755cbfee131fdfd5ef7e288099 Mon Sep 17 00:00:00 2001 From: dumko2001 Date: Wed, 18 Mar 2026 16:16:50 +0530 Subject: [PATCH 5/5] fix(auth): reuse HTTP client and use URL-encoded token in status command --- src/auth_commands.rs | 151 +++++++++++++++++++++---------------------- 1 file changed, 74 insertions(+), 77 deletions(-) diff --git a/src/auth_commands.rs b/src/auth_commands.rs index 54940f37..07e7023f 100644 --- a/src/auth_commands.rs +++ b/src/auth_commands.rs @@ -923,6 +923,71 @@ fn run_simple_scope_picker(services_filter: Option<&HashSet>) -> Option< } } +async fn get_status_access_token( + http_client: &reqwest::Client, + output: &mut serde_json::Value, +) -> Option { + let direct_token = std::env::var("GOOGLE_WORKSPACE_CLI_TOKEN") + .ok() + .filter(|t| !t.is_empty()); + + if let Some(token) = direct_token { + return Some(token); + } + + let enc_path = credential_store::encrypted_credentials_path(); + let plain_path = plain_credentials_path(); + + let creds_json_str = if enc_path.exists() { + credential_store::load_encrypted().ok() + } else if plain_path.exists() { + std::fs::read_to_string(&plain_path).ok() + } else { + None + }; + + let creds_str = creds_json_str?; + let creds: serde_json::Value = serde_json::from_str(&creds_str).ok()?; + let client_id = creds.get("client_id")?.as_str()?; + let client_secret = creds.get("client_secret")?.as_str()?; + let refresh_token = creds.get("refresh_token")?.as_str()?; + + // Exchange refresh token for access token + let token_resp = http_client + .post("https://oauth2.googleapis.com/token") + .form(&[ + ("client_id", client_id), + ("client_secret", client_secret), + ("refresh_token", refresh_token), + ("grant_type", "refresh_token"), + ]) + .send() + .await; + + match token_resp { + Ok(resp) => { + if let Ok(token_json) = resp.json::().await { + if let Some(access_token) = token_json.get("access_token").and_then(|v| v.as_str()) { + Some(access_token.to_string()) + } else { + output["token_valid"] = serde_json::json!(false); + if let Some(err) = token_json.get("error_description").and_then(|v| v.as_str()) { + output["token_error"] = serde_json::json!(err); + } + None + } + } else { + None + } + } + Err(e) => { + output["token_valid"] = serde_json::json!(false); + output["token_error"] = serde_json::json!(e.to_string()); + None + } + } +} + async fn handle_status() -> Result<(), GwsError> { let plain_path = plain_credentials_path(); let enc_path = credential_store::encrypted_credentials_path(); @@ -1070,84 +1135,11 @@ async fn handle_status() -> Result<(), GwsError> { // If we have credentials or a direct token, try to get live info (user, scopes, APIs) // Skip all network calls and subprocess spawning in test builds if !cfg!(test) { - let direct_token = std::env::var("GOOGLE_WORKSPACE_CLI_TOKEN") - .ok() - .filter(|t| !t.is_empty()); - - let access_token = if let Some(token) = direct_token { - Some(token) - } else { - let creds_json_str = if has_encrypted { - credential_store::load_encrypted().ok() - } else if has_plain { - tokio::fs::read_to_string(&plain_path).await.ok() - } else { - None - }; - - if let Some(creds_str) = creds_json_str { - if let Ok(creds) = serde_json::from_str::(&creds_str) { - let client_id = creds.get("client_id").and_then(|v| v.as_str()); - let client_secret = creds.get("client_secret").and_then(|v| v.as_str()); - let refresh_token = creds.get("refresh_token").and_then(|v| v.as_str()); - - if let (Some(cid), Some(csec), Some(rt)) = - (client_id, client_secret, refresh_token) - { - // Exchange refresh token for access token - let http_client = reqwest::Client::new(); - let token_resp = http_client - .post("https://oauth2.googleapis.com/token") - .form(&[ - ("client_id", cid), - ("client_secret", csec), - ("refresh_token", rt), - ("grant_type", "refresh_token"), - ]) - .send() - .await; - - match token_resp { - Ok(resp) => { - if let Ok(token_json) = resp.json::().await { - if let Some(access_token) = - token_json.get("access_token").and_then(|v| v.as_str()) - { - Some(access_token.to_string()) - } else { - output["token_valid"] = json!(false); - if let Some(err) = token_json - .get("error_description") - .and_then(|v| v.as_str()) - { - output["token_error"] = json!(err); - } - None - } - } else { - None - } - } - Err(e) => { - output["token_valid"] = json!(false); - output["token_error"] = json!(e.to_string()); - None - } - } - } else { - None - } - } else { - None - } - } else { - None - } - }; + let http_client = reqwest::Client::new(); + let access_token = get_status_access_token(&http_client, &mut output).await; if let Some(at) = access_token { output["token_valid"] = json!(true); - let http_client = reqwest::Client::new(); // Get user info if let Ok(user_resp) = http_client @@ -1164,8 +1156,12 @@ async fn handle_status() -> Result<(), GwsError> { } // Get granted scopes via tokeninfo - let tokeninfo_url = format!("https://oauth2.googleapis.com/tokeninfo?access_token={}", at); - if let Ok(info_resp) = http_client.get(&tokeninfo_url).send().await { + if let Ok(info_resp) = http_client + .get("https://oauth2.googleapis.com/tokeninfo") + .query(&[("access_token", &at)]) + .send() + .await + { if let Ok(info_json) = info_resp.json::().await { if let Some(scope_str) = info_json.get("scope").and_then(|v| v.as_str()) { let scopes: Vec<&str> = scope_str.split(' ').collect(); @@ -1186,6 +1182,7 @@ async fn handle_status() -> Result<(), GwsError> { } } // end !cfg!(test) + println!( "{}", serde_json::to_string_pretty(&output).unwrap_or_default()