gogcli Authentication for Headless Servers: Manual and Remote OAuth Flows

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 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, 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:


# Set your client bucket if not using default

export GOG_CLIENT=headless

# Initiate manual authentication

gog --client "$GOG_CLIENT" auth add [email protected] --manual

The terminal will output a URL similar to:

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, this path creates a temporary state file via saveManualState (defined in internal/googleauth/manual_state.go) with a 10-minute TTL.


# Generate auth URL for CI pipeline

gog auth add [email protected] --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:


# Exchange the redirect URL obtained from browser

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

gog auth add [email protected] --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, 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 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:

export GOG_KEYRING_PASSWORD="$(cat /run/secrets/gog_keyring_pass)"
gog auth add [email protected] --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.

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.
  • 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 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 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 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.

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, the validateManualState function checks that the state exists in a temporary file (created by saveManualState in 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →