Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/oauth-redirect-override.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@googleworkspace/cli": minor
---

Allow customizing the `gws auth login` OAuth callback so it works when the CLI runs on a remote host whose `localhost` the browser connot reach directly: `GOOGLE_WORKSPACE_CLI_OAUTH_REDIRECT_URI` overrides the redirect URI sent to Google, `GOOGLE_WORKSPACE_CLI_OAUTH_STATE` sets the `state` value passed to the OAuth server, and `GOOGLE_WORKSPACE_CLI_OAUTH_PORT` pins the local callback port. When a custom `state` is set, the value returned on the callback is verified against it to reject forged (CSRF) responses.
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@
# GOOGLE_WORKSPACE_CLI_CLIENT_ID=
# GOOGLE_WORKSPACE_CLI_CLIENT_SECRET=

# ── OAuth callback customization (for remote-host logins) ─────────
# Override the redirect URI sent to Google, set a state value passed to the OAuth server (verified on the callback), and/or pin the local callback port.
# GOOGLE_WORKSPACE_CLI_OAUTH_REDIRECT_URI=
# GOOGLE_WORKSPACE_CLI_OAUTH_STATE=
# GOOGLE_WORKSPACE_CLI_OAUTH_PORT=

# ── Configuration ─────────────────────────────────────────────────
# Override the config directory (default: ~/.config/gws)
# GOOGLE_WORKSPACE_CLI_CONFIG_DIR=
Expand Down
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,9 @@ See [`src/helpers/README.md`](crates/google-workspace-cli/src/helpers/README.md)
|---|---|
| `GOOGLE_WORKSPACE_CLI_CLIENT_ID` | OAuth client ID (for `gws auth login` when no `client_secret.json` is saved) |
| `GOOGLE_WORKSPACE_CLI_CLIENT_SECRET` | OAuth client secret (paired with `CLIENT_ID` above) |
| `GOOGLE_WORKSPACE_CLI_OAUTH_REDIRECT_URI` | Override the OAuth redirect URI (for remote-host logins) |
| `GOOGLE_WORKSPACE_CLI_OAUTH_STATE` | OAuth `state` value, passed to the OAuth server and verified on the callback |
| `GOOGLE_WORKSPACE_CLI_OAUTH_PORT` | Pin the local OAuth callback port (default: random) |

### Sanitization (Model Armor)

Expand Down
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ The CLI supports multiple auth workflows so it works on your laptop, in CI, and
| A GCP project but no `gcloud` | [Manual OAuth setup](#manual-oauth-setup-google-cloud-console) |
| An existing OAuth access token | [`GOOGLE_WORKSPACE_CLI_TOKEN`](#pre-obtained-access-token) |
| Existing Credentials | [`GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE`](#service-account-server-to-server) |
| A remote host unreachable via `localhost` from my browser | [Remote host](#remote-host-browser-on-another-machine) |

### Interactive (local desktop)

Expand Down Expand Up @@ -187,6 +188,22 @@ If scope checkboxes appear, select required scopes (or **Select all**) before co
gws drive files list # just works
```

### Remote host (browser on another machine)

When the CLI runs where the browser cannot reach it via `localhost` (e.g. a cloud development environment), point the OAuth redirect at an address the browser can reach, which delivers the authorization code back to the CLI:

```bash
# Where Google sends the browser (must be an authorized redirect URI on the OAuth client)
export GOOGLE_WORKSPACE_CLI_OAUTH_REDIRECT_URI="https://example.com/callback"k
# Optional value passed to the OAuth server and verified when returned on the callback
export GOOGLE_WORKSPACE_CLI_OAUTH_STATE="$STATE"
# Optional port for the CLI's callback server
export GOOGLE_WORKSPACE_CLI_OAUTH_PORT=42067
gws auth login
```

This flow requires a web application OAuth client that allows redirects to URIs other than `http://localhost`. Provide the client's details via `GOOGLE_WORKSPACE_CLI_CLIENT_ID` and `GOOGLE_WORKSPACE_CLI_CLIENT_SECRET` environment variables.

### Service Account (server-to-server)

Point to your key file; no login needed.
Expand Down Expand Up @@ -381,6 +398,9 @@ All variables are optional. See [`.env.example`](.env.example) for a copy-paste
| `GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE` | Path to OAuth credentials JSON (user or service account) |
| `GOOGLE_WORKSPACE_CLI_CLIENT_ID` | OAuth client ID (alternative to `client_secret.json`) |
| `GOOGLE_WORKSPACE_CLI_CLIENT_SECRET` | OAuth client secret (paired with `CLIENT_ID`) |
| `GOOGLE_WORKSPACE_CLI_OAUTH_REDIRECT_URI` | Override the OAuth redirect URI (for remote-host logins) |
| `GOOGLE_WORKSPACE_CLI_OAUTH_STATE` | OAuth `state` value, passed to the OAuth server and verified on the callback |
| `GOOGLE_WORKSPACE_CLI_OAUTH_PORT` | Pin the local OAuth callback port (default: random) |
| `GOOGLE_WORKSPACE_CLI_CONFIG_DIR` | Override config directory (default: `~/.config/gws`) |
| `GOOGLE_WORKSPACE_CLI_SANITIZE_TEMPLATE` | Default Model Armor template |
| `GOOGLE_WORKSPACE_CLI_SANITIZE_MODE` | `warn` (default) or `block` |
Expand Down Expand Up @@ -457,6 +477,8 @@ gws auth login --scopes drive,gmail,calendar

The OAuth client was not created as a **Desktop app** type. In the [Credentials](https://console.cloud.google.com/apis/credentials) page, delete the existing client, create a new one with type **Desktop app**, and download the new JSON.

Using the [remote-host flow](#remote-host-browser-on-another-machine) instead? There it means the URL in `GOOGLE_WORKSPACE_CLI_OAUTH_REDIRECT_URI` is not registered on your **Web application** client - add it under **Authorized redirect URIs**.

### API not enabled — `accessNotConfigured`

If a required Google API is not enabled for your GCP project, you will see a
Expand Down
39 changes: 1 addition & 38 deletions crates/google-workspace-cli/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -436,47 +436,10 @@ async fn load_credentials_inner(
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::EnvVarGuard;
use std::io::Write;
use tempfile::NamedTempFile;

/// RAII guard that saves the current value of an environment variable and
/// restores it when dropped. This ensures cleanup even if a test panics.
struct EnvVarGuard {
name: String,
original: Option<std::ffi::OsString>,
}

impl EnvVarGuard {
/// Save the current value of `name`, then set it to `value`.
fn set(name: &str, value: impl AsRef<std::ffi::OsStr>) -> Self {
let original = std::env::var_os(name);
std::env::set_var(name, value);
Self {
name: name.to_string(),
original,
}
}

/// Save the current value of `name`, then remove it.
fn remove(name: &str) -> Self {
let original = std::env::var_os(name);
std::env::remove_var(name);
Self {
name: name.to_string(),
original,
}
}
}

impl Drop for EnvVarGuard {
fn drop(&mut self) {
match &self.original {
Some(v) => std::env::set_var(&self.name, v),
None => std::env::remove_var(&self.name),
}
}
}

fn clear_proxy_env() -> Vec<EnvVarGuard> {
PROXY_ENV_VARS
.iter()
Expand Down
Loading
Loading