Artifacts Platform
Agent DocsAppschatgpt-app

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

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

  1. Authorization Server: Clerk (handles OAuth flow, token issuance)
  2. Resource Server: Our Next.js MCP server (validates tokens, executes tools)
  3. 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.userId

Implementation 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 by auth() from @clerk/nextjs/server
  • CLERK_SECRET_KEY - Used for JWT validation via verifyClerkToken()

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.userId in tool handlers
  • Dynamic per-tool authentication via protectedTools map

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:

  1. Clerk JWT token passed to tool handler via context.authInfo
  2. resolveAuthContext() extracts userId from JWT
  3. Supabase client created with service role key (bypasses RLS)
  4. 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.

References

On this page