Skip to content

Commit 83881d6

Browse files
committed
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
1 parent bdb458b commit 83881d6

2 files changed

Lines changed: 114 additions & 10 deletions

File tree

src/auth_commands.rs

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,37 @@ use serde_json::json;
1919
use crate::credential_store;
2020
use crate::error::GwsError;
2121

22-
/// Default scopes for login — broad Workspace access.
23-
pub const DEFAULT_SCOPES: &[&str] = &[
22+
/// Minimal scopes for first-run login — only core Workspace APIs that never
23+
/// trigger Google's `restricted_client` / unverified-app block.
24+
///
25+
/// These are the safest scopes for unverified OAuth apps and personal Cloud
26+
/// projects. Users can request broader access with `--scopes` or `--full`.
27+
pub const MINIMAL_SCOPES: &[&str] = &[
28+
"https://www.googleapis.com/auth/drive",
29+
"https://www.googleapis.com/auth/spreadsheets",
30+
"https://www.googleapis.com/auth/gmail.modify",
31+
"https://www.googleapis.com/auth/calendar",
32+
"https://www.googleapis.com/auth/documents",
33+
"https://www.googleapis.com/auth/presentations",
34+
"https://www.googleapis.com/auth/tasks",
35+
];
36+
37+
/// Default scopes for login. Alias for [`MINIMAL_SCOPES`] — deliberately kept
38+
/// narrow so first-run logins succeed even with an unverified OAuth app.
39+
///
40+
/// Previously this included `pubsub` and `cloud-platform`, which Google marks
41+
/// as *restricted* and blocks for unverified apps, causing `Error 403:
42+
/// restricted_client`. Use `--scopes` to add those scopes explicitly when you
43+
/// have a verified app or a GCP project with the APIs enabled and approved.
44+
pub const DEFAULT_SCOPES: &[&str] = MINIMAL_SCOPES;
45+
46+
/// Full scopes — all common Workspace APIs plus GCP platform access.
47+
///
48+
/// Use `gws auth login --full` to request these. Unverified OAuth apps will
49+
/// receive a Google consent-screen warning, and some scopes (e.g. `pubsub`,
50+
/// `cloud-platform`) require app verification or a Workspace domain admin to
51+
/// grant access.
52+
pub const FULL_SCOPES: &[&str] = &[
2453
"https://www.googleapis.com/auth/drive",
2554
"https://www.googleapis.com/auth/spreadsheets",
2655
"https://www.googleapis.com/auth/gmail.modify",
@@ -72,6 +101,8 @@ pub async fn handle_auth_command(args: &[String]) -> Result<(), GwsError> {
72101
"Usage: gws auth <login|setup|status|export|logout>\n\n\
73102
login Authenticate via OAuth2 (opens browser)\n\
74103
--readonly Request read-only scopes\n\
104+
--full Request all scopes incl. pubsub + cloud-platform\n\
105+
(may trigger restricted_client for unverified apps)\n\
75106
--scopes Comma-separated custom scopes\n\
76107
setup Configure GCP project + OAuth client (requires gcloud)\n\
77108
--project Use a specific GCP project\n\
@@ -317,6 +348,9 @@ async fn resolve_scopes(args: &[String], project_id: Option<&str>) -> Vec<String
317348
if args.iter().any(|a| a == "--readonly") {
318349
return READONLY_SCOPES.iter().map(|s| s.to_string()).collect();
319350
}
351+
if args.iter().any(|a| a == "--full") {
352+
return FULL_SCOPES.iter().map(|s| s.to_string()).collect();
353+
}
320354

321355
// Interactive scope picker when running in a TTY
322356
if !cfg!(test) && std::io::IsTerminal::is_terminal(&std::io::stdin()) {

src/setup.rs

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1163,6 +1163,69 @@ async fn stage_enable_apis(ctx: &mut SetupContext) -> Result<SetupStage, GwsErro
11631163
Ok(SetupStage::ConfigureOauth)
11641164
}
11651165

1166+
/// Build actionable manual OAuth setup instructions for non-interactive environments.
1167+
///
1168+
/// Returned as the error message when `gws auth setup` cannot prompt interactively,
1169+
/// so users get a clear checklist instead of a cryptic "run interactively" error.
1170+
fn manual_oauth_instructions(project_id: &str) -> String {
1171+
let consent_url = if project_id.is_empty() {
1172+
"https://console.cloud.google.com/apis/credentials/consent".to_string()
1173+
} else {
1174+
format!(
1175+
"https://console.cloud.google.com/apis/credentials/consent?project={}",
1176+
project_id
1177+
)
1178+
};
1179+
let creds_url = if project_id.is_empty() {
1180+
"https://console.cloud.google.com/apis/credentials".to_string()
1181+
} else {
1182+
format!(
1183+
"https://console.cloud.google.com/apis/credentials?project={}",
1184+
project_id
1185+
)
1186+
};
1187+
1188+
format!(
1189+
"OAuth client creation requires manual setup in the Google Cloud Console.\n\
1190+
\n\
1191+
Follow these steps:\n\
1192+
\n\
1193+
1. Configure the OAuth consent screen (if not already done):\n\
1194+
{consent_url}\n\
1195+
→ User Type: External\n\
1196+
→ App name: gws CLI (or your preferred name)\n\
1197+
→ Support email: your Google account email\n\
1198+
→ Save and continue through all screens\n\
1199+
\n\
1200+
2. Create an OAuth client ID:\n\
1201+
{creds_url}\n\
1202+
→ Click 'Create Credentials' → 'OAuth client ID'\n\
1203+
→ Application type: Desktop app\n\
1204+
→ Name: gws CLI (or your preferred name)\n\
1205+
→ Click 'Create'\n\
1206+
\n\
1207+
3. Copy the Client ID and Client Secret shown in the dialog.\n\
1208+
\n\
1209+
4. Provide the credentials to gws using one of these methods:\n\
1210+
\n\
1211+
Option A — Environment variables (recommended for CI/scripts):\n\
1212+
export GOOGLE_WORKSPACE_CLI_CLIENT_ID=\"<your-client-id>\"\n\
1213+
export GOOGLE_WORKSPACE_CLI_CLIENT_SECRET=\"<your-client-secret>\"\n\
1214+
gws auth login\n\
1215+
\n\
1216+
Option B — Download the JSON file:\n\
1217+
Download 'client_secret_*.json' from the Cloud Console dialog\n\
1218+
and save it to: ~/.config/gws/client_secret.json\n\
1219+
Then run: gws auth login\n\
1220+
\n\
1221+
Option C — Re-run setup interactively (recommended for first-time setup):\n\
1222+
gws auth setup\n\
1223+
\n\
1224+
Note: The redirect URI used by gws is http://localhost (auto-negotiated port).\n\
1225+
Desktop app clients do not require you to register a redirect URI manually."
1226+
)
1227+
}
1228+
11661229
/// Stage 5: Configure OAuth consent screen and collect client credentials.
11671230
async fn stage_configure_oauth(ctx: &mut SetupContext) -> Result<SetupStage, GwsError> {
11681231
ctx.wiz(4, StepStatus::InProgress("Configuring...".into()));
@@ -1175,9 +1238,7 @@ async fn stage_configure_oauth(ctx: &mut SetupContext) -> Result<SetupStage, Gws
11751238
StepStatus::InProgress("Waiting for manual input...".into()),
11761239
);
11771240
if !ctx.interactive {
1178-
return Err(GwsError::Validation(
1179-
"Cannot automate OAuth client creation. Please run setup interactively.".to_string(),
1180-
));
1241+
return Err(GwsError::Validation(manual_oauth_instructions(&ctx.project_id)));
11811242
}
11821243

11831244
let (cid_result, csecret_result) = if let Some(ref mut w) = ctx.wizard {
@@ -1186,11 +1247,20 @@ async fn stage_configure_oauth(ctx: &mut SetupContext) -> Result<SetupStage, Gws
11861247
.and_then(|s| serde_json::from_str(&s).ok());
11871248

11881249
w.show_message(&format!(
1189-
"Go to: https://console.cloud.google.com/apis/credentials/consent?project={}\n\
1190-
Ensure 'External' consent screen is configured. Then,\n\
1191-
Go to: https://console.cloud.google.com/apis/credentials?project={}\n\
1192-
Click 'Create Credentials' -> 'OAuth client ID' -> 'Desktop app'",
1193-
ctx.project_id, ctx.project_id
1250+
"Manual OAuth client setup required.\n\
1251+
\n\
1252+
Step A — Consent screen (if not configured):\n\
1253+
https://console.cloud.google.com/apis/credentials/consent?project={project}\n\
1254+
→ User Type: External, then save through all screens.\n\
1255+
\n\
1256+
Step B — Create an OAuth client:\n\
1257+
https://console.cloud.google.com/apis/credentials?project={project}\n\
1258+
→ 'Create Credentials' → 'OAuth client ID'\n\
1259+
→ Application type: Desktop app\n\
1260+
→ Redirect URI: http://localhost (auto-negotiated; no manual entry needed)\n\
1261+
\n\
1262+
Copy the Client ID and Client Secret from the dialog, then paste them below.",
1263+
project = ctx.project_id
11941264
))
11951265
.ok();
11961266

0 commit comments

Comments
 (0)