Labsco
vercel-labs logo

google

1,500

by vercel · part of vercel-labs/emulate

Emulated Google OAuth 2.0, OpenID Connect, Gmail, Calendar, and Drive for local development and testing. Use when the user needs to test Google sign-in…

🔥🔥🔥✓ VerifiedFreeNeeds API keys
🧰 Not standalone. This skill ships with vercel-labs/emulate and only works together with that tool — install the tool first, then add this skill.

Emulated Google OAuth 2.0, OpenID Connect, Gmail, Calendar, and Drive for local development and testing. Use when the user needs to test Google sign-in…

Inspect the full instructions your agent will receiveExpand

This is the exact playbook injected into your agent when the skill activates — shown here so you can audit it before installing. You don't need to read it to use the skill.

by vercel

Emulated Google OAuth 2.0, OpenID Connect, Gmail, Calendar, and Drive for local development and testing. Use when the user needs to test Google sign-in… npx skills add https://github.com/vercel-labs/emulate --skill google Download ZIPGitHub1.5k

Google OAuth 2.0 / OIDC + Gmail, Calendar & Drive Emulator

OAuth 2.0 and OpenID Connect emulation with authorization code flow, PKCE support, ID tokens, OIDC discovery, refresh tokens, plus Gmail, Google Calendar, and Google Drive REST API surfaces.

Start

Copy & paste — that's it
# Google only
npx emulate --service google

# Default port
# http://localhost:4002

Or programmatically:

Copy & paste — that's it
import { createEmulator } from 'emulate'

const google = await createEmulator({ service: 'google', port: 4002 })
// google.url === 'http://localhost:4002'

Pointing Your App at the Emulator

Environment Variable

Copy & paste — that's it
GOOGLE_EMULATOR_URL=http://localhost:4002

OAuth URL Mapping

Real Google URL Emulator URL https://accounts.google.com/o/oauth2/v2/auth $GOOGLE_EMULATOR_URL/o/oauth2/v2/auth https://oauth2.googleapis.com/token $GOOGLE_EMULATOR_URL/oauth2/token https://www.googleapis.com/oauth2/v2/userinfo $GOOGLE_EMULATOR_URL/oauth2/v2/userinfo https://accounts.google.com/.well-known/openid-configuration $GOOGLE_EMULATOR_URL/.well-known/openid-configuration https://www.googleapis.com/oauth2/v3/certs $GOOGLE_EMULATOR_URL/oauth2/v3/certs https://gmail.googleapis.com/gmail/v1/... $GOOGLE_EMULATOR_URL/gmail/v1/... https://www.googleapis.com/calendar/v3/... $GOOGLE_EMULATOR_URL/calendar/v3/... https://www.googleapis.com/drive/v3/... $GOOGLE_EMULATOR_URL/drive/v3/...

google-auth-library (Node.js)

Copy & paste — that's it
import { OAuth2Client } from 'google-auth-library'

const GOOGLE_URL = process.env.GOOGLE_EMULATOR_URL ?? 'https://accounts.google.com'

const client = new OAuth2Client({
 clientId: process.env.GOOGLE_CLIENT_ID,
 clientSecret: process.env.GOOGLE_CLIENT_SECRET,
 redirectUri: 'http://localhost:3000/api/auth/callback/google',
})

const emulatorAuthorizeUrl = `${GOOGLE_URL}/o/oauth2/v2/auth?client_id=${process.env.GOOGLE_CLIENT_ID}&redirect_uri=...&scope=openid+email+profile&response_type=code&state=...`

Auth.js / NextAuth.js

Copy & paste — that's it
import Google from '@auth/core/providers/google'

Google({
 clientId: process.env.GOOGLE_CLIENT_ID,
 clientSecret: process.env.GOOGLE_CLIENT_SECRET,
 authorization: {
 url: `${process.env.GOOGLE_EMULATOR_URL}/o/oauth2/v2/auth`,
 params: { scope: 'openid email profile' },
 },
 token: {
 url: `${process.env.GOOGLE_EMULATOR_URL}/oauth2/token`,
 },
 userinfo: {
 url: `${process.env.GOOGLE_EMULATOR_URL}/oauth2/v2/userinfo`,
 },
})

Passport.js

Copy & paste — that's it
import { Strategy as GoogleStrategy } from 'passport-google-oauth20'

const GOOGLE_URL = process.env.GOOGLE_EMULATOR_URL ?? 'https://accounts.google.com'

new GoogleStrategy({
 clientID: process.env.GOOGLE_CLIENT_ID,
 clientSecret: process.env.GOOGLE_CLIENT_SECRET,
 callbackURL: 'http://localhost:3000/api/auth/callback/google',
 authorizationURL: `${GOOGLE_URL}/o/oauth2/v2/auth`,
 tokenURL: `${GOOGLE_URL}/oauth2/token`,
 userProfileURL: `${GOOGLE_URL}/oauth2/v2/userinfo`,
}, verifyCallback)

Seed Config

Copy & paste — that's it
google:
 users:
 - email: [email protected]
 name: Test User
 given_name: Test
 family_name: User
 picture: https://lh3.googleusercontent.com/a/default-user
 email_verified: true
 locale: en
 - email: [email protected]
 name: Developer
 - email: [email protected]
 name: Admin
 hd: acme.com
 oauth_clients:
 - client_id: my-client-id.apps.googleusercontent.com
 client_secret: GOCSPX-secret
 name: My App
 redirect_uris:
 - http://localhost:3000/api/auth/callback/google
 labels:
 - id: Label_ops
 user_email: [email protected]
 name: Ops/Review
 color_background: "#DDEEFF"
 color_text: "#111111"
 messages:
 - id: msg_welcome
 user_email: [email protected]
 thread_id: thr_welcome
 from: "[email protected]"
 to: [email protected]
 subject: Welcome to the Gmail emulator
 body_text: You can now test Gmail flows locally.
 label_ids: [INBOX, UNREAD, CATEGORY_UPDATES]
 date: "2025-01-04T10:00:00.000Z"
 calendars:
 - id: primary
 user_email: [email protected]
 summary: [email protected]
 primary: true
 selected: true
 time_zone: UTC
 calendar_events:
 - id: evt_kickoff
 user_email: [email protected]
 calendar_id: primary
 summary: Project Kickoff
 start_date_time: "2025-01-10T09:00:00.000Z"
 end_date_time: "2025-01-10T09:30:00.000Z"
 attendees:
 - email: [email protected]
 display_name: Test User
 conference_entry_points:
 - entry_point_type: video
 uri: https://meet.google.com/example
 label: Google Meet
 hangout_link: https://meet.google.com/example
 drive_items:
 - id: drv_docs
 user_email: [email protected]
 name: Docs
 mime_type: application/vnd.google-apps.folder
 parent_ids: [root]
 - id: drv_readme
 user_email: [email protected]
 name: README.md
 mime_type: text/markdown
 parent_ids: [drv_docs]
 data: "# Hello World"

When no OAuth clients are configured, the emulator accepts any client_id. With clients configured, strict validation is enforced for client_id, client_secret, and redirect_uri.

Hosted domain (hd) claim

Google Workspace accounts include an hd claim in ID tokens and userinfo responses identifying the user's hosted domain. The emulator derives this automatically from the user's email domain. Consumer domains (gmail.com, googlemail.com) omit the claim, matching real Google behavior.

To override the derived value, set hd on a seeded user. To suppress the claim entirely, set hd to an empty string.

OAuth / OIDC Endpoints

OIDC Discovery

Copy & paste — that's it
curl http://localhost:4002/.well-known/openid-configuration

JWKS

Copy & paste — that's it
curl http://localhost:4002/oauth2/v3/certs

Returns { "keys": [] }. ID tokens are signed with HS256 using an internal secret.

Authorization

Copy & paste — that's it
# Browser flow: redirects to a user picker page
curl -v "http://localhost:4002/o/oauth2/v2/auth?\
client_id=my-client-id.apps.googleusercontent.com&\
redirect_uri=http://localhost:3000/api/auth/callback/google&\
scope=openid+email+profile&\
response_type=code&\
state=random-state&\
nonce=random-nonce"

Supports code_challenge and code_challenge_method for PKCE.

Token Exchange

Copy & paste — that's it
curl -X POST http://localhost:4002/oauth2/token \
 -H "Content-Type: application/x-www-form-urlencoded" \
 -d "code= &\
client_id=my-client-id.apps.googleusercontent.com&\
client_secret=GOCSPX-secret&\
redirect_uri=http://localhost:3000/api/auth/callback/google&\
grant_type=authorization_code"

Also accepts application/json body. Returns:

Copy & paste — that's it
{
 "access_token": "google_...",
 "refresh_token": "google_refresh_...",
 "id_token": " ",
 "token_type": "Bearer",
 "expires_in": 3600,
 "scope": "openid email profile"
}

Refresh Token

Copy & paste — that's it
curl -X POST http://localhost:4002/oauth2/token \
 -H "Content-Type: application/x-www-form-urlencoded" \
 -d "refresh_token=google_refresh_...&\
client_id=my-client-id.apps.googleusercontent.com&\
client_secret=GOCSPX-secret&\
grant_type=refresh_token"

Returns a new access_token (no new refresh_token or id_token on refresh).

User Info

Copy & paste — that's it
curl http://localhost:4002/oauth2/v2/userinfo \
 -H "Authorization: Bearer google_..."

Token Revocation

Copy & paste — that's it
curl -X POST http://localhost:4002/oauth2/revoke \
 -H "Content-Type: application/x-www-form-urlencoded" \
 -d "token=google_..."

Gmail API

All Gmail endpoints are under /gmail/v1/users/:userId/... where :userId is me or the authenticated user's email.

Messages

Copy & paste — that's it
# List messages (filter by labels, search query)
curl "http://localhost:4002/gmail/v1/users/me/messages?labelIds=INBOX&q=from:welcome&maxResults=10" \
 -H "Authorization: Bearer $TOKEN"

# Get message (format: full, metadata, minimal, raw)
curl "http://localhost:4002/gmail/v1/users/me/messages/msg_welcome?format=full" \
 -H "Authorization: Bearer $TOKEN"

# Send message
curl -X POST http://localhost:4002/gmail/v1/users/me/messages/send \
 -H "Authorization: Bearer $TOKEN" \
 -H "Content-Type: application/json" \
 -d '{"to": "[email protected]", "subject": "Hello", "body_text": "Hi there"}'

# Insert message (bypass send)
curl -X POST http://localhost:4002/gmail/v1/users/me/messages \
 -H "Authorization: Bearer $TOKEN" \
 -H "Content-Type: application/json" \
 -d '{"to": "[email protected]", "from": "[email protected]", "subject": "Test", "body_text": "Body", "labelIds": ["INBOX"]}'

# Import message
curl -X POST http://localhost:4002/gmail/v1/users/me/messages/import \
 -H "Authorization: Bearer $TOKEN" \
 -H "Content-Type: application/json" \
 -d '{"to": "[email protected]", "from": "[email protected]", "subject": "Imported", "body_text": "Content"}'

# Modify labels on a message
curl -X POST http://localhost:4002/gmail/v1/users/me/messages/msg_welcome/modify \
 -H "Authorization: Bearer $TOKEN" \
 -H "Content-Type: application/json" \
 -d '{"addLabelIds": ["STARRED"], "removeLabelIds": ["UNREAD"]}'

# Trash / untrash
curl -X POST http://localhost:4002/gmail/v1/users/me/messages/msg_welcome/trash \
 -H "Authorization: Bearer $TOKEN"
curl -X POST http://localhost:4002/gmail/v1/users/me/messages/msg_welcome/untrash \
 -H "Authorization: Bearer $TOKEN"

# Delete permanently
curl -X DELETE http://localhost:4002/gmail/v1/users/me/messages/msg_welcome \
 -H "Authorization: Bearer $TOKEN"

# Batch modify
curl -X POST http://localhost:4002/gmail/v1/users/me/messages/batchModify \
 -H "Authorization: Bearer $TOKEN" \
 -H "Content-Type: application/json" \
 -d '{"ids": ["msg_welcome", "msg_build"], "addLabelIds": ["STARRED"]}'

# Batch delete
curl -X POST http://localhost:4002/gmail/v1/users/me/messages/batchDelete \
 -H "Authorization: Bearer $TOKEN" \
 -H "Content-Type: application/json" \
 -d '{"ids": ["msg_welcome"]}'

# Get attachment
curl http://localhost:4002/gmail/v1/users/me/messages/msg_id/attachments/att_id \
 -H "Authorization: Bearer $TOKEN"

Upload variants also available at /upload/gmail/v1/users/:userId/messages, .../messages/send, .../messages/import.

Drafts

Copy & paste — that's it
# List drafts
curl http://localhost:4002/gmail/v1/users/me/drafts \
 -H "Authorization: Bearer $TOKEN"

# Create draft
curl -X POST http://localhost:4002/gmail/v1/users/me/drafts \
 -H "Authorization: Bearer $TOKEN" \
 -H "Content-Type: application/json" \
 -d '{"message": {"to": "[email protected]", "subject": "Draft subject", "body_text": "Draft body"}}'

# Get draft (format: full, metadata, minimal, raw)
curl "http://localhost:4002/gmail/v1/users/me/drafts/draft_id?format=full" \
 -H "Authorization: Bearer $TOKEN"

# Update draft
curl -X PUT http://localhost:4002/gmail/v1/users/me/drafts/draft_id \
 -H "Authorization: Bearer $TOKEN" \
 -H "Content-Type: application/json" \
 -d '{"message": {"subject": "Updated subject", "body_text": "Updated body"}}'

# Send draft
curl -X POST http://localhost:4002/gmail/v1/users/me/drafts/send \
 -H "Authorization: Bearer $TOKEN" \
 -H "Content-Type: application/json" \
 -d '{"id": "draft_id"}'

# Delete draft
curl -X DELETE http://localhost:4002/gmail/v1/users/me/drafts/draft_id \
 -H "Authorization: Bearer $TOKEN"

Threads

Copy & paste — that's it
# List threads (filter by labels, search query)
curl "http://localhost:4002/gmail/v1/users/me/threads?labelIds=INBOX&maxResults=20" \
 -H "Authorization: Bearer $TOKEN"

# Get thread (all messages in thread)
curl "http://localhost:4002/gmail/v1/users/me/threads/thr_welcome?format=full" \
 -H "Authorization: Bearer $TOKEN"

# Modify labels on all messages in thread
curl -X POST http://localhost:4002/gmail/v1/users/me/threads/thr_welcome/modify \
 -H "Authorization: Bearer $TOKEN" \
 -H "Content-Type: application/json" \
 -d '{"addLabelIds": ["STARRED"], "removeLabelIds": ["UNREAD"]}'

# Trash / untrash / delete thread
curl -X POST http://localhost:4002/gmail/v1/users/me/threads/thr_welcome/trash \
 -H "Authorization: Bearer $TOKEN"
curl -X DELETE http://localhost:4002/gmail/v1/users/me/threads/thr_welcome \
 -H "Authorization: Bearer $TOKEN"

Labels

Copy & paste — that's it
# List labels
curl http://localhost:4002/gmail/v1/users/me/labels \
 -H "Authorization: Bearer $TOKEN"

# Get label
curl http://localhost:4002/gmail/v1/users/me/labels/INBOX \
 -H "Authorization: Bearer $TOKEN"

# Create label
curl -X POST http://localhost:4002/gmail/v1/users/me/labels \
 -H "Authorization: Bearer $TOKEN" \
 -H "Content-Type: application/json" \
 -d '{"name": "My Label", "color": {"backgroundColor": "#DDEEFF", "textColor": "#111111"}}'

# Update label (PUT replaces, PATCH merges)
curl -X PATCH http://localhost:4002/gmail/v1/users/me/labels/Label_ops \
 -H "Authorization: Bearer $TOKEN" \
 -H "Content-Type: application/json" \
 -d '{"name": "Ops/Reviewed"}'

# Delete label (user labels only)
curl -X DELETE http://localhost:4002/gmail/v1/users/me/labels/Label_ops \
 -H "Authorization: Bearer $TOKEN"

History & Watch

Copy & paste — that's it
# List history changes since a given historyId
curl "http://localhost:4002/gmail/v1/users/me/history?startHistoryId=1&historyTypes=messageAdded&maxResults=100" \
 -H "Authorization: Bearer $TOKEN"

# Set up push notification watch (stub)
curl -X POST http://localhost:4002/gmail/v1/users/me/watch \
 -H "Authorization: Bearer $TOKEN" \
 -H "Content-Type: application/json" \
 -d '{"topicName": "projects/my-project/topics/gmail", "labelIds": ["INBOX"]}'

# Stop watch
curl -X POST http://localhost:4002/gmail/v1/users/me/stop \
 -H "Authorization: Bearer $TOKEN"

Settings

Copy & paste — that's it
# List filters
curl http://localhost:4002/gmail/v1/users/me/settings/filters \
 -H "Authorization: Bearer $TOKEN"

# Create filter (auto-label incoming messages matching criteria)
curl -X POST http://localhost:4002/gmail/v1/users/me/settings/filters \
 -H "Authorization: Bearer $TOKEN" \
 -H "Content-Type: application/json" \
 -d '{"criteria": {"from": "[email protected]"}, "action": {"addLabelIds": ["Label_ops"]}}'

# Delete filter
curl -X DELETE http://localhost:4002/gmail/v1/users/me/settings/filters/filter_id \
 -H "Authorization: Bearer $TOKEN"

# List forwarding addresses
curl http://localhost:4002/gmail/v1/users/me/settings/forwardingAddresses \
 -H "Authorization: Bearer $TOKEN"

# List send-as aliases
curl http://localhost:4002/gmail/v1/users/me/settings/sendAs \
 -H "Authorization: Bearer $TOKEN"

Google Calendar API

Calendar List

Copy & paste — that's it
curl http://localhost:4002/calendar/v3/users/me/calendarList \
 -H "Authorization: Bearer $TOKEN"

Events

Copy & paste — that's it
# List events (filter by time range, search, order)
curl "http://localhost:4002/calendar/v3/calendars/primary/events?\
timeMin=2025-01-01T00:00:00Z&timeMax=2025-12-31T23:59:59Z&maxResults=50&orderBy=startTime" \
 -H "Authorization: Bearer $TOKEN"

# Create event
curl -X POST http://localhost:4002/calendar/v3/calendars/primary/events \
 -H "Authorization: Bearer $TOKEN" \
 -H "Content-Type: application/json" \
 -d '{"summary": "Team Meeting", "start": {"dateTime": "2025-01-10T14:00:00Z"}, "end": {"dateTime": "2025-01-10T15:00:00Z"}, "attendees": [{"email": "[email protected]"}]}'

# Delete event
curl -X DELETE http://localhost:4002/calendar/v3/calendars/primary/events/evt_kickoff \
 -H "Authorization: Bearer $TOKEN"

FreeBusy

Copy & paste — that's it
curl -X POST http://localhost:4002/calendar/v3/freeBusy \
 -H "Authorization: Bearer $TOKEN" \
 -H "Content-Type: application/json" \
 -d '{"timeMin": "2025-01-10T00:00:00Z", "timeMax": "2025-01-10T23:59:59Z", "items": [{"id": "primary"}]}'

Google Drive API

Files

Copy & paste — that's it
# List files (with query filter, pagination, ordering)
curl "http://localhost:4002/drive/v3/files?q='root'+in+parents&pageSize=20" \
 -H "Authorization: Bearer $TOKEN"

# Create file (JSON metadata)
curl -X POST http://localhost:4002/drive/v3/files \
 -H "Authorization: Bearer $TOKEN" \
 -H "Content-Type: application/json" \
 -d '{"name": "notes.txt", "mimeType": "text/plain", "parents": ["root"]}'

# Create file with content (multipart/related upload)
curl -X POST http://localhost:4002/upload/drive/v3/files \
 -H "Authorization: Bearer $TOKEN" \
 -H "Content-Type: multipart/related; boundary=boundary" \
 --data-binary $'--boundary\r\nContent-Type: application/json\r\n\r\n{"name":"data.csv","mimeType":"text/csv"}\r\n--boundary\r\nContent-Type: text/csv\r\n\r\na,b,c\n1,2,3\r\n--boundary--'

# Get file metadata
curl http://localhost:4002/drive/v3/files/drv_readme \
 -H "Authorization: Bearer $TOKEN"

# Download file content
curl "http://localhost:4002/drive/v3/files/drv_readme?alt=media" \
 -H "Authorization: Bearer $TOKEN"

# Update file (PATCH or PUT; move parents with query params)
curl -X PATCH "http://localhost:4002/drive/v3/files/drv_readme?addParents=folder_id&removeParents=root" \
 -H "Authorization: Bearer $TOKEN" \
 -H "Content-Type: application/json" \
 -d '{"name": "README-updated.md"}'

Common Patterns

Full Authorization Code Flow

Copy & paste — that's it
GOOGLE_URL="http://localhost:4002"
CLIENT_ID="my-client-id.apps.googleusercontent.com"
CLIENT_SECRET="GOCSPX-secret"
REDIRECT_URI="http://localhost:3000/api/auth/callback/google"

# 1. Open in browser (user picks a seeded account)
# $GOOGLE_URL/o/oauth2/v2/auth?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&scope=openid+email+profile&response_type=code&state=abc

# 2. After user selection, emulator redirects to:
# $REDIRECT_URI?code= &state=abc

# 3. Exchange code for tokens
curl -X POST $GOOGLE_URL/oauth2/token \
 -H "Content-Type: application/x-www-form-urlencoded" \
 -d "code= &client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET&redirect_uri=$REDIRECT_URI&grant_type=authorization_code"

# 4. Fetch user info with the access_token
curl $GOOGLE_URL/oauth2/v2/userinfo \
 -H "Authorization: Bearer "

OIDC Discovery-Based Setup

Copy & paste — that's it
import { Issuer } from 'openid-client'

const googleIssuer = await Issuer.discover(
 process.env.GOOGLE_EMULATOR_URL ?? 'https://accounts.google.com'
)

const client = new googleIssuer.Client({
 client_id: process.env.GOOGLE_CLIENT_ID,
 client_secret: process.env.GOOGLE_CLIENT_SECRET,
 redirect_uris: ['http://localhost:3000/api/auth/callback/google'],
})

Send a Gmail Message and Check the Thread

Copy & paste — that's it
TOKEN="test_token_admin"
BASE="http://localhost:4002"

# Send a message
curl -X POST $BASE/gmail/v1/users/me/messages/send \
 -H "Authorization: Bearer $TOKEN" \
 -H "Content-Type: application/json" \
 -d '{"to": "[email protected]", "subject": "Test", "body_text": "Hello"}'

# List threads in INBOX
curl "$BASE/gmail/v1/users/me/threads?labelIds=INBOX" \
 -H "Authorization: Bearer $TOKEN"