From 83881d6037eadd2e7073a274f2dce7e4158d99be Mon Sep 17 00:00:00 2001 From: jpoehnelt-bot Date: Tue, 3 Mar 2026 21:20:50 -0700 Subject: [PATCH 1/4] fix: narrow default OAuth scopes to avoid restricted_client, add --full flag, improve non-interactive setup UX Fixes #24, #25 - DEFAULT_SCOPES now aliases MINIMAL_SCOPES (no pubsub/cloud-platform) which avoids Google's restricted_client 403 on unverified OAuth apps - Add FULL_SCOPES and --full flag for users who need the broader set - Replace cryptic 'run setup interactively' error with step-by-step manual OAuth console instructions including URLs, options A/B/C --- src/auth_commands.rs | 38 ++++++++++++++++++-- src/setup.rs | 86 +++++++++++++++++++++++++++++++++++++++----- 2 files changed, 114 insertions(+), 10 deletions(-) diff --git a/src/auth_commands.rs b/src/auth_commands.rs index d7278e3b7..6622fbbe6 100644 --- a/src/auth_commands.rs +++ b/src/auth_commands.rs @@ -19,8 +19,37 @@ use serde_json::json; use crate::credential_store; use crate::error::GwsError; -/// Default scopes for login — broad Workspace access. -pub const DEFAULT_SCOPES: &[&str] = &[ +/// Minimal scopes for first-run login — only core Workspace APIs that never +/// trigger Google's `restricted_client` / unverified-app block. +/// +/// These are the safest scopes for unverified OAuth apps and personal Cloud +/// projects. Users can request broader access with `--scopes` or `--full`. +pub const MINIMAL_SCOPES: &[&str] = &[ + "https://www.googleapis.com/auth/drive", + "https://www.googleapis.com/auth/spreadsheets", + "https://www.googleapis.com/auth/gmail.modify", + "https://www.googleapis.com/auth/calendar", + "https://www.googleapis.com/auth/documents", + "https://www.googleapis.com/auth/presentations", + "https://www.googleapis.com/auth/tasks", +]; + +/// Default scopes for login. Alias for [`MINIMAL_SCOPES`] — deliberately kept +/// narrow so first-run logins succeed even with an unverified OAuth app. +/// +/// Previously this included `pubsub` and `cloud-platform`, which Google marks +/// as *restricted* and blocks for unverified apps, causing `Error 403: +/// restricted_client`. Use `--scopes` to add those scopes explicitly when you +/// have a verified app or a GCP project with the APIs enabled and approved. +pub const DEFAULT_SCOPES: &[&str] = MINIMAL_SCOPES; + +/// Full scopes — all common Workspace APIs plus GCP platform access. +/// +/// Use `gws auth login --full` to request these. Unverified OAuth apps will +/// receive a Google consent-screen warning, and some scopes (e.g. `pubsub`, +/// `cloud-platform`) require app verification or a Workspace domain admin to +/// grant access. +pub const FULL_SCOPES: &[&str] = &[ "https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/spreadsheets", "https://www.googleapis.com/auth/gmail.modify", @@ -72,6 +101,8 @@ pub async fn handle_auth_command(args: &[String]) -> Result<(), GwsError> { "Usage: gws auth \n\n\ login Authenticate via OAuth2 (opens browser)\n\ --readonly Request read-only scopes\n\ + --full Request all scopes incl. pubsub + cloud-platform\n\ + (may trigger restricted_client for unverified apps)\n\ --scopes Comma-separated custom scopes\n\ setup Configure GCP project + OAuth client (requires gcloud)\n\ --project Use a specific GCP project\n\ @@ -317,6 +348,9 @@ async fn resolve_scopes(args: &[String], project_id: Option<&str>) -> Vec Result String { + let consent_url = if project_id.is_empty() { + "https://console.cloud.google.com/apis/credentials/consent".to_string() + } else { + format!( + "https://console.cloud.google.com/apis/credentials/consent?project={}", + project_id + ) + }; + let creds_url = if project_id.is_empty() { + "https://console.cloud.google.com/apis/credentials".to_string() + } else { + format!( + "https://console.cloud.google.com/apis/credentials?project={}", + project_id + ) + }; + + format!( + "OAuth client creation requires manual setup in the Google Cloud Console.\n\ + \n\ + Follow these steps:\n\ + \n\ + 1. Configure the OAuth consent screen (if not already done):\n\ + {consent_url}\n\ + → User Type: External\n\ + → App name: gws CLI (or your preferred name)\n\ + → Support email: your Google account email\n\ + → Save and continue through all screens\n\ + \n\ + 2. Create an OAuth client ID:\n\ + {creds_url}\n\ + → Click 'Create Credentials' → 'OAuth client ID'\n\ + → Application type: Desktop app\n\ + → Name: gws CLI (or your preferred name)\n\ + → Click 'Create'\n\ + \n\ + 3. Copy the Client ID and Client Secret shown in the dialog.\n\ + \n\ + 4. Provide the credentials to gws using one of these methods:\n\ + \n\ + Option A — Environment variables (recommended for CI/scripts):\n\ + export GOOGLE_WORKSPACE_CLI_CLIENT_ID=\"\"\n\ + export GOOGLE_WORKSPACE_CLI_CLIENT_SECRET=\"\"\n\ + gws auth login\n\ + \n\ + Option B — Download the JSON file:\n\ + Download 'client_secret_*.json' from the Cloud Console dialog\n\ + and save it to: ~/.config/gws/client_secret.json\n\ + Then run: gws auth login\n\ + \n\ + Option C — Re-run setup interactively (recommended for first-time setup):\n\ + gws auth setup\n\ + \n\ + Note: The redirect URI used by gws is http://localhost (auto-negotiated port).\n\ + Desktop app clients do not require you to register a redirect URI manually." + ) +} + /// Stage 5: Configure OAuth consent screen and collect client credentials. async fn stage_configure_oauth(ctx: &mut SetupContext) -> Result { ctx.wiz(4, StepStatus::InProgress("Configuring...".into())); @@ -1175,9 +1238,7 @@ async fn stage_configure_oauth(ctx: &mut SetupContext) -> Result Result 'OAuth client ID' -> 'Desktop app'", - ctx.project_id, ctx.project_id + "Manual OAuth client setup required.\n\ + \n\ + Step A — Consent screen (if not configured):\n\ + https://console.cloud.google.com/apis/credentials/consent?project={project}\n\ + → User Type: External, then save through all screens.\n\ + \n\ + Step B — Create an OAuth client:\n\ + https://console.cloud.google.com/apis/credentials?project={project}\n\ + → 'Create Credentials' → 'OAuth client ID'\n\ + → Application type: Desktop app\n\ + → Redirect URI: http://localhost (auto-negotiated; no manual entry needed)\n\ + \n\ + Copy the Client ID and Client Secret from the dialog, then paste them below.", + project = ctx.project_id )) .ok(); From b1e5d20fde24e6f16a6d4f5a933297b5a086a9a0 Mon Sep 17 00:00:00 2001 From: jpoehnelt-bot Date: Tue, 3 Mar 2026 21:20:54 -0700 Subject: [PATCH 2/4] chore: add changeset --- .changeset/9df09438f1eb.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/9df09438f1eb.md diff --git a/.changeset/9df09438f1eb.md b/.changeset/9df09438f1eb.md new file mode 100644 index 000000000..ae644e45d --- /dev/null +++ b/.changeset/9df09438f1eb.md @@ -0,0 +1,5 @@ +--- +"@googleworkspace/cli": patch +--- + +Narrow default OAuth scopes to avoid `Error 403: restricted_client` on unverified apps and add a `--full` flag for broader access (fixes #25). Replace the cryptic non-interactive setup error with actionable step-by-step OAuth console instructions (fixes #24). From d415d8b6298618a6a58f5e8df063982c8c476467 Mon Sep 17 00:00:00 2001 From: jpoehnelt-bot Date: Tue, 3 Mar 2026 21:24:28 -0700 Subject: [PATCH 3/4] chore: cargo fmt --- src/setup.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/setup.rs b/src/setup.rs index 9b1d7cdd5..94bfd1523 100644 --- a/src/setup.rs +++ b/src/setup.rs @@ -1238,7 +1238,9 @@ async fn stage_configure_oauth(ctx: &mut SetupContext) -> Result Date: Tue, 3 Mar 2026 21:30:57 -0700 Subject: [PATCH 4/4] fix: refactor format! with backslash continuations to concat! macro Address Gemini review (PR #30): replace hard-to-read backslash line continuations in large format! macros with concat! for clearer structure: - manual_oauth_instructions(): full step-by-step guide - stage_configure_oauth() wizard show_message: interactive prompt text No functional change; output text is identical. --- src/setup.rs | 94 ++++++++++++++++++++++++---------------------------- 1 file changed, 44 insertions(+), 50 deletions(-) diff --git a/src/setup.rs b/src/setup.rs index 94bfd1523..2a2f83534 100644 --- a/src/setup.rs +++ b/src/setup.rs @@ -1186,43 +1186,38 @@ fn manual_oauth_instructions(project_id: &str) -> String { }; format!( - "OAuth client creation requires manual setup in the Google Cloud Console.\n\ - \n\ - Follow these steps:\n\ - \n\ - 1. Configure the OAuth consent screen (if not already done):\n\ - {consent_url}\n\ - → User Type: External\n\ - → App name: gws CLI (or your preferred name)\n\ - → Support email: your Google account email\n\ - → Save and continue through all screens\n\ - \n\ - 2. Create an OAuth client ID:\n\ - {creds_url}\n\ - → Click 'Create Credentials' → 'OAuth client ID'\n\ - → Application type: Desktop app\n\ - → Name: gws CLI (or your preferred name)\n\ - → Click 'Create'\n\ - \n\ - 3. Copy the Client ID and Client Secret shown in the dialog.\n\ - \n\ - 4. Provide the credentials to gws using one of these methods:\n\ - \n\ - Option A — Environment variables (recommended for CI/scripts):\n\ - export GOOGLE_WORKSPACE_CLI_CLIENT_ID=\"\"\n\ - export GOOGLE_WORKSPACE_CLI_CLIENT_SECRET=\"\"\n\ - gws auth login\n\ - \n\ - Option B — Download the JSON file:\n\ - Download 'client_secret_*.json' from the Cloud Console dialog\n\ - and save it to: ~/.config/gws/client_secret.json\n\ - Then run: gws auth login\n\ - \n\ - Option C — Re-run setup interactively (recommended for first-time setup):\n\ - gws auth setup\n\ - \n\ - Note: The redirect URI used by gws is http://localhost (auto-negotiated port).\n\ - Desktop app clients do not require you to register a redirect URI manually." + concat!( + "OAuth client creation requires manual setup in the Google Cloud Console.\n\n", + "Follow these steps:\n\n", + "1. Configure the OAuth consent screen (if not already done):\n", + " {consent_url}\n", + " → User Type: External\n", + " → App name: gws CLI (or your preferred name)\n", + " → Support email: your Google account email\n", + " → Save and continue through all screens\n\n", + "2. Create an OAuth client ID:\n", + " {creds_url}\n", + " → Click 'Create Credentials' → 'OAuth client ID'\n", + " → Application type: Desktop app\n", + " → Name: gws CLI (or your preferred name)\n", + " → Click 'Create'\n\n", + "3. Copy the Client ID and Client Secret shown in the dialog.\n\n", + "4. Provide the credentials to gws using one of these methods:\n\n", + " Option A — Environment variables (recommended for CI/scripts):\n", + " export GOOGLE_WORKSPACE_CLI_CLIENT_ID=\"\"\n", + " export GOOGLE_WORKSPACE_CLI_CLIENT_SECRET=\"\"\n", + " gws auth login\n\n", + " Option B — Download the JSON file:\n", + " Download 'client_secret_*.json' from the Cloud Console dialog\n", + " and save it to: ~/.config/gws/client_secret.json\n", + " Then run: gws auth login\n\n", + " Option C — Re-run setup interactively (recommended for first-time setup):\n", + " gws auth setup\n\n", + "Note: The redirect URI used by gws is http://localhost (auto-negotiated port).\n", + "Desktop app clients do not require you to register a redirect URI manually." + ), + consent_url = consent_url, + creds_url = creds_url ) } @@ -1249,19 +1244,18 @@ async fn stage_configure_oauth(ctx: &mut SetupContext) -> Result