# Google OAUTH

> Self-hosted Google OAuth authentication for any Google API (Gmail, Calendar, Drive, Ads). Runs local callback server, handles token exchange and refresh. No external dependencies like Nango.

- Skill: `shakudo-io/google-oauth` (Agent Skill, multi-file: 7 files)
- Install (CLI): `npx skillmds@latest add shakudo-io/google-oauth`
- Raw SKILL.md: https://api.skillmd.com/api/skills/shakudo-io/google-oauth/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- License: MIT
- Author: Shakudo-io (https://skillmd.com/u/shakudo-io)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/shakudo-io/google-oauth

---


# Google OAuth Agent Skill

Self-hosted Google OAuth authentication for any Google API (Gmail, Calendar, Drive, Ads, etc.).

## Overview

This skill provides a complete OAuth 2.0 flow for Google APIs without external dependencies like Nango. It:

- Runs a local callback server on port 3000
- Generates dynamic callback URLs for Shakudo deployments
- Handles token exchange and storage
- Supports automatic token refresh
- Works with any Google API scope

## Quick Start

```bash
# 1. Set up credentials (one-time)
export GOOGLE_CLIENT_ID="your-client-id.apps.googleusercontent.com"
export GOOGLE_CLIENT_SECRET="your-client-secret"

# 2. Run OAuth flow for Gmail
cd scripts/
python auth_flow.py --scopes gmail_full

# 3. Tokens saved to .env, ready to use
```

---

## Prerequisites

### Python Dependencies

```bash
pip install httpx python-dotenv
```

### Google Cloud OAuth App (One-Time Setup)

1. Go to [Google Cloud Console](https://console.cloud.google.com/apis/credentials)
2. Create a project (or select existing)
3. Click **Create Credentials** → **OAuth Client ID**
4. Select **Web application**
5. Add redirect URI (see [Callback URL](#callback-url) below)
6. Copy the **Client ID** and **Client Secret**

### Enable Required APIs

For the Google APIs you want to use:
- [Gmail API](https://console.cloud.google.com/apis/library/gmail.googleapis.com)
- [Google Calendar API](https://console.cloud.google.com/apis/library/calendar-json.googleapis.com)
- [Google Drive API](https://console.cloud.google.com/apis/library/drive.googleapis.com)
- [Google Ads API](https://console.cloud.google.com/apis/library/googleads.googleapis.com)

---

## Callback URL

The callback URL is dynamically generated based on the `MY_JOB_ID` environment variable (set automatically in Shakudo deployments).

**Pattern**: `https://nextjs-{first-6-chars-of-job-id}.dev.hyperplane.dev/oauth/callback`

**Example**: If `MY_JOB_ID=abc123xyz`, callback URL is:
```
https://nextjs-abc123.dev.hyperplane.dev/oauth/callback
```

**For local development** (no `MY_JOB_ID`):
```
https://nextjs-000000.dev.hyperplane.dev/oauth/callback
```

**Add this URL to your Google OAuth app's authorized redirect URIs.**

---

## Supported Scopes

| Preset | Scope URL | Description |
|--------|-----------|-------------|
| `gmail_full` | `https://mail.google.com/` | Full Gmail access (read, send, delete) |
| `gmail_readonly` | `https://www.googleapis.com/auth/gmail.readonly` | Read-only Gmail access |
| `gmail_send` | `https://www.googleapis.com/auth/gmail.send` | Send emails only |
| `gmail_compose` | `https://www.googleapis.com/auth/gmail.compose` | Compose and send |
| `gmail_labels` | `https://www.googleapis.com/auth/gmail.labels` | Manage labels |
| `calendar` | `https://www.googleapis.com/auth/calendar` | Full calendar access |
| `calendar_readonly` | `https://www.googleapis.com/auth/calendar.readonly` | Read-only calendar |
| `drive` | `https://www.googleapis.com/auth/drive` | Full Drive access |
| `drive_readonly` | `https://www.googleapis.com/auth/drive.readonly` | Read-only Drive |
| `ads` | `https://www.googleapis.com/auth/adwords` | Google Ads API |
| `sheets` | `https://www.googleapis.com/auth/spreadsheets` | Google Sheets |
| `docs` | `https://www.googleapis.com/auth/documents` | Google Docs |

**Custom scopes**: You can also use full scope URLs directly:
```bash
python auth_flow.py --scopes "https://www.googleapis.com/auth/userinfo.email"
```

---

## Environment Variables

| Variable | Required | Description |
|----------|----------|-------------|
| `GOOGLE_CLIENT_ID` | Yes* | OAuth client ID from Google Cloud Console |
| `GOOGLE_CLIENT_SECRET` | Yes* | OAuth client secret |
| `GOOGLE_ADS_CLIENT_ID` | Fallback | Alternative credential prefix (marketing TUI compatibility) |
| `GOOGLE_ADS_CLIENT_SECRET` | Fallback | Alternative credential prefix (marketing TUI compatibility) |
| `GOOGLE_ACCESS_TOKEN` | Auto | Access token (populated after OAuth flow) |
| `GOOGLE_REFRESH_TOKEN` | Auto | Refresh token (for offline access) |
| `GOOGLE_TOKEN_EXPIRY` | Auto | ISO 8601 datetime when access token expires |
| `GOOGLE_SCOPES` | Auto | Space-separated scopes that were authorized |
| `GOOGLE_OAUTH_ENV_PATH` | No | Custom path to .env file (default: `./.env`) |
| `MY_JOB_ID` | No | Shakudo job ID (auto-set in deployments) |

*The scripts check `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` first, then fall back to `GOOGLE_ADS_CLIENT_ID` / `GOOGLE_ADS_CLIENT_SECRET` for compatibility with the marketing TUI.

---

## User Experience Flow

### First-Time Setup

```
User: "Connect my Gmail account"

Agent: "I don't see Gmail credentials configured. Let me help you set up OAuth.
        Do you have a Google Cloud OAuth Client ID and Secret?"

User: "No"

Agent: "No problem. Here's how to create one:
        1. Go to https://console.cloud.google.com/apis/credentials
        2. Create OAuth 2.0 Client ID (Web application)
        3. Add redirect URI: https://nextjs-XXXXXX.dev.hyperplane.dev/oauth/callback
        4. Copy the Client ID and Client Secret
        
        Let me know when you have them."

User: "Got it. Client ID is XXX, Secret is YYY"

Agent: [Saves credentials to .env]
       "Starting OAuth server...
        
        Please open this URL in your browser to authorize Gmail access:
        https://accounts.google.com/o/oauth2/v2/auth?client_id=...
        
        Waiting for authorization... (5 min timeout)"

[User authorizes in browser, gets redirected to callback]

Agent: "Authorization received! Exchanging for tokens...
        
        Gmail connected successfully!
        • Access token: saved (expires in 1 hour)
        • Refresh token: saved (auto-renews)
        • Scopes: gmail.readonly, gmail.send
        
        You can now ask me to read or send emails."
```

### Subsequent Usage

```
User: "Check my unread emails"

Agent: [Loads tokens from .env]
       [Auto-refreshes if access token expired]
       [Calls Gmail API]
       "You have 3 unread emails:
        1. From: boss@company.com - Subject: Q1 Report
        ..."
```

### Token Expired / Revoked

```
User: "Send an email"

Agent: [Detects refresh token no longer works]
       "Your Gmail authorization has expired. Let me reconnect..."
       [Restarts OAuth flow]
```

---

## Script Reference

### oauth_server.py

Callback server for receiving OAuth redirects.

```bash
# Start server and wait for callback
python oauth_server.py --wait --timeout 300

# Just start server (for debugging)
python oauth_server.py
```

**Python API:**
```python
from oauth_server import OAuthServer, GOOGLE_SCOPE_PRESETS

server = OAuthServer()
if server.start():
    # Generate auth URL
    url, state = server.generate_auth_url(
        client_id="your-client-id",
        scopes=["gmail_full", "calendar"],
        access_type="offline",
        prompt="consent"
    )
    print(f"Open: {url}")
    
    # Wait for callback
    result = server.wait_for_callback(timeout=300)
    if result.success:
        print(f"Code: {result.code}")
    else:
        print(f"Error: {result.error}")
    
    server.stop()
```

### auth_flow.py

Complete OAuth authorization flow.

```bash
# Interactive flow (recommended)
python auth_flow.py --scopes gmail_full

# Multiple scopes
python auth_flow.py --scopes gmail_full calendar drive

# Just generate URL (for manual flow)
python auth_flow.py --scopes gmail_full --url-only

# Exchange code manually
python auth_flow.py --exchange-code "4/0AY0e-g..."

# List available scope presets
python auth_flow.py --list-scopes
```

### token_refresh.py

Token status and refresh.

```bash
# Check token status
python token_refresh.py --status

# Force refresh access token
python token_refresh.py --refresh

# Auto-refresh only if needed (silent, for scripts)
python token_refresh.py --auto

# Get valid access token (refreshes if needed)
TOKEN=$(python token_refresh.py --get-token)
```

**Python API:**
```python
from token_refresh import get_valid_token

# Get valid access token (auto-refreshes if needed)
token = get_valid_token()
if token:
    # Use token with Google APIs
    headers = {"Authorization": f"Bearer {token}"}
```

---

## Integration Examples

### With google_workspace_mcp

```python
from token_refresh import get_valid_token

# Get token for google_workspace_mcp
access_token = get_valid_token()
if not access_token:
    print("Need to run OAuth flow first")
    exit(1)

# Configure google_workspace_mcp to use this token
# (implementation depends on how you've integrated it)
```

### With Gmail API Directly

```python
import httpx
from token_refresh import get_valid_token

token = get_valid_token()
if not token:
    raise RuntimeError("No valid token")

async with httpx.AsyncClient() as client:
    response = await client.get(
        "https://gmail.googleapis.com/gmail/v1/users/me/messages",
        headers={"Authorization": f"Bearer {token}"},
        params={"maxResults": 10}
    )
    messages = response.json()
```

### With Google Calendar API

```python
import httpx
from token_refresh import get_valid_token

token = get_valid_token()

async with httpx.AsyncClient() as client:
    response = await client.get(
        "https://www.googleapis.com/calendar/v3/calendars/primary/events",
        headers={"Authorization": f"Bearer {token}"},
        params={"maxResults": 10}
    )
    events = response.json()
```

---

## Error Handling

| Error | Cause | Solution |
|-------|-------|----------|
| "Port 3000 is already in use" | Another service on port 3000 | Stop the other service |
| "Authorization timed out" | User didn't complete auth in time | Run auth flow again |
| "User declined permission" | User clicked "Deny" | Run auth flow again, accept permissions |
| "Invalid client credentials" | Wrong client ID/secret | Check credentials in Google Cloud Console |
| "Token refresh failed: invalid_grant" | Refresh token revoked | Run full auth flow again |
| "No refresh token available" | Didn't use `access_type=offline` | Re-run auth with `--scopes` (uses offline by default) |

---

## Security Notes

1. **Never commit credentials** - Use `.env` files and add to `.gitignore`
2. **Refresh tokens persist** - Treat refresh tokens like passwords
3. **Scope minimization** - Only request scopes you need
4. **Token storage** - Tokens are stored in plaintext `.env` files
5. **HTTPS callbacks** - Google requires HTTPS for production callback URLs

---

## Troubleshooting

### "redirect_uri_mismatch" Error

The callback URL in your Google OAuth app doesn't match. Check:
1. Your `MY_JOB_ID` environment variable
2. The redirect URI in Google Cloud Console

### Tokens Not Persisting

Check that:
1. `.env` file is writable
2. `GOOGLE_OAUTH_ENV_PATH` is set correctly (if using custom path)
3. No file permission issues

### Auto-Refresh Not Working

1. Check you have a refresh token: `python token_refresh.py --status`
2. Verify client credentials are correct
3. Ensure the refresh token hasn't been revoked

---

## Files

```
google-oauth/
├── SKILL.md              # This documentation
├── .env.example          # Credential template
└── scripts/
    ├── oauth_server.py   # Callback server (port 3000)
    ├── auth_flow.py      # Authorization flow
    └── token_refresh.py  # Token refresh utility
```

