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
189 changes: 107 additions & 82 deletions src/auth_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -923,6 +923,80 @@ 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 {
None
};

let creds_json_str = if creds_json_str.is_none() && plain_path.exists() {
tokio::fs::read_to_string(&plain_path).await.ok()
} else {
creds_json_str
};

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) => {
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 +1141,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 +1191,7 @@ async fn handle_status() -> Result<(), GwsError> {
}
} // end !cfg!(test)


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