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 Or programmatically: Pointing Your App at the Emulator Environment Variable OAuth URL Mapping | Real Google URL | Emulator URL | |-----------------|-------------| | | | | | | | | | | | | | | | | | | | | | | | | google-auth-library (Node.js) Auth.js / NextAuth.js Passport.js Seed Config When no OAuth clients are configured, the emulator accep…

--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--'\n\n# Get file metadata\ncurl http://localhost:4002/drive/v3/files/drv_readme \\\n -H \"Authorization: Bearer $TOKEN\"\n\n# Download file content\ncurl \"http://localhost:4002/drive/v3/files/drv_readme?alt=media\" \\\n -H \"Authorization: Bearer $TOKEN\"\n\n# Update file (PATCH or PUT; move parents with query params)\ncurl -X PATCH \"http://localhost:4002/drive/v3/files/drv_readme?addParents=folder_id&removeParents=root\" \\\n -H \"Authorization: Bearer $TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"name\": \"README-updated.md\"}'\n```\n\n## Common Patterns\n\n### Full Authorization Code Flow\n\n```bash\nGOOGLE_URL=\"http://localhost:4002\"\nCLIENT_ID=\"my-client-id.apps.googleusercontent.com\"\nCLIENT_SECRET=\"GOCSPX-secret\"\nREDIRECT_URI=\"http://localhost:3000/api/auth/callback/google\"\n\n# 1. Open in browser (user picks a seeded account)\n# $GOOGLE_URL/o/oauth2/v2/auth?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&scope=openid+email+profile&response_type=code&state=abc\n\n# 2. After user selection, emulator redirects to:\n# $REDIRECT_URI?code=\u003ccode>&state=abc\n\n# 3. Exchange code for tokens\ncurl -X POST $GOOGLE_URL/oauth2/token \\\n -H \"Content-Type: application/x-www-form-urlencoded\" \\\n -d \"code=\u003ccode>&client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET&redirect_uri=$REDIRECT_URI&grant_type=authorization_code\"\n\n# 4. Fetch user info with the access_token\ncurl $GOOGLE_URL/oauth2/v2/userinfo \\\n -H \"Authorization: Bearer \u003caccess_token>\"\n```\n\n### OIDC Discovery-Based Setup\n\n```typescript\nimport { Issuer } from 'openid-client'\n\nconst googleIssuer = await Issuer.discover(\n process.env.GOOGLE_EMULATOR_URL ?? 'https://accounts.google.com'\n)\n\nconst client = new googleIssuer.Client({\n client_id: process.env.GOOGLE_CLIENT_ID,\n client_secret: process.env.GOOGLE_CLIENT_SECRET,\n redirect_uris: ['http://localhost:3000/api/auth/callback/google'],\n})\n```\n\n### Send a Gmail Message and Check the Thread\n\n```bash\nTOKEN=\"test_token_admin\"\nBASE=\"http://localhost:4002\"\n\n# Send a message\ncurl -X POST $BASE/gmail/v1/users/me/messages/send \\\n -H \"Authorization: Bearer $TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"to\": \"[email protected]\", \"subject\": \"Test\", \"body_text\": \"Hello\"}'\n\n# List threads in INBOX\ncurl \"$BASE/gmail/v1/users/me/threads?labelIds=INBOX\" \\\n -H \"Authorization: Bearer $TOKEN\"\n```\n---","attachment_filenames":[],"attachments":[],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Google OAuth 2.0 / OIDC + Gmail, Calendar & Drive Emulator","type":"text"}]},{"type":"paragraph","content":[{"text":"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.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Start","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Google only\nnpx emulate --service google\n\n# Default port\n# http://localhost:4002","type":"text"}]},{"type":"paragraph","content":[{"text":"Or programmatically:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"typescript"},"content":[{"text":"import { createEmulator } from 'emulate'\n\nconst google = await createEmulator({ service: 'google', port: 4002 })\n// google.url === 'http://localhost:4002'","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Pointing Your App at the Emulator","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Environment Variable","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"GOOGLE_EMULATOR_URL=http://localhost:4002","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"OAuth URL Mapping","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Real Google URL","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Emulator URL","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"https://accounts.google.com/o/oauth2/v2/auth","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"$GOOGLE_EMULATOR_URL/o/oauth2/v2/auth","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"https://oauth2.googleapis.com/token","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"$GOOGLE_EMULATOR_URL/oauth2/token","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"https://www.googleapis.com/oauth2/v2/userinfo","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"$GOOGLE_EMULATOR_URL/oauth2/v2/userinfo","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"https://accounts.google.com/.well-known/openid-configuration","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"$GOOGLE_EMULATOR_URL/.well-known/openid-configuration","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"https://www.googleapis.com/oauth2/v3/certs","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"$GOOGLE_EMULATOR_URL/oauth2/v3/certs","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"https://gmail.googleapis.com/gmail/v1/...","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"$GOOGLE_EMULATOR_URL/gmail/v1/...","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"https://www.googleapis.com/calendar/v3/...","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"$GOOGLE_EMULATOR_URL/calendar/v3/...","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"https://www.googleapis.com/drive/v3/...","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"$GOOGLE_EMULATOR_URL/drive/v3/...","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"google-auth-library (Node.js)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"typescript"},"content":[{"text":"import { OAuth2Client } from 'google-auth-library'\n\nconst GOOGLE_URL = process.env.GOOGLE_EMULATOR_URL ?? 'https://accounts.google.com'\n\nconst client = new OAuth2Client({\n clientId: process.env.GOOGLE_CLIENT_ID,\n clientSecret: process.env.GOOGLE_CLIENT_SECRET,\n redirectUri: 'http://localhost:3000/api/auth/callback/google',\n})\n\nconst emulatorAuthorizeUrl = `${GOOGLE_URL}/o/oauth2/v2/auth?client_id=${process.env.GOOGLE_CLIENT_ID}&redirect_uri=...&scope=openid+email+profile&response_type=code&state=...`","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Auth.js / NextAuth.js","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"typescript"},"content":[{"text":"import Google from '@auth/core/providers/google'\n\nGoogle({\n clientId: process.env.GOOGLE_CLIENT_ID,\n clientSecret: process.env.GOOGLE_CLIENT_SECRET,\n authorization: {\n url: `${process.env.GOOGLE_EMULATOR_URL}/o/oauth2/v2/auth`,\n params: { scope: 'openid email profile' },\n },\n token: {\n url: `${process.env.GOOGLE_EMULATOR_URL}/oauth2/token`,\n },\n userinfo: {\n url: `${process.env.GOOGLE_EMULATOR_URL}/oauth2/v2/userinfo`,\n },\n})","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Passport.js","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"typescript"},"content":[{"text":"import { Strategy as GoogleStrategy } from 'passport-google-oauth20'\n\nconst GOOGLE_URL = process.env.GOOGLE_EMULATOR_URL ?? 'https://accounts.google.com'\n\nnew GoogleStrategy({\n clientID: process.env.GOOGLE_CLIENT_ID,\n clientSecret: process.env.GOOGLE_CLIENT_SECRET,\n callbackURL: 'http://localhost:3000/api/auth/callback/google',\n authorizationURL: `${GOOGLE_URL}/o/oauth2/v2/auth`,\n tokenURL: `${GOOGLE_URL}/oauth2/token`,\n userProfileURL: `${GOOGLE_URL}/oauth2/v2/userinfo`,\n}, verifyCallback)","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Seed Config","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"yaml"},"content":[{"text":"google:\n users:\n - email: [email protected]\n name: Test User\n given_name: Test\n family_name: User\n picture: https://lh3.googleusercontent.com/a/default-user\n email_verified: true\n locale: en\n - email: [email protected]\n name: Developer\n - email: [email protected]\n name: Admin\n hd: acme.com\n oauth_clients:\n - client_id: my-client-id.apps.googleusercontent.com\n client_secret: GOCSPX-secret\n name: My App\n redirect_uris:\n - http://localhost:3000/api/auth/callback/google\n labels:\n - id: Label_ops\n user_email: [email protected]\n name: Ops/Review\n color_background: \"#DDEEFF\"\n color_text: \"#111111\"\n messages:\n - id: msg_welcome\n user_email: [email protected]\n thread_id: thr_welcome\n from: \"[email protected]\"\n to: [email protected]\n subject: Welcome to the Gmail emulator\n body_text: You can now test Gmail flows locally.\n label_ids: [INBOX, UNREAD, CATEGORY_UPDATES]\n date: \"2025-01-04T10:00:00.000Z\"\n calendars:\n - id: primary\n user_email: [email protected]\n summary: [email protected]\n primary: true\n selected: true\n time_zone: UTC\n calendar_events:\n - id: evt_kickoff\n user_email: [email protected]\n calendar_id: primary\n summary: Project Kickoff\n start_date_time: \"2025-01-10T09:00:00.000Z\"\n end_date_time: \"2025-01-10T09:30:00.000Z\"\n attendees:\n - email: [email protected]\n display_name: Test User\n conference_entry_points:\n - entry_point_type: video\n uri: https://meet.google.com/example\n label: Google Meet\n hangout_link: https://meet.google.com/example\n drive_items:\n - id: drv_docs\n user_email: [email protected]\n name: Docs\n mime_type: application/vnd.google-apps.folder\n parent_ids: [root]\n - id: drv_readme\n user_email: [email protected]\n name: README.md\n mime_type: text/markdown\n parent_ids: [drv_docs]\n data: \"# Hello World\"","type":"text"}]},{"type":"paragraph","content":[{"text":"When no OAuth clients are configured, the emulator accepts any ","type":"text"},{"text":"client_id","type":"text","marks":[{"type":"code_inline"}]},{"text":". With clients configured, strict validation is enforced for ","type":"text"},{"text":"client_id","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"client_secret","type":"text","marks":[{"type":"code_inline"}]},{"text":", and ","type":"text"},{"text":"redirect_uri","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Hosted domain (hd) claim","type":"text"}]},{"type":"paragraph","content":[{"text":"Google Workspace accounts include an ","type":"text"},{"text":"hd","type":"text","marks":[{"type":"code_inline"}]},{"text":" 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 (","type":"text"},{"text":"gmail.com","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"googlemail.com","type":"text","marks":[{"type":"code_inline"}]},{"text":") omit the claim, matching real Google behavior.","type":"text"}]},{"type":"paragraph","content":[{"text":"To override the derived value, set ","type":"text"},{"text":"hd","type":"text","marks":[{"type":"code_inline"}]},{"text":" on a seeded user. To suppress the claim entirely, set ","type":"text"},{"text":"hd","type":"text","marks":[{"type":"code_inline"}]},{"text":" to an empty string.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"OAuth / OIDC Endpoints","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"OIDC Discovery","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"curl http://localhost:4002/.well-known/openid-configuration","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"JWKS","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"curl http://localhost:4002/oauth2/v3/certs","type":"text"}]},{"type":"paragraph","content":[{"text":"Returns ","type":"text"},{"text":"{ \"keys\": [] }","type":"text","marks":[{"type":"code_inline"}]},{"text":". ID tokens are signed with HS256 using an internal secret.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Authorization","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Browser flow: redirects to a user picker page\ncurl -v \"http://localhost:4002/o/oauth2/v2/auth?\\\nclient_id=my-client-id.apps.googleusercontent.com&\\\nredirect_uri=http://localhost:3000/api/auth/callback/google&\\\nscope=openid+email+profile&\\\nresponse_type=code&\\\nstate=random-state&\\\nnonce=random-nonce\"","type":"text"}]},{"type":"paragraph","content":[{"text":"Supports ","type":"text"},{"text":"code_challenge","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"code_challenge_method","type":"text","marks":[{"type":"code_inline"}]},{"text":" for PKCE.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Token Exchange","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"curl -X POST http://localhost:4002/oauth2/token \\\n -H \"Content-Type: application/x-www-form-urlencoded\" \\\n -d \"code=\u003cauthorization_code>&\\\nclient_id=my-client-id.apps.googleusercontent.com&\\\nclient_secret=GOCSPX-secret&\\\nredirect_uri=http://localhost:3000/api/auth/callback/google&\\\ngrant_type=authorization_code\"","type":"text"}]},{"type":"paragraph","content":[{"text":"Also accepts ","type":"text"},{"text":"application/json","type":"text","marks":[{"type":"code_inline"}]},{"text":" body. Returns:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"json"},"content":[{"text":"{\n \"access_token\": \"google_...\",\n \"refresh_token\": \"google_refresh_...\",\n \"id_token\": \"\u003cjwt>\",\n \"token_type\": \"Bearer\",\n \"expires_in\": 3600,\n \"scope\": \"openid email profile\"\n}","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Refresh Token","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"curl -X POST http://localhost:4002/oauth2/token \\\n -H \"Content-Type: application/x-www-form-urlencoded\" \\\n -d \"refresh_token=google_refresh_...&\\\nclient_id=my-client-id.apps.googleusercontent.com&\\\nclient_secret=GOCSPX-secret&\\\ngrant_type=refresh_token\"","type":"text"}]},{"type":"paragraph","content":[{"text":"Returns a new ","type":"text"},{"text":"access_token","type":"text","marks":[{"type":"code_inline"}]},{"text":" (no new ","type":"text"},{"text":"refresh_token","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"id_token","type":"text","marks":[{"type":"code_inline"}]},{"text":" on refresh).","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"User Info","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"curl http://localhost:4002/oauth2/v2/userinfo \\\n -H \"Authorization: Bearer google_...\"","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Token Revocation","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"curl -X POST http://localhost:4002/oauth2/revoke \\\n -H \"Content-Type: application/x-www-form-urlencoded\" \\\n -d \"token=google_...\"","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Gmail API","type":"text"}]},{"type":"paragraph","content":[{"text":"All Gmail endpoints are under ","type":"text"},{"text":"/gmail/v1/users/:userId/...","type":"text","marks":[{"type":"code_inline"}]},{"text":" where ","type":"text"},{"text":":userId","type":"text","marks":[{"type":"code_inline"}]},{"text":" is ","type":"text"},{"text":"me","type":"text","marks":[{"type":"code_inline"}]},{"text":" or the authenticated user's email.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Messages","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# List messages (filter by labels, search query)\ncurl \"http://localhost:4002/gmail/v1/users/me/messages?labelIds=INBOX&q=from:welcome&maxResults=10\" \\\n -H \"Authorization: Bearer $TOKEN\"\n\n# Get message (format: full, metadata, minimal, raw)\ncurl \"http://localhost:4002/gmail/v1/users/me/messages/msg_welcome?format=full\" \\\n -H \"Authorization: Bearer $TOKEN\"\n\n# Send message\ncurl -X POST http://localhost:4002/gmail/v1/users/me/messages/send \\\n -H \"Authorization: Bearer $TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"to\": \"[email protected]\", \"subject\": \"Hello\", \"body_text\": \"Hi there\"}'\n\n# Insert message (bypass send)\ncurl -X POST http://localhost:4002/gmail/v1/users/me/messages \\\n -H \"Authorization: Bearer $TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"to\": \"[email protected]\", \"from\": \"[email protected]\", \"subject\": \"Test\", \"body_text\": \"Body\", \"labelIds\": [\"INBOX\"]}'\n\n# Import message\ncurl -X POST http://localhost:4002/gmail/v1/users/me/messages/import \\\n -H \"Authorization: Bearer $TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"to\": \"[email protected]\", \"from\": \"[email protected]\", \"subject\": \"Imported\", \"body_text\": \"Content\"}'\n\n# Modify labels on a message\ncurl -X POST http://localhost:4002/gmail/v1/users/me/messages/msg_welcome/modify \\\n -H \"Authorization: Bearer $TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"addLabelIds\": [\"STARRED\"], \"removeLabelIds\": [\"UNREAD\"]}'\n\n# Trash / untrash\ncurl -X POST http://localhost:4002/gmail/v1/users/me/messages/msg_welcome/trash \\\n -H \"Authorization: Bearer $TOKEN\"\ncurl -X POST http://localhost:4002/gmail/v1/users/me/messages/msg_welcome/untrash \\\n -H \"Authorization: Bearer $TOKEN\"\n\n# Delete permanently\ncurl -X DELETE http://localhost:4002/gmail/v1/users/me/messages/msg_welcome \\\n -H \"Authorization: Bearer $TOKEN\"\n\n# Batch modify\ncurl -X POST http://localhost:4002/gmail/v1/users/me/messages/batchModify \\\n -H \"Authorization: Bearer $TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"ids\": [\"msg_welcome\", \"msg_build\"], \"addLabelIds\": [\"STARRED\"]}'\n\n# Batch delete\ncurl -X POST http://localhost:4002/gmail/v1/users/me/messages/batchDelete \\\n -H \"Authorization: Bearer $TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"ids\": [\"msg_welcome\"]}'\n\n# Get attachment\ncurl http://localhost:4002/gmail/v1/users/me/messages/msg_id/attachments/att_id \\\n -H \"Authorization: Bearer $TOKEN\"","type":"text"}]},{"type":"paragraph","content":[{"text":"Upload variants also available at ","type":"text"},{"text":"/upload/gmail/v1/users/:userId/messages","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":".../messages/send","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":".../messages/import","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Drafts","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# List drafts\ncurl http://localhost:4002/gmail/v1/users/me/drafts \\\n -H \"Authorization: Bearer $TOKEN\"\n\n# Create draft\ncurl -X POST http://localhost:4002/gmail/v1/users/me/drafts \\\n -H \"Authorization: Bearer $TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"message\": {\"to\": \"[email protected]\", \"subject\": \"Draft subject\", \"body_text\": \"Draft body\"}}'\n\n# Get draft (format: full, metadata, minimal, raw)\ncurl \"http://localhost:4002/gmail/v1/users/me/drafts/draft_id?format=full\" \\\n -H \"Authorization: Bearer $TOKEN\"\n\n# Update draft\ncurl -X PUT http://localhost:4002/gmail/v1/users/me/drafts/draft_id \\\n -H \"Authorization: Bearer $TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"message\": {\"subject\": \"Updated subject\", \"body_text\": \"Updated body\"}}'\n\n# Send draft\ncurl -X POST http://localhost:4002/gmail/v1/users/me/drafts/send \\\n -H \"Authorization: Bearer $TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"id\": \"draft_id\"}'\n\n# Delete draft\ncurl -X DELETE http://localhost:4002/gmail/v1/users/me/drafts/draft_id \\\n -H \"Authorization: Bearer $TOKEN\"","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Threads","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# List threads (filter by labels, search query)\ncurl \"http://localhost:4002/gmail/v1/users/me/threads?labelIds=INBOX&maxResults=20\" \\\n -H \"Authorization: Bearer $TOKEN\"\n\n# Get thread (all messages in thread)\ncurl \"http://localhost:4002/gmail/v1/users/me/threads/thr_welcome?format=full\" \\\n -H \"Authorization: Bearer $TOKEN\"\n\n# Modify labels on all messages in thread\ncurl -X POST http://localhost:4002/gmail/v1/users/me/threads/thr_welcome/modify \\\n -H \"Authorization: Bearer $TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"addLabelIds\": [\"STARRED\"], \"removeLabelIds\": [\"UNREAD\"]}'\n\n# Trash / untrash / delete thread\ncurl -X POST http://localhost:4002/gmail/v1/users/me/threads/thr_welcome/trash \\\n -H \"Authorization: Bearer $TOKEN\"\ncurl -X DELETE http://localhost:4002/gmail/v1/users/me/threads/thr_welcome \\\n -H \"Authorization: Bearer $TOKEN\"","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Labels","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# List labels\ncurl http://localhost:4002/gmail/v1/users/me/labels \\\n -H \"Authorization: Bearer $TOKEN\"\n\n# Get label\ncurl http://localhost:4002/gmail/v1/users/me/labels/INBOX \\\n -H \"Authorization: Bearer $TOKEN\"\n\n# Create label\ncurl -X POST http://localhost:4002/gmail/v1/users/me/labels \\\n -H \"Authorization: Bearer $TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"name\": \"My Label\", \"color\": {\"backgroundColor\": \"#DDEEFF\", \"textColor\": \"#111111\"}}'\n\n# Update label (PUT replaces, PATCH merges)\ncurl -X PATCH http://localhost:4002/gmail/v1/users/me/labels/Label_ops \\\n -H \"Authorization: Bearer $TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"name\": \"Ops/Reviewed\"}'\n\n# Delete label (user labels only)\ncurl -X DELETE http://localhost:4002/gmail/v1/users/me/labels/Label_ops \\\n -H \"Authorization: Bearer $TOKEN\"","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"History & Watch","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# List history changes since a given historyId\ncurl \"http://localhost:4002/gmail/v1/users/me/history?startHistoryId=1&historyTypes=messageAdded&maxResults=100\" \\\n -H \"Authorization: Bearer $TOKEN\"\n\n# Set up push notification watch (stub)\ncurl -X POST http://localhost:4002/gmail/v1/users/me/watch \\\n -H \"Authorization: Bearer $TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"topicName\": \"projects/my-project/topics/gmail\", \"labelIds\": [\"INBOX\"]}'\n\n# Stop watch\ncurl -X POST http://localhost:4002/gmail/v1/users/me/stop \\\n -H \"Authorization: Bearer $TOKEN\"","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Settings","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# List filters\ncurl http://localhost:4002/gmail/v1/users/me/settings/filters \\\n -H \"Authorization: Bearer $TOKEN\"\n\n# Create filter (auto-label incoming messages matching criteria)\ncurl -X POST http://localhost:4002/gmail/v1/users/me/settings/filters \\\n -H \"Authorization: Bearer $TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"criteria\": {\"from\": \"[email protected]\"}, \"action\": {\"addLabelIds\": [\"Label_ops\"]}}'\n\n# Delete filter\ncurl -X DELETE http://localhost:4002/gmail/v1/users/me/settings/filters/filter_id \\\n -H \"Authorization: Bearer $TOKEN\"\n\n# List forwarding addresses\ncurl http://localhost:4002/gmail/v1/users/me/settings/forwardingAddresses \\\n -H \"Authorization: Bearer $TOKEN\"\n\n# List send-as aliases\ncurl http://localhost:4002/gmail/v1/users/me/settings/sendAs \\\n -H \"Authorization: Bearer $TOKEN\"","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Google Calendar API","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Calendar List","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"curl http://localhost:4002/calendar/v3/users/me/calendarList \\\n -H \"Authorization: Bearer $TOKEN\"","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Events","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# List events (filter by time range, search, order)\ncurl \"http://localhost:4002/calendar/v3/calendars/primary/events?\\\ntimeMin=2025-01-01T00:00:00Z&timeMax=2025-12-31T23:59:59Z&maxResults=50&orderBy=startTime\" \\\n -H \"Authorization: Bearer $TOKEN\"\n\n# Create event\ncurl -X POST http://localhost:4002/calendar/v3/calendars/primary/events \\\n -H \"Authorization: Bearer $TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"summary\": \"Team Meeting\", \"start\": {\"dateTime\": \"2025-01-10T14:00:00Z\"}, \"end\": {\"dateTime\": \"2025-01-10T15:00:00Z\"}, \"attendees\": [{\"email\": \"[email protected]\"}]}'\n\n# Delete event\ncurl -X DELETE http://localhost:4002/calendar/v3/calendars/primary/events/evt_kickoff \\\n -H \"Authorization: Bearer $TOKEN\"","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"FreeBusy","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"curl -X POST http://localhost:4002/calendar/v3/freeBusy \\\n -H \"Authorization: Bearer $TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"timeMin\": \"2025-01-10T00:00:00Z\", \"timeMax\": \"2025-01-10T23:59:59Z\", \"items\": [{\"id\": \"primary\"}]}'","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Google Drive API","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Files","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# List files (with query filter, pagination, ordering)\ncurl \"http://localhost:4002/drive/v3/files?q='root'+in+parents&pageSize=20\" \\\n -H \"Authorization: Bearer $TOKEN\"\n\n# Create file (JSON metadata)\ncurl -X POST http://localhost:4002/drive/v3/files \\\n -H \"Authorization: Bearer $TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"name\": \"notes.txt\", \"mimeType\": \"text/plain\", \"parents\": [\"root\"]}'\n\n# Create file with content (multipart/related upload)\ncurl -X POST http://localhost:4002/upload/drive/v3/files \\\n -H \"Authorization: Bearer $TOKEN\" \\\n -H \"Content-Type: multipart/related; boundary=boundary\" \\\n --data-binary

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 Or programmatically: Pointing Your App at the Emulator Environment Variable OAuth URL Mapping | Real Google URL | Emulator URL | |-----------------|-------------| | | | | | | | | | | | | | | | | | | | | | | | | google-auth-library (Node.js) Auth.js / NextAuth.js Passport.js Seed Config When no OAuth clients are configured, the emulator accep…

--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--'\n\n# Get file metadata\ncurl http://localhost:4002/drive/v3/files/drv_readme \\\n -H \"Authorization: Bearer $TOKEN\"\n\n# Download file content\ncurl \"http://localhost:4002/drive/v3/files/drv_readme?alt=media\" \\\n -H \"Authorization: Bearer $TOKEN\"\n\n# Update file (PATCH or PUT; move parents with query params)\ncurl -X PATCH \"http://localhost:4002/drive/v3/files/drv_readme?addParents=folder_id&removeParents=root\" \\\n -H \"Authorization: Bearer $TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"name\": \"README-updated.md\"}'","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Common Patterns","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Full Authorization Code Flow","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"GOOGLE_URL=\"http://localhost:4002\"\nCLIENT_ID=\"my-client-id.apps.googleusercontent.com\"\nCLIENT_SECRET=\"GOCSPX-secret\"\nREDIRECT_URI=\"http://localhost:3000/api/auth/callback/google\"\n\n# 1. Open in browser (user picks a seeded account)\n# $GOOGLE_URL/o/oauth2/v2/auth?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&scope=openid+email+profile&response_type=code&state=abc\n\n# 2. After user selection, emulator redirects to:\n# $REDIRECT_URI?code=\u003ccode>&state=abc\n\n# 3. Exchange code for tokens\ncurl -X POST $GOOGLE_URL/oauth2/token \\\n -H \"Content-Type: application/x-www-form-urlencoded\" \\\n -d \"code=\u003ccode>&client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET&redirect_uri=$REDIRECT_URI&grant_type=authorization_code\"\n\n# 4. Fetch user info with the access_token\ncurl $GOOGLE_URL/oauth2/v2/userinfo \\\n -H \"Authorization: Bearer \u003caccess_token>\"","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"OIDC Discovery-Based Setup","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"typescript"},"content":[{"text":"import { Issuer } from 'openid-client'\n\nconst googleIssuer = await Issuer.discover(\n process.env.GOOGLE_EMULATOR_URL ?? 'https://accounts.google.com'\n)\n\nconst client = new googleIssuer.Client({\n client_id: process.env.GOOGLE_CLIENT_ID,\n client_secret: process.env.GOOGLE_CLIENT_SECRET,\n redirect_uris: ['http://localhost:3000/api/auth/callback/google'],\n})","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Send a Gmail Message and Check the Thread","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"TOKEN=\"test_token_admin\"\nBASE=\"http://localhost:4002\"\n\n# Send a message\ncurl -X POST $BASE/gmail/v1/users/me/messages/send \\\n -H \"Authorization: Bearer $TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"to\": \"[email protected]\", \"subject\": \"Test\", \"body_text\": \"Hello\"}'\n\n# List threads in INBOX\ncurl \"$BASE/gmail/v1/users/me/threads?labelIds=INBOX\" \\\n -H \"Authorization: Bearer $TOKEN\"","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}}]},"metadata":{"date":"2026-06-05","name":"google","author":"@skillopedia","source":{"stars":1322,"repo_name":"emulate","origin_url":"https://github.com/vercel-labs/emulate/blob/HEAD/skills/google/SKILL.md","repo_owner":"vercel-labs","body_sha256":"ea48f630239665af9c57eea97532d4bc6b5d77bc027d842c1b23819a448daea7","cluster_key":"eaacf582392bb49e91342fcb09fb7591f5444164cc1a0ecdc2729d959bacb9c7","clean_bundle":{"format":"clean-skill-bundle-v1","source":"vercel-labs/emulate/skills/google/SKILL.md","bundle_sha256":"12d28cfb05bb1b13741d93b6163698196755a1b93d1eee95c00d18a92532038c","attachment_count":0,"text_attachments":0,"binary_attachments":0},"cluster_size":1,"skill_md_path":"skills/google/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"testing-qa","category_label":"Testing"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"testing-qa","import_tag":"clean-skills-v1","description":"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 locally, emulate OIDC discovery, handle Google token exchange, configure Google OAuth clients, work with Gmail messages/drafts/threads/labels, manage Calendar events, upload or list Drive files, or work with Google userinfo without hitting real Google APIs. Triggers include \"Google OAuth\", \"emulate Google\", \"mock Google login\", \"test Google sign-in\", \"OIDC emulator\", \"Google OIDC\", \"Gmail API\", \"Google Calendar\", \"Google Drive\", \"local Google auth\", or any task requiring a local Google API.","allowed-tools":"Bash(npx emulate:*), Bash(emulate:*), Bash(curl:*)"}},"renderedAt":1782986498451}

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 Or programmatically: Pointing Your App at the Emulator Environment Variable OAuth URL Mapping | Real Google URL | Emulator URL | |-----------------|-------------| | | | | | | | | | | | | | | | | | | | | | | | | google-auth-library (Node.js) Auth.js / NextAuth.js Passport.js Seed Config When no OAuth clients are configured, the emulator accep…