# gogcli Authentication for Headless Servers: Manual and Remote OAuth Flows

> Learn gogcli authentication for headless servers using OAuth 2.0 offline flows. Securely exchange authorization codes for refresh tokens for automated CI pipelines or manual setup.

- Repository: [Peter Steinberger/gogcli](https://github.com/steipete/gogcli)
- Tags: how-to-guide
- Published: 2026-02-19

---

**`gogcli` authenticates to Google services on headless servers by using OAuth 2.0 offline flows that exchange authorization codes for refresh tokens without requiring a local browser, supporting both interactive manual pasting and fully automated CI pipelines.**

The `steipete/gogcli` project provides robust authentication mechanisms designed specifically for environments without graphical interfaces. Whether you are running backups on a Raspberry Pi, deploying to a Docker container, or automating Google Drive uploads in a CI/CD pipeline, understanding how `gogcli` handles OAuth for headless servers ensures secure, non-interactive token management.

## Understanding the OAuth Flow for Headless Environments

Standard OAuth 2.0 flows rely on launching a local browser to capture the authorization callback. On headless servers, this fails because no browser is available and the redirect cannot reach a local HTTP server. The `gogcli` implementation in [`internal/googleauth/oauth_flow_manual.go`](https://github.com/steipete/gogcli/blob/main/internal/googleauth/oauth_flow_manual.go) solves this by implementing the **manual OAuth flow**, which decouples the authorization request from the code exchange.

The process relies on obtaining a **refresh token** that is stored in the OS keyring (or an encrypted file fallback). Once acquired, the token allows the CLI to request new access tokens indefinitely without further user interaction.

## Manual Authentication Flow (--manual)

The manual flow is the simplest headless authentication method, requiring only that the user can access a browser on any device to complete the Google consent screen.

### How the Manual Flow Works

According to the source code in [`internal/googleauth/oauth_flow_manual.go`](https://github.com/steipete/gogcli/blob/main/internal/googleauth/oauth_flow_manual.go), the `authorizeManual` function determines whether to run interactively or with pre-supplied credentials. When invoked with the `--manual` flag, the CLI executes `authorizeManualInteractive`, which:

1. Generates the OAuth authorization URL with a unique `state` parameter
2. Prints the URL to `stdout`
3. Blocks waiting for the user to paste the final redirect URL (containing the `code` and `state` parameters)
4. Validates the state via `validateManualState` and exchanges the code for a refresh token

### Step-by-Step Implementation

To authenticate on a headless server using the manual flow:

```bash

# Set your client bucket if not using default

export GOG_CLIENT=headless

# Initiate manual authentication

gog --client "$GOG_CLIENT" auth add you@example.com --manual

```

The terminal will output a URL similar to:

```text
Please open this URL in your browser:
https://accounts.google.com/o/oauth2/auth?client_id=...&redirect_uri=urn:ietf:wg:oauth:2.0:oob&state=xyz123&...

```

After granting consent in your browser, Google will redirect to a localhost URL (or display the code directly for out-of-band flows). Copy the entire redirect URL (e.g., `http://127.0.0.1:8085/?code=4/abc123&state=xyz123`) and paste it back into the terminal. The CLI then calls `authorizeManualWithCode` to parse the URL, extract the code, and complete the exchange.

## Remote Two-Step Flow (--remote) for CI/CD

For fully automated environments where interactive pasting is impossible, `gogcli` provides the **remote flow** using the `--remote` flag with `--step` parameters. This splits the authentication into two discrete phases that can execute on different machines or at different times.

### Step 1: Generating the Authorization URL

Running with `--remote --step 1` executes the first phase, which generates and stores the OAuth state without blocking for user input. As implemented in [`internal/googleauth/oauth_flow_manual.go`](https://github.com/steipete/gogcli/blob/main/internal/googleauth/oauth_flow_manual.go), this path creates a temporary state file via `saveManualState` (defined in [`internal/googleauth/manual_state.go`](https://github.com/steipete/gogcli/blob/main/internal/googleauth/manual_state.go)) with a 10-minute TTL.

```bash

# Generate auth URL for CI pipeline

gog auth add service@example.com --remote --step 1 --client ci > auth_url.txt

```

The output contains the authorization URL that must be opened in a browser. In a CI context, this URL might be exposed as an artifact or sent to a Slack channel for manual approval.

### Step 2: Exchanging the Redirect URL

Once the user (or an automated browser automation tool) completes the consent flow and obtains the redirect URL, the second step exchanges it for a refresh token. This is executed with `--remote --step 2` and accepts the redirect URL via `--auth-url`:

```bash

# Exchange the redirect URL obtained from browser

AUTH_URL="http://127.0.0.1:8085/?code=4/abc123&state=xyz789"

gog auth add service@example.com --remote --step 2 \
    --auth-url "$AUTH_URL" \
    --client ci \
    --timeout 5m

```

The `authorizeManualWithCode` function validates that the `state` parameter in the URL matches the temporary state file created in step 1, preventing CSRF attacks even in automated environments.

## State Management and Security

### Temporary State Storage

The remote flow relies on temporary state files to maintain continuity between steps. As defined in [`internal/googleauth/manual_state.go`](https://github.com/steipete/gogcli/blob/main/internal/googleauth/manual_state.go), the `ManualState` struct stores:

- Client ID and secret
- Requested OAuth scopes
- Redirect URI
- Force consent flag
- Timestamp for TTL enforcement

These files are stored as `oauth-manual-state-<state>.json` in the system temporary directory and expire after **10 minutes** (`manualStateTTL`). This TTL ensures that abandoned authentication attempts do not leave sensitive state data on disk indefinitely.

### Token Storage and Keyring Fallback

Once obtained, refresh tokens are persisted using the [`internal/secrets/store.go`](https://github.com/steipete/gogcli/blob/main/internal/secrets/store.go) implementation, which wraps `github.com/99designs/keyring`. On headless servers where no system keyring (macOS Keychain, GNOME Keyring, etc.) is available, the library automatically falls back to an encrypted file backend.

The encryption key for the file backend can be provided via the `GOG_KEYRING_PASSWORD` environment variable, enabling non-interactive decryption in Docker containers or CI environments:

```bash
export GOG_KEYRING_PASSWORD="$(cat /run/secrets/gog_keyring_pass)"
gog auth add service@example.com --remote --step 2 --auth-url "$AUTH_URL"

```

## Programmatic Usage in Go

For developers integrating `gogcli` authentication into their own Go applications, the `internal/googleauth` package exposes the same manual flow primitives used by the CLI.

```go
package main

import (
    "context"
    "log"
    "time"
    
    "github.com/steipete/gogcli/internal/config"
    "github.com/steipete/gogcli/internal/googleauth"
)

func main() {
    ctx := context.Background()
    
    // Load client credentials from config
    creds, err := config.ReadClientCredentials("headless")
    if err != nil {
        log.Fatalf("failed to read credentials: %v", err)
    }
    
    opts := googleauth.AuthorizeOptions{
        Client:       "headless",
        Scopes:       []string{googleauth.ScopeUserinfoEmail, googleauth.ScopeDrive},
        RequireState: true,
        Manual:       true,
        Timeout:      2 * time.Minute,
    }
    
    // Execute manual authorization
    token, err := googleauth.AuthorizeManual(ctx, opts, creds)
    if err != nil {
        log.Fatalf("authorization failed: %v", err)
    }
    
    log.Printf("Refresh token obtained: %s...", token.RefreshToken[:10])
}

```

The `AuthorizeManual` function wraps the internal `authorizeManual` logic, handling both interactive and non-interactive paths based on the presence of pre-supplied authorization URLs.

## Summary

- **Headless servers** cannot use standard OAuth browser flows, so `gogcli` implements **manual** and **remote** authentication modes in [`internal/googleauth/oauth_flow_manual.go`](https://github.com/steipete/gogcli/blob/main/internal/googleauth/oauth_flow_manual.go).
- The **manual flow** (`--manual`) prints an authorization URL for the user to open on any device, then accepts the redirect URL pasted back into the terminal.
- The **remote flow** (`--remote --step 1|2`) splits authentication into URL generation and code exchange, enabling fully automated CI/CD pipelines.
- **State validation** uses temporary JSON files with a 10-minute TTL stored in [`internal/googleauth/manual_state.go`](https://github.com/steipete/gogcli/blob/main/internal/googleauth/manual_state.go) to prevent CSRF attacks between steps.
- **Token storage** defaults to the OS keyring but falls back to an encrypted file backend using the `GOG_KEYRING_PASSWORD` environment variable for containerized environments.

## Frequently Asked Questions

### How do I authenticate gogcli on a server without a browser?

Use the `--manual` flag with `gog auth add`. The CLI will display a URL that you can open on your local machine's browser. After granting permission, copy the final redirect URL (containing the `code` parameter) and paste it back into the terminal on your headless server. The `authorizeManualInteractive` function in [`internal/googleauth/oauth_flow_manual.go`](https://github.com/steipete/gogcli/blob/main/internal/googleauth/oauth_flow_manual.go) handles this exchange.

### Can I fully automate gogcli authentication in a CI/CD pipeline?

Yes, using the `--remote` flow with `--step 1` and `--step 2`. In step 1, generate the authorization URL and expose it as a build artifact. A human or automated browser tool can complete the consent flow and provide the resulting redirect URL. In step 2, pass that URL via `--auth-url` to complete the exchange non-interactively. The temporary state files in [`internal/googleauth/manual_state.go`](https://github.com/steipete/gogcli/blob/main/internal/googleauth/manual_state.go) maintain security between these steps.

### Where are refresh tokens stored on headless systems without a keyring?

When no system keyring (like macOS Keychain or GNOME Keyring) is available, `gogcli` falls back to an encrypted file backend located at `$(os.UserConfigDir())/gogcli/keyring/`. The encryption password can be supplied via the `GOG_KEYRING_PASSWORD` environment variable, which is essential for Docker containers and automated server deployments. This logic is implemented in [`internal/secrets/store.go`](https://github.com/steipete/gogcli/blob/main/internal/secrets/store.go).

### What is the purpose of the state parameter in the manual flow?

The `state` parameter prevents Cross-Site Request Forgery (CSRF) attacks by ensuring that the authorization code received in step 2 matches the request initiated in step 1. In [`internal/googleauth/oauth_flow_manual.go`](https://github.com/steipete/gogcli/blob/main/internal/googleauth/oauth_flow_manual.go), the `validateManualState` function checks that the state exists in a temporary file (created by `saveManualState` in [`internal/googleauth/manual_state.go`](https://github.com/steipete/gogcli/blob/main/internal/googleauth/manual_state.go)) and that parameters like client ID and scopes match. These state files expire after 10 minutes to prevent replay attacks.