How to Set Up Vercel OAuth Authentication for Open Agents

Open Agents implements the standard OAuth 2.0 PKCE flow to authenticate users with Vercel, storing encrypted tokens in a PostgreSQL database and syncing them to sandboxed CLI environments through three core layers: PKCE helpers in oauth.ts, persistent storage in token.ts, and sandbox configuration in vercel-cli-auth.ts.

This guide explains how to implement Vercel OAuth authentication for Open Agents, an open-source project by Vercel Labs that enables AI agents to deploy and manage Vercel projects. The repository provides a complete, production-ready OAuth 2.0 PKCE implementation with automatic token refresh and secure sandbox integration.

Configure Vercel Credentials

Before implementing the flow, add your Vercel OAuth application credentials to the environment variables of your Open Agents deployment:

NEXT_PUBLIC_VERCEL_APP_CLIENT_ID=<your-vercel-client-id>
VERCEL_APP_CLIENT_SECRET=<your-vercel-client-secret>

The client ID is public and used to build authorization URLs, while the client secret remains server-only and is used for token exchange. Store these in your Vercel dashboard or .env.local file for local development.

OAuth Implementation Architecture

The Vercel OAuth authentication for Open Agents is organized into three logical layers that handle distinct responsibilities:

Layer Responsibility Core Source Files
PKCE & Token Exchange Generates code verifiers, derives code challenges, builds authorization URLs, exchanges codes for tokens, refreshes expired tokens, and handles revocation. apps/web/lib/vercel/oauth.ts
Persistent Storage Reads and writes encrypted tokens to the users table, decrypts tokens on demand, and automatically refreshes expired tokens before returning them. apps/web/lib/vercel/token.ts
Sandbox Sync Mirrors stored tokens into the Vercel CLI configuration files (auth.json and .vercel/project.json) that sandboxed processes expect for authentication. apps/web/lib/sandbox/vercel-cli-auth.ts

Step 1: Initiate the Login Flow

To start the Vercel OAuth authentication for Open Agents, generate a PKCE pair and redirect the user to Vercel's authorization endpoint. This typically happens in a server-side route such as /api/auth/vercel.

import { 
  generateCodeVerifier, 
  generateCodeChallenge, 
  getVercelAuthorizationUrl 
} from '@/lib/vercel/oauth';

export async function GET(request: Request) {
  // 1. Create PKCE values
  const verifier = generateCodeVerifier();
  const challenge = await generateCodeChallenge(verifier);
  
  // 2. Build the authorization URL
  const authUrl = getVercelAuthorizationUrl({
    clientId: process.env.NEXT_PUBLIC_VERCEL_APP_CLIENT_ID!,
    redirectUri: 'https://your-site.com/api/auth/vercel/callback',
    state: crypto.randomUUID(),  // CSRF protection
    codeChallenge: challenge,
  });
  
  // 3. Store the verifier in a signed cookie or session
  // Then redirect the user to Vercel
  return Response.redirect(authUrl);
}

The generateCodeVerifier function creates a cryptographically random string, while generateCodeChallenge hashes it using SHA-256. These helper functions are defined in apps/web/lib/vercel/oauth.ts.

Step 2: Handle the OAuth Callback

After the user authorizes your application, Vercel redirects to your callback URL with an authorization code and state parameter. Exchange this code for access and refresh tokens, then persist them securely.

import { exchangeVercelCode } from '@/lib/vercel/oauth';
import { db } from '@/lib/db/client';
import { encrypt } from '@/lib/crypto';
import { users } from '@/lib/db/schema';

export async function GET(request: Request) {
  const url = new URL(request.url);
  const code = url.searchParams.get('code');
  const state = url.searchParams.get('state');
  
  // Retrieve the code verifier you stored in Step 1
  const verifier = await getVerifierFromCookieOrSession(state);
  
  // Exchange the code for tokens
  const tokenResponse = await exchangeVercelCode({
    code: code!,
    codeVerifier: verifier,
    clientId: process.env.NEXT_PUBLIC_VERCEL_APP_CLIENT_ID!,
    clientSecret: process.env.VERCEL_APP_CLIENT_SECRET!,
    redirectUri: 'https://your-site.com/api/auth/vercel/callback',
  });
  
  // Encrypt and store the tokens
  await db.insert(users).values({
    id: currentUserId,
    provider: 'vercel',
    accessToken: encrypt(tokenResponse.access_token),
    refreshToken: tokenResponse.refresh_token 
      ? encrypt(tokenResponse.refresh_token) 
      : null,
    tokenExpiresAt: new Date(Date.now() + tokenResponse.expires_in * 1000),
    externalId: await fetchVercelUserId(tokenResponse.access_token),
  });
  
  return Response.redirect('/');
}

The exchangeVercelCode function handles the POST request to Vercel's token endpoint and returns the standard OAuth 2.0 token response including access_token, refresh_token, and expires_in.

Step 3: Retrieve and Refresh Tokens

When your application needs to call the Vercel API or spawn a sandboxed CLI, use the getUserVercelAuthInfo function from apps/web/lib/vercel/token.ts. This helper automatically refreshes expired tokens before returning them.

import { getUserVercelAuthInfo } from '@/lib/vercel/token';

const authInfo = await getUserVercelAuthInfo(userId);

if (authInfo) {
  const { token, expiresAt, externalId } = authInfo;
  // token is guaranteed to be fresh – refresh happens automatically if needed
  console.log(`Access token expires at: ${expiresAt}`);
}

The getUserVercelAuthInfo implementation in apps/web/lib/vercel/token.ts performs the following steps:

  1. Reads the encrypted row from the users table defined in apps/web/lib/db/schema.ts
  2. Decrypts the access token using helpers from apps/web/lib/crypto.ts
  3. Checks the expiration timestamp against the current time
  4. Calls refreshVercelToken from apps/web/lib/vercel/oauth.ts if expired
  5. Persists the new token back to the database and returns the updated auth info

Step 4: Sync Tokens to Sandbox Environment

When running a sandboxed Vercel CLI process (e.g., for user-submitted agents), you must mirror the stored token into the CLI configuration files. The syncVercelCliAuthToSandbox function in apps/web/lib/sandbox/vercel-cli-auth.ts handles this.

import { 
  getVercelCliSandboxSetup, 
  syncVercelCliAuthToSandbox 
} from '@/lib/sandbox/vercel-cli-auth';

// Build the sandbox setup for the current user/session
const setup = await getVercelCliSandboxSetup({
  userId,
  sessionRecord: {
    vercelProjectId: 'proj_123',
    vercelProjectName: 'my-project',
    vercelTeamId: authInfo?.externalId ?? null,
  },
});

// Write the files inside the sandbox container
await syncVercelCliAuthToSandbox({ sandbox, setup });

This writes the auth.json file to ~/.local/share/com.vercel.cli/auth.json and optionally creates .vercel/project.json for project linking, ensuring the Vercel CLI inside the sandbox can authenticate without user intervention.

Optional: Revoke Vercel Access

If a user disconnects their Vercel account, call revokeVercelToken from apps/web/lib/vercel/oauth.ts and remove the stored credentials from your database.

import { revokeVercelToken } from '@/lib/vercel/oauth';

await revokeVercelToken({
  token: storedAccessToken,
  clientId: process.env.NEXT_PUBLIC_VERCEL_APP_CLIENT_ID!,
  clientSecret: process.env.VERCEL_APP_CLIENT_SECRET!,
});

// Clear the database row
await db.delete(users).where(eq(users.id, userId));

This performs a POST request to Vercel's token revocation endpoint, invalidating the tokens remotely.

Summary

  • Vercel OAuth authentication for Open Agents implements the OAuth 2.0 PKCE flow to securely connect user accounts without exposing client secrets to the browser.
  • The implementation spans three core files: apps/web/lib/vercel/oauth.ts for PKCE and token exchange, apps/web/lib/vercel/token.ts for encrypted persistent storage, and apps/web/lib/sandbox/vercel-cli-auth.ts for sandbox CLI configuration.
  • Always store the VERCEL_APP_CLIENT_SECRET server-side while exposing only NEXT_PUBLIC_VERCEL_APP_CLIENT_ID to the client.
  • Use getUserVercelAuthInfo to automatically handle token refresh when calling the Vercel API or spawning sandboxes.
  • Sync tokens to sandbox environments using syncVercelCliAuthToSandbox to enable authenticated Vercel CLI operations inside isolated processes.

Frequently Asked Questions

What OAuth flow does Open Agents use for Vercel authentication?

Open Agents uses the OAuth 2.0 PKCE (Proof Key for Code Exchange) flow. This method generates a random code verifier in the client, hashes it to create a code challenge, and sends only the challenge to the authorization server. According to the source code in apps/web/lib/vercel/oauth.ts, this prevents authorization code interception attacks while allowing secure authentication without a backend-stored client secret.

How are Vercel tokens stored securely in Open Agents?

All tokens are encrypted using symmetric encryption before storage in the PostgreSQL database. The apps/web/lib/vercel/token.ts file handles reading and writing to the users table defined in apps/web/lib/db/schema.ts, utilizing encryption helpers from apps/web/lib/crypto.ts. Access tokens, refresh tokens, and expiration timestamps are all stored in encrypted form, ensuring that database leaks do not expose live Vercel credentials.

How does Open Agents handle token expiration automatically?

The getUserVercelAuthInfo function in apps/web/lib/vercel/token.ts automatically checks token expiration against the current time. If the access token has expired, it transparently calls refreshVercelToken from apps/web/lib/vercel/oauth.ts to obtain a new access token using the stored refresh token. The new tokens are then encrypted and persisted back to the database before returning the fresh credentials to the caller, ensuring uninterrupted API access.

Can users disconnect or revoke Vercel access in Open Agents?

Yes, users can revoke access by calling the revokeVercelToken function exported from apps/web/lib/vercel/oauth.ts. This function sends a revocation request to Vercel's token endpoint, invalidating both the access and refresh tokens remotely. After revocation, the application should delete the corresponding encrypted token records from the database using the Drizzle ORM client, completing the disconnection process.

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 →