fix: narrow default OAuth scopes to avoid restricted_client, improve non-interactive setup UX (#30)
* 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 * chore: add changeset * chore: cargo fmt * 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.
This commit is contained in:
@@ -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).
|
||||
+36
-2
@@ -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 <login|setup|status|export|logout>\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<String
|
||||
if args.iter().any(|a| a == "--readonly") {
|
||||
return READONLY_SCOPES.iter().map(|s| s.to_string()).collect();
|
||||
}
|
||||
if args.iter().any(|a| a == "--full") {
|
||||
return FULL_SCOPES.iter().map(|s| s.to_string()).collect();
|
||||
}
|
||||
|
||||
// Interactive scope picker when running in a TTY
|
||||
if !cfg!(test) && std::io::IsTerminal::is_terminal(&std::io::stdin()) {
|
||||
|
||||
+74
-8
@@ -1163,6 +1163,64 @@ async fn stage_enable_apis(ctx: &mut SetupContext) -> Result<SetupStage, GwsErro
|
||||
Ok(SetupStage::ConfigureOauth)
|
||||
}
|
||||
|
||||
/// Build actionable manual OAuth setup instructions for non-interactive environments.
|
||||
///
|
||||
/// Returned as the error message when `gws auth setup` cannot prompt interactively,
|
||||
/// so users get a clear checklist instead of a cryptic "run interactively" error.
|
||||
fn manual_oauth_instructions(project_id: &str) -> 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=\"<your-client-id>\"\n",
|
||||
" export GOOGLE_WORKSPACE_CLI_CLIENT_SECRET=\"<your-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<SetupStage, GwsError> {
|
||||
ctx.wiz(4, StepStatus::InProgress("Configuring...".into()));
|
||||
@@ -1175,9 +1233,9 @@ async fn stage_configure_oauth(ctx: &mut SetupContext) -> Result<SetupStage, Gws
|
||||
StepStatus::InProgress("Waiting for manual input...".into()),
|
||||
);
|
||||
if !ctx.interactive {
|
||||
return Err(GwsError::Validation(
|
||||
"Cannot automate OAuth client creation. Please run setup interactively.".to_string(),
|
||||
));
|
||||
return Err(GwsError::Validation(manual_oauth_instructions(
|
||||
&ctx.project_id,
|
||||
)));
|
||||
}
|
||||
|
||||
let (cid_result, csecret_result) = if let Some(ref mut w) = ctx.wizard {
|
||||
@@ -1186,11 +1244,19 @@ async fn stage_configure_oauth(ctx: &mut SetupContext) -> Result<SetupStage, Gws
|
||||
.and_then(|s| serde_json::from_str(&s).ok());
|
||||
|
||||
w.show_message(&format!(
|
||||
"Go to: https://console.cloud.google.com/apis/credentials/consent?project={}\n\
|
||||
Ensure 'External' consent screen is configured. Then,\n\
|
||||
Go to: https://console.cloud.google.com/apis/credentials?project={}\n\
|
||||
Click 'Create Credentials' -> '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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user