diff --git a/.changeset/9df09438f1eb.md b/.changeset/9df09438f1eb.md new file mode 100644 index 00000000..ae644e45 --- /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). diff --git a/src/auth_commands.rs b/src/auth_commands.rs index d7278e3b..6622fbbe 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!( + 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 + ) +} + /// 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 +1233,9 @@ async fn stage_configure_oauth(ctx: &mut SetupContext) -> Result Result 'OAuth client ID' -> 'Desktop app'", - ctx.project_id, ctx.project_id + concat!( + "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();