Clamper aiYour 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.
OAuth was designed for web apps where a human user is present to authorize access. The typical flow:
But AI agents don't have a browser. They run headless on a server, often in a terminal or as a background process.
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.
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 tokenprompt=consent — Forces the consent screen to show (needed to get refresh token)redirect_uri — Must match exactly what you registered in Google Cloud ConsolePerfect 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.
http://localhost:3000/oauth/callback
// 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);
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;
}
}
}
.oauth-tokens.json
.env
*-credentials.json
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;
}
// ❌ Don't ask for everything
scope: 'https://www.googleapis.com/auth/drive'
// ✅ Request minimal access
scope: 'https://www.googleapis.com/auth/spreadsheets.readonly'
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:
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 };
OAuth for AI agents requires:
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