From ff5a3fbe5dd0cedd85631f205e1fe65ad6865e84 Mon Sep 17 00:00:00 2001 From: Victor Maestro Lara Date: Wed, 18 Mar 2026 23:10:21 +0100 Subject: [PATCH 1/3] feat(auth): add --port flag to gws auth login Organizations with strict Google Workspace admin policies (admin_policy_enforced) block the "installed" (Desktop) OAuth flow that uses a random port. This adds a --port flag that uses yup_oauth2's HTTPPortRedirect with a fixed port, enabling authentication with Web Application type OAuth clients. Usage: gws auth login --port 8080 When --port is provided, uses HTTPPortRedirect(port) with a fixed redirect URI (http://localhost:). When omitted, preserves current behavior (random port, Desktop flow). Closes #557 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/auth_commands.rs | 31 ++++++++++++++++++++++++++++--- src/oauth_config.rs | 2 +- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/auth_commands.rs b/src/auth_commands.rs index 6d0ffb7d..e3f41fae 100644 --- a/src/auth_commands.rs +++ b/src/auth_commands.rs @@ -139,6 +139,9 @@ pub async fn handle_auth_command(args: &[String]) -> Result<(), GwsError> { " --scopes Comma-separated custom scopes\n", " -s, --services Comma-separated service names to limit scope picker\n", " (e.g. -s drive,gmail,sheets)\n", + " --port Use a fixed port for the OAuth redirect server\n", + " (use with Web Application type OAuth clients for orgs\n", + " that block Desktop OAuth via admin_policy_enforced)\n", " setup Configure GCP project + OAuth client (requires gcloud)\n", " --project Use a specific GCP project\n", " --login Run `gws auth login` after successful setup\n", @@ -211,8 +214,9 @@ impl yup_oauth2::authenticator_delegate::InstalledFlowDelegate for CliFlowDelega } async fn handle_login(args: &[String]) -> Result<(), GwsError> { - // Extract -s/--services from args + // Extract -s/--services and --port from args let mut services_filter: Option> = None; + let mut fixed_port: Option = None; let mut filtered_args: Vec = Vec::new(); let mut skip_next = false; for i in 0..args.len() { @@ -220,6 +224,21 @@ async fn handle_login(args: &[String]) -> Result<(), GwsError> { skip_next = false; continue; } + + // Parse --port or --port= + let port_str = if args[i] == "--port" && i + 1 < args.len() { + skip_next = true; + Some(args[i + 1].as_str()) + } else { + args[i].strip_prefix("--port=") + }; + if let Some(value) = port_str { + fixed_port = Some(value.parse::().map_err(|_| { + GwsError::Validation(format!("Invalid port number: {value}")) + })?); + continue; + } + let services_str = if (args[i] == "-s" || args[i] == "--services") && i + 1 < args.len() { skip_next = true; Some(args[i + 1].as_str()) @@ -271,7 +290,10 @@ async fn handle_login(args: &[String]) -> Result<(), GwsError> { client_secret: client_secret.clone(), auth_uri: "https://accounts.google.com/o/oauth2/auth".to_string(), token_uri: "https://oauth2.googleapis.com/token".to_string(), - redirect_uris: vec!["http://localhost".to_string()], + redirect_uris: vec![match fixed_port { + Some(p) => format!("http://localhost:{p}"), + None => "http://localhost".to_string(), + }], ..Default::default() }; @@ -304,7 +326,10 @@ async fn handle_login(args: &[String]) -> Result<(), GwsError> { let auth = yup_oauth2::InstalledFlowAuthenticator::builder( secret, - yup_oauth2::InstalledFlowReturnMethod::HTTPRedirect, + match fixed_port { + Some(p) => yup_oauth2::InstalledFlowReturnMethod::HTTPPortRedirect(p), + None => yup_oauth2::InstalledFlowReturnMethod::HTTPRedirect, + }, ) .with_storage(Box::new(crate::token_storage::EncryptedTokenStorage::new( temp_path.clone(), diff --git a/src/oauth_config.rs b/src/oauth_config.rs index 89f34535..2b5e7553 100644 --- a/src/oauth_config.rs +++ b/src/oauth_config.rs @@ -71,7 +71,7 @@ pub fn save_client_config( auth_uri: "https://accounts.google.com/o/oauth2/auth".to_string(), token_uri: "https://oauth2.googleapis.com/token".to_string(), auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs".to_string(), - redirect_uris: vec!["http://localhost".to_string()], + redirect_uris: vec!["http://localhost:8080".to_string()], }, }; From 0a911039f8d2fed1cd6d8d1a2a464fc77470cfbc Mon Sep 17 00:00:00 2001 From: Victor Maestro Lara Date: Wed, 18 Mar 2026 23:23:22 +0100 Subject: [PATCH 2/3] fix: revert redirect_uris in oauth_config.rs to http://localhost MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverts the change to save_client_config that hardcoded port 8080 in the client_secret.json redirect_uris. This was unnecessary because: - handle_login overwrites redirect_uris based on the --port flag - The default Desktop flow expects http://localhost (no port) - Hardcoding 8080 would confuse users who run setup without --port The --port flag in auth_commands.rs handles the redirect URI dynamically — no config file changes needed. --- src/oauth_config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/oauth_config.rs b/src/oauth_config.rs index 2b5e7553..89f34535 100644 --- a/src/oauth_config.rs +++ b/src/oauth_config.rs @@ -71,7 +71,7 @@ pub fn save_client_config( auth_uri: "https://accounts.google.com/o/oauth2/auth".to_string(), token_uri: "https://oauth2.googleapis.com/token".to_string(), auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs".to_string(), - redirect_uris: vec!["http://localhost:8080".to_string()], + redirect_uris: vec!["http://localhost".to_string()], }, }; From 627c5aecd902469d62e877ce82956bb4a56a3cb5 Mon Sep 17 00:00:00 2001 From: Victor Maestro Date: Wed, 18 Mar 2026 23:32:03 +0100 Subject: [PATCH 3/3] Update src/auth_commands.rs Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/auth_commands.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/auth_commands.rs b/src/auth_commands.rs index e3f41fae..2acc841c 100644 --- a/src/auth_commands.rs +++ b/src/auth_commands.rs @@ -233,9 +233,15 @@ async fn handle_login(args: &[String]) -> Result<(), GwsError> { args[i].strip_prefix("--port=") }; if let Some(value) = port_str { - fixed_port = Some(value.parse::().map_err(|_| { + let port = value.parse::().map_err(|_| { GwsError::Validation(format!("Invalid port number: {value}")) - })?); + })?; + if port == 0 { + return Err(GwsError::Validation( + "Port number must be a non-zero value between 1 and 65535.".to_string(), + )); + } + fixed_port = Some(port); continue; }