Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changeset/fix-auth-status-token.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
---\n"@googleworkspace/cli": patch\n---\n\nfix(auth): report token info in status when using GOOGLE_WORKSPACE_CLI_TOKEN to improve clarity
180 changes: 98 additions & 82 deletions src/auth_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -923,6 +923,71 @@ fn run_simple_scope_picker(services_filter: Option<&HashSet<String>>) -> Option<
}
}

async fn get_status_access_token(
http_client: &reqwest::Client,
output: &mut serde_json::Value,
) -> Option<String> {
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
};
Comment on lines +941 to +947
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This implementation uses a synchronous file read (std::fs::read_to_string) within an async function. This blocks the execution thread and is a performance regression from the previous implementation which used tokio::fs::read_to_string. To avoid blocking, you should use the asynchronous version and restructure the logic slightly. Note that credential_store::load_encrypted() is also synchronous, but this suggestion focuses on the immediate regression introduced in this change.

Suggested change
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 mut creds_json_str = if enc_path.exists() {
credential_store::load_encrypted().ok()
} else {
None
};
if creds_json_str.is_none() && plain_path.exists() {
creds_json_str = tokio::fs::read_to_string(&plain_path).await.ok();
}


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::<serde_json::Value>().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
}
Comment on lines +969 to +981
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There's a missing error handling case here. If resp.json::<serde_json::Value>().await fails (e.g., due to a malformed response from the server or a network issue), the else branch on line 979 is taken, which returns None without updating the output JSON value. This means the user will not see any error message and might be confused about why their authentication status is not being reported correctly. You should handle this error case and populate token_valid and token_error in the output.

Suggested change
if let Ok(token_json) = resp.json::<serde_json::Value>().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
}
match resp.json::<serde_json::Value>().await {
Ok(token_json) => {
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
}
}
Err(e) => {
output["token_valid"] = serde_json::json!(false);
output["token_error"] = serde_json::json!(format!("Failed to parse token response: {e}"));
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();
Expand Down Expand Up @@ -1067,91 +1132,41 @@ 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()
} 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);

// 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::<serde_json::Value>().await {
if let Some(email) = user_json.get("email").and_then(|v| v.as_str()) {
output["user"] = json!(email);
}
}
}

if let Some(creds_str) = creds_json_str {
if let Ok(creds) = serde_json::from_str::<serde_json::Value>(&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::<serde_json::Value>().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::<serde_json::Value>().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::<serde_json::Value>().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());
}
}
}
} 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);
}
}
}
// Get granted scopes via tokeninfo
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::<serde_json::Value>().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());
}
}
}
Expand All @@ -1167,6 +1182,7 @@ async fn handle_status() -> Result<(), GwsError> {
}
} // end !cfg!(test)


println!(
"{}",
serde_json::to_string_pretty(&output).unwrap_or_default()
Expand Down
Loading