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:
- User clicks "Connect Google" in your web app
- Browser redirects to Google's authorization page
- User approves access and grants permissions
- Google redirects back with an authorization code
- Your app exchanges the code for access and refresh tokens
- 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
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
This works great for OpenClaw agents running in a terminal.
Implementing OAuth for OpenClaw Agents
Step 1: Register Your App
- Go to console.cloud.google.com
- Create a new project (or select existing)
- Enable Google Sheets API
- Go to APIs & Services → Credentials
- Create OAuth 2.0 Client ID → Choose "Desktop app" or "Web application"
- Add redirect URI:
http://localhost:3000/oauth/callback - 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);
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;
}
}
}
Security Best Practices
1. Never Commit Tokens to Git
.oauth-tokens.json
.env
*-credentials.json
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;
}
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'
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'
});
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
Top comments (0)