OAuth Flow Implementation for AI Agents: A Complete Guide

# oauth# ai# automation# security
OAuth Flow Implementation for AI Agents: A Complete GuideClamper ai

Your AI agent needs to access Google Sheets, send emails through Gmail, or post to LinkedIn. But...

Your AI agent needs to access Google Sheets, send emails through Gmail, or post to LinkedIn. But OAuth flows weren't designed for autonomous agents running on your server. They expect a human with a browser to click "Authorize."

This guide shows you how to implement OAuth 2.0 properly for AI agents, handle token refresh automatically, and build a system that works reliably in production.

Why OAuth Is Hard for AI Agents

OAuth was designed for web apps where a human user is present to authorize access. The typical flow:

  1. User clicks "Connect Google" in your web app
  2. Browser redirects to Google's authorization page
  3. User approves access and grants permissions
  4. Google redirects back with an authorization code
  5. Your app exchanges the code for access and refresh tokens
  6. Your app stores the tokens and uses them to make API calls

But AI agents don't have a browser. They run headless on a server, often in a terminal or as a background process.

The Three Problems

Problem 1: No Browser Interface
The agent can't open Google's OAuth page and click "Authorize." You need an out-of-band flow or a separate web server to handle the redirect.

Problem 2: Token Expiry
Access tokens expire (usually after 1 hour). Your agent needs to detect expiry and automatically refresh tokens without human intervention.

Problem 3: Secure Storage
Tokens are sensitive credentials. Storing them in plain text config files is a security disaster waiting to happen.

OAuth 2.0 Flow Types

Authorization Code Flow (Recommended)

This is the most secure and widely supported flow. You need a way to capture the authorization code after the user approves access.

# Step 1: Generate authorization URL
AUTH_URL="https://accounts.google.com/o/oauth2/v2/auth?
  client_id=YOUR_CLIENT_ID
  &redirect_uri=http://localhost:3000/oauth/callback
  &response_type=code
  &scope=https://www.googleapis.com/auth/spreadsheets
  &access_type=offline
  &prompt=consent"

# Step 2: User opens URL in browser, approves access
# Google redirects to: http://localhost:3000/oauth/callback?code=AUTH_CODE

# Step 3: Exchange code for tokens
curl -X POST https://oauth2.googleapis.com/token \\
  -d client_id=YOUR_CLIENT_ID \\
  -d client_secret=YOUR_CLIENT_SECRET \\
  -d code=AUTH_CODE \\
  -d redirect_uri=http://localhost:3000/oauth/callback \\
  -d grant_type=authorization_code
Enter fullscreen mode Exit fullscreen mode

Key parameters:

  • access_type=offline — Critical! This ensures you get a refresh token
  • prompt=consent — Forces the consent screen to show (needed to get refresh token)
  • redirect_uri — Must match exactly what you registered in Google Cloud Console

Device Code Flow (Alternative)

Perfect for CLI tools and headless agents. The user approves on a separate device.

# Step 1: Request device code
curl -X POST https://oauth2.googleapis.com/device/code \\
  -d client_id=YOUR_CLIENT_ID \\
  -d scope=https://www.googleapis.com/auth/spreadsheets

# Step 2: Show user the code
echo "Go to https://www.google.com/device"
echo "Enter code: GQVQ-JKEC"

# Step 3: Poll for authorization
while true; do
  curl -X POST https://oauth2.googleapis.com/token \\
    -d client_id=YOUR_CLIENT_ID \\
    -d client_secret=YOUR_CLIENT_SECRET \\
    -d device_code=AH-1Ng2... \\
    -d grant_type=urn:ietf:params:oauth:grant-type:device_code
  sleep 5
done
Enter fullscreen mode Exit fullscreen mode

This works great for OpenClaw agents running in a terminal.

Implementing OAuth for OpenClaw Agents

Step 1: Register Your App

  1. Go to console.cloud.google.com
  2. Create a new project (or select existing)
  3. Enable Google Sheets API
  4. Go to APIs & Services → Credentials
  5. Create OAuth 2.0 Client ID → Choose "Desktop app" or "Web application"
  6. Add redirect URI: http://localhost:3000/oauth/callback
  7. Save your Client ID and Client Secret

Step 2: Build the Authorization Flow

// oauth-server.js
import express from 'express';
import { google } from 'googleapis';
import fs from 'fs/promises';

const app = express();
const PORT = 3000;

const oauth2Client = new google.auth.OAuth2(
  process.env.GOOGLE_CLIENT_ID,
  process.env.GOOGLE_CLIENT_SECRET,
  'http://localhost:3000/oauth/callback'
);

// Step 1: Start auth flow
app.get('/auth', (req, res) => {
  const authUrl = oauth2Client.generateAuthUrl({
    access_type: 'offline',
    scope: ['https://www.googleapis.com/auth/spreadsheets'],
    prompt: 'consent'
  });
  res.redirect(authUrl);
});

// Step 2: Handle callback
app.get('/oauth/callback', async (req, res) => {
  const { code } = req.query;

  try {
    const { tokens } = await oauth2Client.getToken(code);
    await fs.writeFile('.oauth-tokens.json', JSON.stringify(tokens, null, 2));
    res.send('Authorization successful!');
    process.exit(0);
  } catch (error) {
    res.status(500).send('Authorization failed: ' + error.message);
  }
});

app.listen(PORT);
Enter fullscreen mode Exit fullscreen mode

Step 3: Token Refresh Logic

class GoogleSheetsClient {
  constructor() {
    this.oauth2Client = new google.auth.OAuth2(
      process.env.GOOGLE_CLIENT_ID,
      process.env.GOOGLE_CLIENT_SECRET,
      'http://localhost:3000/oauth/callback'
    );
  }

  async loadTokens() {
    const tokens = JSON.parse(await fs.readFile('.oauth-tokens.json', 'utf-8'));
    this.oauth2Client.setCredentials(tokens);

    // Set up auto-refresh
    this.oauth2Client.on('tokens', async (newTokens) => {
      const updated = { ...tokens, ...newTokens };
      await fs.writeFile('.oauth-tokens.json', JSON.stringify(updated, null, 2));
    });

    this.sheets = google.sheets({ version: 'v4', auth: this.oauth2Client });
  }

  async readSheet(spreadsheetId, range) {
    if (!this.sheets) await this.loadTokens();

    try {
      const response = await this.sheets.spreadsheets.values.get({
        spreadsheetId,
        range
      });
      return response.data.values;
    } catch (error) {
      if (error.code === 401) {
        await this.oauth2Client.refreshAccessToken();
        return this.readSheet(spreadsheetId, range);
      }
      throw error;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Security Best Practices

1. Never Commit Tokens to Git

.oauth-tokens.json
.env
*-credentials.json
Enter fullscreen mode Exit fullscreen mode

2. Encrypt Tokens at Rest

import crypto from 'crypto';

function encryptTokens(tokens, key) {
  const iv = crypto.randomBytes(16);
  const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(key, 'hex'), iv);
  let encrypted = cipher.update(JSON.stringify(tokens), 'utf8', 'hex');
  encrypted += cipher.final('hex');
  return iv.toString('hex') + ':' + encrypted;
}
Enter fullscreen mode Exit fullscreen mode

3. Use Minimal Scopes

// ❌ Don't ask for everything
scope: 'https://www.googleapis.com/auth/drive'

// ✅ Request minimal access
scope: 'https://www.googleapis.com/auth/spreadsheets.readonly'
Enter fullscreen mode Exit fullscreen mode

How Clamper's Managed OAuth Works

Building OAuth flows from scratch is tedious. Clamper provides managed OAuth connections that handle authorization, token refresh, and secure storage automatically.

// With Clamper — no OAuth code needed
import { ClamperClient } from '@clamper/sdk';

const client = new ClamperClient(process.env.CLAMPER_API_KEY);

// Read spreadsheet (Clamper handles auth automatically)
const data = await client.sheets.read({
  spreadsheetId: '1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms',
  range: 'Sheet1!A1:D10'
});
Enter fullscreen mode Exit fullscreen mode

Behind the scenes, Clamper:

  • Maintains OAuth connections per user account
  • Encrypts tokens at rest with AES-256
  • Refreshes tokens proactively before expiry
  • Handles errors and re-authorization flows
  • Provides audit logs of all API calls

Common Pitfalls

Pitfall 1: Not Getting a Refresh Token
Cause: You didn't set access_type=offline and prompt=consent.
Fix: Revoke existing tokens and re-authorize with correct parameters.

Pitfall 2: Redirect URI Mismatch
Cause: The redirect URI doesn't exactly match what's registered.
Fix: Double-check both values match character-for-character.

Pitfall 3: Refresh Token Goes Missing
Cause: Google only returns refresh_token on first authorization.
Fix: Merge new tokens with existing: const updated = { ...existingTokens, ...newTokens };

Conclusion

OAuth for AI agents requires:

  • Authorization without a browser (device flow or temporary web server)
  • Token refresh before expiry
  • Secure token storage and encryption
  • Error recovery and re-authorization

If you're building a production system, consider using a managed OAuth service like Clamper to save weeks of development time.

Read the full guide with detailed code examples at clamper.tech/blog/oauth-flow-implementation