# Google OAuth on a Headless VPS with an SSH Local Tunnel

> Complete browser-based Google OAuth on a VPS without exposing the callback publicly, using loopback redirects, SSH forwarding and safe token handling.

- **Published**: 2026-08-14
- **Category**: DevOps
- **URL**: https://agentpedia.codes/blog/google-oauth-headless-vps-ssh-local-forwarding

---

> **Important callout**

**Bottom line:** A headless VPS does not need a browser or public OAuth callback. Run the OAuth listener on the VPS loopback interface, forward a laptop port through SSH, open the Google consent URL in your own browser, and keep client JSON, authorization codes and tokens out of chat and agent context.

Browser-based OAuth is designed around a user who can sign in and approve access. A VPS is designed around a process with no desktop. The bridge is an SSH local-forwarding tunnel: the browser remains on the laptop, while the callback listener remains private on the VPS. For a broader remote-agent tunnel threat model, see the [secure MCP tunnels guide](/blog/openai-secure-mcp-tunnels-guide).

This guide focuses on Google's authorization-code flow, loopback callback design and OpenSSH controls. For broader secure tunnel patterns, see the [OpenAI secure MCP tunnels guide](/blog/openai-secure-mcp-tunnels-guide).

## The practical verdict

Use this topology when the application runs on a remote server but Google login must happen in a user-controlled browser. It avoids putting a public callback endpoint on the VPS and avoids giving a bot your Google password.

The critical invariant is exactness: the redirect URI registered in Google Cloud, the application listener and the laptop-side tunnel must agree on host, port and path.

## Understand the topology

```text
Chrome on laptop
  http://127.0.0.1:8080/oauth2callback
          |
          | SSH local forward: laptop:8080 -> VPS:127.0.0.1:8080
          v
OAuth app on VPS
  listens only on 127.0.0.1:8080
          |
          v
Google authorization and token endpoints over HTTPS
```

The browser opens Google's HTTPS consent page. After approval, Google redirects to the registered loopback callback. The browser connects to the laptop's local port, and SSH carries the request to the VPS listener.

The callback is not copied into chat. The authorization code is consumed by the application and exchanged directly with Google.

## Configure Google OAuth

Google's [web-server OAuth documentation](https://developers.google.com/identity/protocols/oauth2/web-server) requires the redirect URI to match exactly. Register a URI such as:

```text
http://127.0.0.1:8080/oauth2callback
```

Use `localhost` everywhere instead if that is what the application and client use. Do not mix `localhost` and `127.0.0.1` casually.

Request only the scopes required by the feature. Google recommends incremental authorization and the narrowest scopes possible. Analytics and Search Console read-only access should not automatically become Gmail or Drive access.

The authorization request should use an unpredictable `state` value, store it server-side and compare it before exchanging the callback code. For refresh-token use when the user is absent, Google documents `access_type=offline`. A refresh token may not be returned on every later authorization.

Sensitive or restricted scopes can trigger Google verification requirements. Personal, internal, development and testing exceptions vary, so check Google's current [OAuth policies](https://developers.google.com/identity/protocols/oauth2/policies) before production launch.

## Create the SSH tunnel

On the laptop, run:

```bash
ssh -N \
  -o ExitOnForwardFailure=yes \
  -o ServerAliveInterval=30 \
  -o ServerAliveCountMax=3 \
  -L 8080:127.0.0.1:8080 \
  user@YOUR_VPS_HOST
```

`-N` creates a forwarding-only session. `ExitOnForwardFailure=yes` prevents a misleading tunnel process from remaining after the local bind fails. Explicitly binding the laptop side to loopback avoids exposing the callback to the laptop's network interfaces.

OpenSSH's `-L` syntax forwards a local port to a host and port reachable from the remote side. The destination `127.0.0.1:8080` therefore refers to the VPS loopback interface, not the laptop.

Do not add `-g` or bind the local side to `0.0.0.0` for an OAuth callback. The callback should be reachable only by the browser on the laptop.

## Complete the callback

In a second VPS session, start the application's login command. A headless-friendly CLI should print a short-lived Google authorization URL instead of requiring a local GUI browser:

```bash
# example application command
aitraffic auth google login --profile work
```

If the application currently attempts to open `xdg-open`, that is harmless on a headless VPS as long as it also prints the URL. Copy the URL from the VPS terminal into Chrome on the laptop and complete consent yourself.

The application should then:

1. receive the loopback callback through SSH;
2. validate `state`;
3. reject an OAuth error or missing code;
4. exchange the code over HTTPS;
5. verify the granted scopes and account identity;
6. store the refresh token securely;
7. show success without printing the token.

If the callback page never loads, check the local listener, tunnel process, exact port and registered redirect URI. Do not paste the callback URL into a chat system to "finish" the login.

## Protect tokens and scopes

Google's OAuth policy requires secure token handling. Keep these private:

- OAuth client JSON and client secret;
- authorization URL if it contains state and client details;
- authorization code;
- callback URL;
- access token and refresh token;
- account identifiers and scope grants.

Store tokens encrypted at rest or in the operating system credential store. Restrict the token file or vault to the service account. Never commit it, include it in a bug report or pass it through an agent prompt.

Handle `invalid_grant` as a possible revocation or expired refresh token and require a new browser authorization. Revoke and delete tokens when the integration is removed. Do not silently broaden scopes during reauthorization.

## Handle failures

| Symptom | Likely cause | Safe response |
| --- | --- | --- |
| `redirect_uri_mismatch` | Host, port, path or trailing slash differs | Compare Google Cloud, app and tunnel character by character |
| Browser connection refused | Tunnel or VPS listener is absent | Check both processes; do not expose a public port as a shortcut |
| OAuth state failure | Wrong/stale callback or interrupted flow | Abort and restart; never exchange a code after state mismatch |
| `invalid_grant` | Revoked, expired or reused code/token | Reauthorize in the browser and rotate affected credentials |
| Consent denied | User or policy rejected a scope | Request less scope or resolve verification; do not retry blindly |
| Tunnel drops | SSH/network interruption | Reconnect and restart the short-lived login flow |
| Port already used | Existing listener or tunnel | Stop the intended disposable process or choose a reviewed port |

SSH forwarding can also be restricted by `AllowTcpForwarding`, `PermitOpen` and `DisableForwarding` in `sshd_config`. If a tunnel fails on a locked-down VPS, review the server policy rather than weakening it globally.

## Security checklist

- [ ] OAuth client JSON is stored only on the VPS, not in chat.
- [ ] Redirect URI matches exactly.
- [ ] Listener binds only to VPS loopback.
- [ ] Laptop forward binds only to laptop loopback.
- [ ] SSH uses a dedicated key and restricted account where possible.
- [ ] `ExitOnForwardFailure` and keepalives are enabled.
- [ ] OAuth `state` is generated, stored and validated.
- [ ] Scopes are minimal and read-only where possible.
- [ ] Tokens are encrypted or stored in a credential vault.
- [ ] Callback URLs and authorization codes are never pasted into an agent.
- [ ] Revocation and reauthorization are tested.

This flow gives the user control of Google login while keeping the server-side callback private. It is simple, auditable and safer than exposing a temporary public OAuth endpoint.

## FAQ

The concise answers are in metadata; the topology, callback and token rules above are the operational guide.

## Official sources

- [Google web-server OAuth](https://developers.google.com/identity/protocols/oauth2/web-server)
- [Google OAuth policies](https://developers.google.com/identity/protocols/oauth2/policies)
- [Google OAuth scopes](https://developers.google.com/identity/protocols/oauth2/scopes)
- [Sensitive-scope verification](https://developers.google.com/identity/protocols/oauth2/production-readiness/sensitive-scope-verification)
- [OpenSSH ssh manual](https://man.openbsd.org/ssh)
- [OpenSSH server configuration](https://man.openbsd.org/sshd_config)

[Browse related Agentpedia articles](https://agentpedia.codes/blog)


---

- [All articles](https://agentpedia.codes/blog)