Authentication
OAuth authentication implementation with Clerk for ChatGPT MCP tools
OAuth Authentication Setup
This document explains how OAuth authentication is implemented in the ChatGPT app using Clerk.
Table of Contents
- Overview - Authentication architecture and flow
- Architecture - Components and authentication flow
- Implementation Details - Dependencies, env vars, server-side integration, endpoints, handlers, CORS
- Security Features - Token validation and error handling
- Dynamic Per-Tool Authentication - Protected tools configuration
- Database Integration - Supabase service role with app-layer filtering
- References - External documentation links
Overview
The MCP server in apps/chatgpt-app/ uses Clerk OAuth to authenticate users before executing tools. This follows the MCP authorization spec and ChatGPT Apps SDK requirements.
Architecture
Components
- Authorization Server: Clerk (handles OAuth flow, token issuance)
- Resource Server: Our Next.js MCP server (validates tokens, executes tools)
- Client: ChatGPT (initiates OAuth flow, attaches tokens to requests)
Authentication Flow
1. ChatGPT queries /.well-known/oauth-protected-resource/mcp
└─> Returns Clerk authorization server URL and required scopes
2. ChatGPT registers with Clerk (dynamic client registration)
└─> Obtains client_id for this connector
3. User invokes a tool in ChatGPT
└─> ChatGPT initiates OAuth authorization code + PKCE flow
4. User authenticates via Clerk OAuth UI
└─> User grants consent for requested scopes
5. ChatGPT exchanges authorization code for access token
└─> Receives JWT signed by Clerk
6. ChatGPT calls MCP tool with Authorization: Bearer <token>
└─> withMcpAuth() validates token via verifyClerkToken()
└─> Tool receives authInfo.extra.userIdImplementation Details
1. Dependencies
{
"@clerk/nextjs": "^6.33.4",
"@clerk/mcp-tools": "^0.3.1"
}2. Environment Variables
Required Clerk credentials (read by app/mcp/route.ts):
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY- Used byauth()from@clerk/nextjs/serverCLERK_SECRET_KEY- Used for JWT validation viaverifyClerkToken()
See .env.example for format.
3. Server-Side Clerk Integration
Clerk is integrated server-side only via @clerk/mcp-tools/next and @clerk/nextjs/server. No client-side ClerkProvider is needed since all authentication happens through MCP tool calls.
4. OAuth Metadata Endpoint
The route app/.well-known/oauth-protected-resource/mcp/route.ts uses protectedResourceHandlerClerk() from @clerk/mcp-tools/next to expose OAuth discovery metadata including the resource URL, authorization server, and required scopes.
5. MCP Handler with Authentication
The MCP handler in app/mcp/route.ts wraps tools with withMcpAuth() to verify Clerk tokens.
Key points:
- Uses
verifyClerkToken()from@clerk/mcp-tools/next - Token verified via
auth({ acceptsToken: 'oauth_token' }) - User ID extracted from
authInfo.extra.userIdin tool handlers - Dynamic per-tool authentication via
protectedToolsmap
See app/mcp/route.ts and mcp-server.md for complete details.
6. CORS Configuration
CORS middleware adds appropriate headers to all routes. The middleware.ts uses clerkMiddleware() only for /mcp routes while adding CORS headers (Access-Control-Allow-Origin: *) to all other routes including OAuth discovery endpoints.
Security Features
Token Validation
verifyClerkToken() validates:
- ✅ JWT signature (using Clerk's public keys)
- ✅ Token expiration
- ✅ Issuer (Clerk authorization server)
- ✅ Audience (your application)
- ✅ Scopes (required permissions)
Error Handling
Unauthenticated requests return:
401 Unauthorized
WWW-Authenticate: Bearer realm="/.well-known/oauth-protected-resource/mcp"This tells ChatGPT to initiate the OAuth flow.
Dynamic Per-Tool Authentication
The MCP route handler uses dynamic authentication routing:
const protectedTools = {
'artifact.list': ['profile'],
'artifact.view': ['profile'],
'artifact.create': ['profile'],
}Tools listed in protectedTools require OAuth. The handler dynamically wraps only those tools with authentication.
Implementation: See app/mcp/route.ts for the dynamic routing logic using withMcpAuth().
Database Integration
Tools persist user data to Supabase:
- Clerk JWT token passed to tool handler via
context.authInfo resolveAuthContext()extractsuserIdfrom JWT- Supabase client created with service role key (bypasses RLS)
- Application layer enforces user isolation by filtering queries with
.eq('user_id', userId)
Implementation: See app/mcp/lib/shared.ts for resolveAuthContext() and packages/artifact-service/src/artifact-store.ts for query filtering.