Project 6: OAuth-Protected Integration App

Project 6: OAuth-Protected Integration App

Build a GitHub-connected ChatGPT app that uses OAuth 2.1 to access user repositories, list issues, and create pull requestsโ€”mastering the complete authentication flow required for real-world integrations.


Table of Contents

  1. Project Overview
  2. Theoretical Foundation
  3. Solution Architecture
  4. Step-by-Step Implementation Guide
  5. Testing & Validation
  6. Common Pitfalls & Debugging
  7. Performance Optimization
  8. Security Considerations
  9. Production Deployment
  10. Variations & Extensions
  11. Additional Resources
  12. Exercises & Challenges
  13. Summary & Next Steps

1. Project Overview

What Youโ€™ll Build

A production-grade ChatGPT app that integrates with GitHub using OAuth 2.1 authentication. Users will be able to:

  • Connect their GitHub account to ChatGPT
  • View their repositories with details
  • List and search issues
  • Create new issues
  • Generate pull requests

Live Example:

User: "Show my GitHub repositories"

ChatGPT: "To access your GitHub repositories, you'll need to connect your account."

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  ๐Ÿ”— Connect to GitHub                                   โ”‚
โ”‚                                                         โ”‚
โ”‚  This app needs access to:                             โ”‚
โ”‚  โ€ข View your repositories                              โ”‚
โ”‚  โ€ข Read and write issues                               โ”‚
โ”‚  โ€ข Create pull requests                                โ”‚
โ”‚                                                         โ”‚
โ”‚              [Connect GitHub Account]                   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

[After OAuth flow completes...]

User: "Show my GitHub repositories"

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  ๐Ÿ“ฆ Your Repositories (12)                              โ”‚
โ”‚                                                         โ”‚
โ”‚  โญ my-awesome-project                                  โ”‚
โ”‚     TypeScript ยท Updated 2 hours ago ยท 3 open issues    โ”‚
โ”‚                                                         โ”‚
โ”‚  ๐Ÿ“ dotfiles                                            โ”‚
โ”‚     Shell ยท Updated 1 week ago ยท 0 issues               โ”‚
โ”‚                                                         โ”‚
โ”‚  [View on GitHub] [Show Issues] [Create Issue]          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Project Metadata

  • File: LEARN_CHATGPT_APPS_DEEP_DIVE.md
  • Main Programming Language: TypeScript/Node.js
  • Alternative Programming Languages: Python (FastAPI)
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 4. The โ€œOpen Coreโ€ Infrastructure
  • Difficulty: Level 3: Advanced
  • Knowledge Area: OAuth 2.1 / Authentication / Security
  • Software or Tool: Passport.js, Auth0, or custom OAuth server
  • Main Book: โ€œOAuth 2 in Actionโ€ by Justin Richer & Antonio Sanso

Learning Objectives

By completing this project, you will:

  1. Master OAuth 2.1: Understand the complete authorization flow with PKCE
  2. Implement Secure Authentication: Build production-ready token validation
  3. Handle Identity Providers: Integrate with GitHub, Auth0, and custom OAuth servers
  4. Manage Token Lifecycle: Implement token refresh, expiration, and revocation
  5. Build Well-Known Endpoints: Expose OAuth metadata for discovery
  6. Implement DCR: Support Dynamic Client Registration
  7. Secure MCP Tools: Protect API endpoints with proper authentication
  8. Handle Auth Failures: Implement WWW-Authenticate responses
  9. Store Tokens Securely: Manage sensitive credentials
  10. Debug OAuth Flows: Troubleshoot common authentication issues

Prerequisites

  • Completion of Projects 1-5
  • Understanding of HTTP and REST APIs
  • Basic knowledge of JWT (JSON Web Tokens)
  • Familiarity with public/private key cryptography
  • Experience with environment variables and secrets management
  • Node.js/TypeScript or Python/FastAPI proficiency

Time Estimate

2-3 weeks for full implementation:

  • Week 1: OAuth server setup, metadata endpoints, basic flow
  • Week 2: Token validation, MCP tool protection, error handling
  • Week 3: Integration testing, security hardening, production prep

2. Theoretical Foundation

2.1 OAuth 2.1: The Modern Authorization Standard

OAuth 2.1 is the updated OAuth specification that consolidates best practices from OAuth 2.0, including mandatory PKCE and removing implicit flow.

Why OAuth 2.1 for ChatGPT Apps?

  1. User Authorization, Not Authentication: OAuth delegates access without sharing passwords
  2. Scoped Permissions: Users grant specific capabilities (read repos, write issues)
  3. Token-Based: Short-lived access tokens with refresh capabilities
  4. Standard Protocol: Works across platforms and identity providers
  5. Security Best Practices: PKCE prevents authorization code interception

OAuth 2.1 vs OAuth 2.0 Key Differences:

OAuth 2.0 โ†’ OAuth 2.1 Changes:
โ”œโ”€โ”€ PKCE is now REQUIRED (was optional)
โ”œโ”€โ”€ Implicit flow REMOVED (insecure)
โ”œโ”€โ”€ Resource Owner Password flow REMOVED (anti-pattern)
โ”œโ”€โ”€ Bearer token usage clarified
โ””โ”€โ”€ Refresh token rotation recommended

2.2 The OAuth 2.1 Authorization Flow

The complete flow has multiple steps involving the user, ChatGPT, your auth server, and your MCP server:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                     OAuth 2.1 Flow with PKCE                        โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                     โ”‚
โ”‚  1. User requests protected resource in ChatGPT                     โ”‚
โ”‚     "Show my GitHub repositories"                                   โ”‚
โ”‚                                                                     โ”‚
โ”‚  2. ChatGPT โ†’ MCP Server: Calls list_repositories tool             โ”‚
โ”‚                                                                     โ”‚
โ”‚  3. MCP Server โ†’ ChatGPT: Returns 401 with WWW-Authenticate         โ”‚
โ”‚     {                                                               โ”‚
โ”‚       "_meta": {                                                    โ”‚
โ”‚         "mcp/www_authenticate": {                                   โ”‚
โ”‚           "scheme": "Bearer",                                       โ”‚
โ”‚           "realm": "github-integration"                             โ”‚
โ”‚         }                                                           โ”‚
โ”‚       }                                                             โ”‚
โ”‚     }                                                               โ”‚
โ”‚                                                                     โ”‚
โ”‚  4. ChatGPT discovers auth server from well-known endpoint          โ”‚
โ”‚     GET /.well-known/oauth-protected-resource                       โ”‚
โ”‚     Response: { "authorization_servers": ["https://auth.app.com"] } โ”‚
โ”‚                                                                     โ”‚
โ”‚  5. ChatGPT generates PKCE parameters:                              โ”‚
โ”‚     code_verifier = random_string(43-128 chars)                     โ”‚
โ”‚     code_challenge = base64url(sha256(code_verifier))               โ”‚
โ”‚     code_challenge_method = "S256"                                  โ”‚
โ”‚                                                                     โ”‚
โ”‚  6. ChatGPT โ†’ Auth Server: Authorization request                    โ”‚
โ”‚     https://auth.app.com/authorize?                                 โ”‚
โ”‚       response_type=code&                                           โ”‚
โ”‚       client_id=CHATGPT_CLIENT_ID&                                  โ”‚
โ”‚       redirect_uri=https://chatgpt.com/oauth/callback&              โ”‚
โ”‚       scope=read:repos write:issues&                                โ”‚
โ”‚       state=RANDOM_STATE&                                           โ”‚
โ”‚       code_challenge=CHALLENGE&                                     โ”‚
โ”‚       code_challenge_method=S256                                    โ”‚
โ”‚                                                                     โ”‚
โ”‚  7. User โ†’ Auth Server: User logs in and grants permissions         โ”‚
โ”‚                                                                     โ”‚
โ”‚  8. Auth Server โ†’ ChatGPT: Redirect with authorization code         โ”‚
โ”‚     https://chatgpt.com/oauth/callback?                             โ”‚
โ”‚       code=AUTH_CODE&                                               โ”‚
โ”‚       state=RANDOM_STATE                                            โ”‚
โ”‚                                                                     โ”‚
โ”‚  9. ChatGPT โ†’ Auth Server: Token exchange with code_verifier        โ”‚
โ”‚     POST /token                                                     โ”‚
โ”‚     {                                                               โ”‚
โ”‚       "grant_type": "authorization_code",                           โ”‚
โ”‚       "code": "AUTH_CODE",                                          โ”‚
โ”‚       "redirect_uri": "https://chatgpt.com/oauth/callback",         โ”‚
โ”‚       "client_id": "CHATGPT_CLIENT_ID",                             โ”‚
โ”‚       "code_verifier": "ORIGINAL_VERIFIER"                          โ”‚
โ”‚     }                                                               โ”‚
โ”‚                                                                     โ”‚
โ”‚  10. Auth Server validates code_verifier:                           โ”‚
โ”‚      base64url(sha256(code_verifier)) == stored_code_challenge      โ”‚
โ”‚                                                                     โ”‚
โ”‚  11. Auth Server โ†’ ChatGPT: Access and refresh tokens               โ”‚
โ”‚      {                                                              โ”‚
โ”‚        "access_token": "eyJhbG...",                                 โ”‚
โ”‚        "token_type": "Bearer",                                      โ”‚
โ”‚        "expires_in": 3600,                                          โ”‚
โ”‚        "refresh_token": "eyJhbG...",                                โ”‚
โ”‚        "scope": "read:repos write:issues"                           โ”‚
โ”‚      }                                                              โ”‚
โ”‚                                                                     โ”‚
โ”‚  12. ChatGPT โ†’ MCP Server: Retry request with token                 โ”‚
โ”‚      Authorization: Bearer eyJhbG...                                โ”‚
โ”‚                                                                     โ”‚
โ”‚  13. MCP Server validates token and returns data                    โ”‚
โ”‚                                                                     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

2.3 PKCE (Proof Key for Code Exchange)

PKCE prevents authorization code interception attacks, especially important for public clients like ChatGPT.

How PKCE Works:

  1. Code Verifier Generation:
    // Generate random string (43-128 characters)
    const codeVerifier = crypto.randomBytes(64).toString('base64url');
    // Example: "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
    
  2. Code Challenge Creation:
    // SHA-256 hash of verifier, then base64url encode
    const hash = crypto.createHash('sha256').update(codeVerifier).digest();
    const codeChallenge = hash.toString('base64url');
    // Example: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
    
  3. Authorization Request (includes challenge):
    GET /authorize?
      response_type=code&
      code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
      code_challenge_method=S256&
      ...
    
  4. Token Exchange (includes verifier):
    POST /token
    {
      "grant_type": "authorization_code",
      "code": "AUTH_CODE",
      "code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
    }
    
  5. Server Verification:
    // Server checks: SHA256(code_verifier) == stored_code_challenge
    const hash = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
    if (hash !== storedCodeChallenge) {
      throw new Error('Invalid code_verifier');
    }
    

Why PKCE is Critical:

WITHOUT PKCE:
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Attacker intercepts authorization code                   โ”‚
โ”‚ โ†“                                                        โ”‚
โ”‚ Attacker exchanges code for token (no verification)      โ”‚
โ”‚ โ†“                                                        โ”‚
โ”‚ Attacker gains access to user's account                  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

WITH PKCE:
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Attacker intercepts authorization code                   โ”‚
โ”‚ โ†“                                                        โ”‚
โ”‚ Attacker tries to exchange code for token                โ”‚
โ”‚ โ†“                                                        โ”‚
โ”‚ Server requires code_verifier (attacker doesn't have it) โ”‚
โ”‚ โ†“                                                        โ”‚
โ”‚ Token exchange FAILS - user protected                    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

2.4 JWT (JSON Web Tokens)

Access tokens in OAuth 2.1 are typically JWTs, which are self-contained and verifiable.

JWT Structure:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc4TSMb4bXP3l3YlNWACwyXPGffz5aXHc6lty1Y2t4SWRqGteragsVdZufDn5BlnJl9pdR_kdVFUsra2rWKEofkZeIC4yWytE58sMIihvo9H1ScmmVwBcQP6XETqYd0aSHp1gOa9RdUPDvoXQ5oqygTqVtxaDr6wUFKrKItgBMzWIdNZ6y7O9E0DhEPTbE9rfBo6KTFsHAZnMg4k68CDp2woYIaXbmYTWcvbzIuHO7_37GT79XdIwkm95QJ7hYC9RiwrV7mesbY4PAahERJawntho0my942XheVLmGwLMBkQ

โ”œโ”€โ”€ Header (Algorithm & Token Type)
โ”‚   {
โ”‚     "alg": "RS256",    // RSA with SHA-256
โ”‚     "typ": "JWT"       // Token type
โ”‚   }
โ”‚
โ”œโ”€โ”€ Payload (Claims)
โ”‚   {
โ”‚     "sub": "user_123",              // Subject (user ID)
โ”‚     "iss": "https://auth.app.com",  // Issuer
โ”‚     "aud": "https://mcp.app.com",   // Audience (your MCP server)
โ”‚     "exp": 1735689600,              // Expiration timestamp
โ”‚     "iat": 1735686000,              // Issued at
โ”‚     "scope": "read:repos write:issues",
โ”‚     "github_token": "gho_xxx"       // Custom claim
โ”‚   }
โ”‚
โ””โ”€โ”€ Signature (Verification)
    RSASHA256(
      base64urlEncode(header) + "." + base64urlEncode(payload),
      privateKey
    )

JWT Validation Steps:

function validateJWT(token: string) {
  // 1. Decode without verification
  const decoded = jwt.decode(token, { complete: true });

  // 2. Get public key (from JWKS endpoint)
  const publicKey = await getPublicKey(decoded.header.kid);

  // 3. Verify signature
  const verified = jwt.verify(token, publicKey, {
    algorithms: ['RS256'],
    issuer: 'https://auth.app.com',
    audience: 'https://mcp.app.com'
  });

  // 4. Check expiration
  if (verified.exp < Date.now() / 1000) {
    throw new Error('Token expired');
  }

  // 5. Check scopes
  const scopes = verified.scope.split(' ');
  if (!scopes.includes('read:repos')) {
    throw new Error('Insufficient scope');
  }

  return verified;
}

2.5 Dynamic Client Registration (DCR)

DCR allows ChatGPT to register itself as an OAuth client dynamically, without manual configuration.

DCR Flow:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  1. ChatGPT discovers auth server from well-known       โ”‚
โ”‚     GET /.well-known/oauth-protected-resource           โ”‚
โ”‚                                                         โ”‚
โ”‚  2. Server returns registration endpoint                โ”‚
โ”‚     {                                                   โ”‚
โ”‚       "authorization_servers": ["https://auth.app.com"],โ”‚
โ”‚       "registration_endpoint":                          โ”‚
โ”‚         "https://auth.app.com/oauth/register"           โ”‚
โ”‚     }                                                   โ”‚
โ”‚                                                         โ”‚
โ”‚  3. ChatGPT registers as a client                       โ”‚
โ”‚     POST https://auth.app.com/oauth/register            โ”‚
โ”‚     {                                                   โ”‚
โ”‚       "client_name": "ChatGPT",                         โ”‚
โ”‚       "redirect_uris": [                                โ”‚
โ”‚         "https://chatgpt.com/oauth/callback"            โ”‚
โ”‚       ],                                                โ”‚
โ”‚       "token_endpoint_auth_method": "none",             โ”‚
โ”‚       "grant_types": ["authorization_code", "refresh"], โ”‚
โ”‚       "response_types": ["code"],                       โ”‚
โ”‚       "scope": "read:repos write:issues read:user"      โ”‚
โ”‚     }                                                   โ”‚
โ”‚                                                         โ”‚
โ”‚  4. Server creates client and returns credentials       โ”‚
โ”‚     {                                                   โ”‚
โ”‚       "client_id": "chatgpt_abc123",                    โ”‚
โ”‚       "client_secret": null,  // Public client          โ”‚
โ”‚       "redirect_uris": [...],                           โ”‚
โ”‚       "token_endpoint_auth_method": "none"              โ”‚
โ”‚     }                                                   โ”‚
โ”‚                                                         โ”‚
โ”‚  5. ChatGPT stores client_id for future auth flows      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

DCR Implementation:

// Server-side DCR endpoint
app.post('/oauth/register', async (req, res) => {
  const registration = req.body;

  // Validate registration request
  validateRegistrationRequest(registration);

  // Generate client_id
  const clientId = `chatgpt_${crypto.randomUUID()}`;

  // For public clients (ChatGPT), no client_secret
  const client = {
    client_id: clientId,
    client_name: registration.client_name,
    redirect_uris: registration.redirect_uris,
    token_endpoint_auth_method: 'none',  // Public client
    grant_types: registration.grant_types,
    response_types: registration.response_types,
    scope: registration.scope
  };

  // Store client in database
  await db.clients.create(client);

  res.json(client);
});

2.6 Token Lifecycle Management

Token States:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    Token Lifecycle                      โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                         โ”‚
โ”‚  1. ISSUED                                              โ”‚
โ”‚     Access token: expires in 1 hour                     โ”‚
โ”‚     Refresh token: expires in 90 days                   โ”‚
โ”‚                                                         โ”‚
โ”‚  2. ACTIVE                                              โ”‚
โ”‚     Token is valid and can be used                      โ”‚
โ”‚     Server validates on every request                   โ”‚
โ”‚                                                         โ”‚
โ”‚  3. EXPIRED                                             โ”‚
โ”‚     Access token expired (exp claim exceeded)           โ”‚
โ”‚     Must use refresh token to get new access token      โ”‚
โ”‚                                                         โ”‚
โ”‚  4. REFRESHED                                           โ”‚
โ”‚     POST /token with grant_type=refresh_token           โ”‚
โ”‚     Old refresh token rotated (invalidated)             โ”‚
โ”‚     New access + refresh tokens issued                  โ”‚
โ”‚                                                         โ”‚
โ”‚  5. REVOKED                                             โ”‚
โ”‚     User disconnects app or token manually revoked      โ”‚
โ”‚     All tokens (access + refresh) invalidated           โ”‚
โ”‚                                                         โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Refresh Token Flow:

// When access token expires, ChatGPT automatically refreshes
async function refreshAccessToken(refreshToken: string) {
  const response = await fetch('https://auth.app.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'refresh_token',
      refresh_token: refreshToken,
      client_id: 'chatgpt_abc123'
    })
  });

  const tokens = await response.json();
  // {
  //   "access_token": "new_access_token",
  //   "refresh_token": "new_refresh_token",  // Token rotation
  //   "expires_in": 3600
  // }

  return tokens;
}

2.7 Well-Known Endpoints

OAuth discovery uses standardized .well-known endpoints:

Resource Server Metadata (/.well-known/oauth-protected-resource):

{
  "resource": "https://mcp.app.com",
  "authorization_servers": [
    "https://auth.app.com"
  ],
  "bearer_methods_supported": ["header"],
  "scopes_supported": [
    "read:repos",
    "write:issues",
    "read:user"
  ]
}

Authorization Server Metadata (/.well-known/oauth-authorization-server):

{
  "issuer": "https://auth.app.com",
  "authorization_endpoint": "https://auth.app.com/oauth/authorize",
  "token_endpoint": "https://auth.app.com/oauth/token",
  "registration_endpoint": "https://auth.app.com/oauth/register",
  "jwks_uri": "https://auth.app.com/.well-known/jwks.json",
  "scopes_supported": ["read:repos", "write:issues", "read:user"],
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "token_endpoint_auth_methods_supported": ["none", "client_secret_post"],
  "code_challenge_methods_supported": ["S256"]
}

JWKS (JSON Web Key Set) (/.well-known/jwks.json):

{
  "keys": [
    {
      "kty": "RSA",
      "use": "sig",
      "kid": "key-2025-01",
      "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx...",
      "e": "AQAB"
    }
  ]
}

2.8 Security Best Practices

Critical Security Considerations:

  1. Always Validate Tokens:
    // WRONG - Trusting token without validation
    const payload = JSON.parse(atob(token.split('.')[1]));
    
    // RIGHT - Full JWT verification
    const payload = jwt.verify(token, publicKey, options);
    
  2. Use HTTPS Everywhere:
    • All OAuth endpoints MUST use HTTPS
    • Tokens transmitted over HTTPS only
    • No mixed content (HTTP + HTTPS)
  3. Rotate Refresh Tokens:
    // Invalidate old refresh token when issuing new one
    await db.refreshTokens.delete(oldRefreshToken);
    await db.refreshTokens.create(newRefreshToken);
    
  4. Store Tokens Securely:
    • Use encryption at rest for stored tokens
    • Never log tokens in plaintext
    • Use environment variables for secrets
  5. Implement Token Revocation:
    // Revoke endpoint
    app.post('/oauth/revoke', async (req, res) => {
      const { token } = req.body;
      await db.tokens.delete({ token });
      res.status(200).send();
    });
    
  6. Validate Redirect URIs:
    // Exact match only - no pattern matching
    if (redirectUri !== client.redirect_uris.find(uri => uri === redirectUri)) {
      throw new Error('Invalid redirect_uri');
    }
    
  7. Implement Rate Limiting:
    // Prevent brute force attacks
    const limiter = rateLimit({
      windowMs: 15 * 60 * 1000, // 15 minutes
      max: 5, // 5 requests per window
      handler: (req, res) => {
        res.status(429).json({ error: 'too_many_requests' });
      }
    });
    
    app.use('/oauth/token', limiter);
    

3. Solution Architecture

3.1 High-Level Architecture

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                   OAuth-Protected ChatGPT App                       โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                     โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚  โ”‚   ChatGPT    โ”‚โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค   Widget     โ”‚โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค   User      โ”‚ โ”‚
โ”‚  โ”‚              โ”‚  iframe โ”‚ (React/TS)   โ”‚  clicks โ”‚             โ”‚ โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”‚         โ”‚                                                           โ”‚
โ”‚         โ”‚ HTTP + Bearer Token                                       โ”‚
โ”‚         โ–ผ                                                           โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
โ”‚  โ”‚              MCP Server (Protected Resource)                โ”‚   โ”‚
โ”‚  โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚   โ”‚
โ”‚  โ”‚  โ”‚ Token Validation Middleware                          โ”‚   โ”‚   โ”‚
โ”‚  โ”‚  โ”‚  1. Extract Bearer token                             โ”‚   โ”‚   โ”‚
โ”‚  โ”‚  โ”‚  2. Verify JWT signature with public key             โ”‚   โ”‚   โ”‚
โ”‚  โ”‚  โ”‚  3. Check expiration, issuer, audience               โ”‚   โ”‚   โ”‚
โ”‚  โ”‚  โ”‚  4. Validate scopes                                  โ”‚   โ”‚   โ”‚
โ”‚  โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚   โ”‚
โ”‚  โ”‚                                                             โ”‚   โ”‚
โ”‚  โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚   โ”‚
โ”‚  โ”‚  โ”‚ Protected MCP Tools                                  โ”‚   โ”‚   โ”‚
โ”‚  โ”‚  โ”‚  โ€ข list_repositories()                               โ”‚   โ”‚   โ”‚
โ”‚  โ”‚  โ”‚  โ€ข get_repository_issues(repo_id)                    โ”‚   โ”‚   โ”‚
โ”‚  โ”‚  โ”‚  โ€ข create_issue(repo_id, title, body)                โ”‚   โ”‚   โ”‚
โ”‚  โ”‚  โ”‚  โ€ข create_pull_request(repo_id, branch, title)       โ”‚   โ”‚   โ”‚
โ”‚  โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚   โ”‚
โ”‚  โ”‚                                                             โ”‚   โ”‚
โ”‚  โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚   โ”‚
โ”‚  โ”‚  โ”‚ OAuth Metadata Endpoint                              โ”‚   โ”‚   โ”‚
โ”‚  โ”‚  โ”‚  GET /.well-known/oauth-protected-resource           โ”‚   โ”‚   โ”‚
โ”‚  โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚   โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
โ”‚         โ”‚                                                           โ”‚
โ”‚         โ”‚ 401 Unauthorized โ†’ triggers OAuth flow                    โ”‚
โ”‚         โ–ผ                                                           โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
โ”‚  โ”‚              Authorization Server                           โ”‚   โ”‚
โ”‚  โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚   โ”‚
โ”‚  โ”‚  โ”‚ DCR Endpoint                                         โ”‚   โ”‚   โ”‚
โ”‚  โ”‚  โ”‚  POST /oauth/register                                โ”‚   โ”‚   โ”‚
โ”‚  โ”‚  โ”‚  โ†’ Returns client_id                                 โ”‚   โ”‚   โ”‚
โ”‚  โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚   โ”‚
โ”‚  โ”‚                                                             โ”‚   โ”‚
โ”‚  โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚   โ”‚
โ”‚  โ”‚  โ”‚ Authorization Endpoint                               โ”‚   โ”‚   โ”‚
โ”‚  โ”‚  โ”‚  GET /oauth/authorize                                โ”‚   โ”‚   โ”‚
โ”‚  โ”‚  โ”‚  โ†’ User login & consent UI                           โ”‚   โ”‚   โ”‚
โ”‚  โ”‚  โ”‚  โ†’ Store code_challenge (PKCE)                       โ”‚   โ”‚   โ”‚
โ”‚  โ”‚  โ”‚  โ†’ Redirect with authorization code                  โ”‚   โ”‚   โ”‚
โ”‚  โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚   โ”‚
โ”‚  โ”‚                                                             โ”‚   โ”‚
โ”‚  โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚   โ”‚
โ”‚  โ”‚  โ”‚ Token Endpoint                                       โ”‚   โ”‚   โ”‚
โ”‚  โ”‚  โ”‚  POST /oauth/token                                   โ”‚   โ”‚   โ”‚
โ”‚  โ”‚  โ”‚  โ†’ Validate code_verifier (PKCE)                     โ”‚   โ”‚   โ”‚
โ”‚  โ”‚  โ”‚  โ†’ Exchange code for access token                    โ”‚   โ”‚   โ”‚
โ”‚  โ”‚  โ”‚  โ†’ Return JWT + refresh token                        โ”‚   โ”‚   โ”‚
โ”‚  โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚   โ”‚
โ”‚  โ”‚                                                             โ”‚   โ”‚
โ”‚  โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚   โ”‚
โ”‚  โ”‚  โ”‚ JWKS Endpoint                                        โ”‚   โ”‚   โ”‚
โ”‚  โ”‚  โ”‚  GET /.well-known/jwks.json                          โ”‚   โ”‚   โ”‚
โ”‚  โ”‚  โ”‚  โ†’ Public keys for JWT verification                  โ”‚   โ”‚   โ”‚
โ”‚  โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚   โ”‚
โ”‚  โ”‚                                                             โ”‚   โ”‚
โ”‚  โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚   โ”‚
โ”‚  โ”‚  โ”‚ OAuth Metadata Endpoint                              โ”‚   โ”‚   โ”‚
โ”‚  โ”‚  โ”‚  GET /.well-known/oauth-authorization-server         โ”‚   โ”‚   โ”‚
โ”‚  โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚   โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
โ”‚         โ”‚                                                           โ”‚
โ”‚         โ”‚ Calls GitHub API with user's token                        โ”‚
โ”‚         โ–ผ                                                           โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
โ”‚  โ”‚              GitHub API                                     โ”‚   โ”‚
โ”‚  โ”‚  โ€ข GET /user/repos                                          โ”‚   โ”‚
โ”‚  โ”‚  โ€ข GET /repos/:owner/:repo/issues                           โ”‚   โ”‚
โ”‚  โ”‚  โ€ข POST /repos/:owner/:repo/issues                          โ”‚   โ”‚
โ”‚  โ”‚  โ€ข POST /repos/:owner/:repo/pulls                           โ”‚   โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
โ”‚                                                                     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

3.2 Component Breakdown

1. MCP Server (Resource Server):

  • Exposes protected tools that require authentication
  • Validates JWT on every request
  • Returns 401 with WWW-Authenticate when unauthenticated
  • Serves OAuth metadata for discovery

2. Authorization Server:

  • Handles user authentication and consent
  • Issues access and refresh tokens
  • Implements PKCE validation
  • Supports Dynamic Client Registration
  • Manages token lifecycle

3. Widget (UI):

  • Displays data from protected tools
  • Shows โ€œConnectโ€ button when unauthenticated
  • Handles OAuth callback (managed by ChatGPT)

3.3 Data Flow

First Request (Unauthenticated):

1. User: "Show my repos"
2. ChatGPT โ†’ MCP: GET /tools/list_repositories
   Headers: (no Authorization)

3. MCP โ†’ ChatGPT: 401 Unauthorized
   {
     "error": "unauthorized",
     "_meta": {
       "mcp/www_authenticate": {
         "scheme": "Bearer",
         "realm": "github-integration"
       }
     }
   }

4. ChatGPT โ†’ MCP: GET /.well-known/oauth-protected-resource

5. MCP โ†’ ChatGPT: OAuth metadata
   {
     "resource": "https://mcp.app.com",
     "authorization_servers": ["https://auth.app.com"]
   }

6. ChatGPT โ†’ User: Shows "Connect" button in widget

OAuth Authorization:

7. User clicks "Connect"

8. ChatGPT โ†’ Auth Server: GET /.well-known/oauth-authorization-server
   โ†’ Discovers endpoints

9. ChatGPT โ†’ Auth Server: POST /oauth/register (if DCR enabled)
   โ†’ Gets client_id

10. ChatGPT generates PKCE:
    code_verifier = random()
    code_challenge = SHA256(code_verifier)

11. ChatGPT โ†’ Auth Server: Redirect to /oauth/authorize
    ?response_type=code
    &client_id=chatgpt_abc123
    &redirect_uri=https://chatgpt.com/oauth/callback
    &scope=read:repos write:issues
    &state=xyz
    &code_challenge=ABC123
    &code_challenge_method=S256

12. User โ†’ Auth Server: Logs in with GitHub OAuth

13. Auth Server: Shows consent screen
    "ChatGPT wants to access:
     โ€ข View your repositories
     โ€ข Create and edit issues"

14. User: Clicks "Authorize"

15. Auth Server: Stores code_challenge

16. Auth Server โ†’ ChatGPT: Redirect to callback
    https://chatgpt.com/oauth/callback?code=ABC&state=xyz

17. ChatGPT โ†’ Auth Server: POST /oauth/token
    {
      "grant_type": "authorization_code",
      "code": "ABC",
      "redirect_uri": "https://chatgpt.com/oauth/callback",
      "client_id": "chatgpt_abc123",
      "code_verifier": "ORIGINAL_VERIFIER"
    }

18. Auth Server: Validates PKCE
    SHA256(code_verifier) == stored_code_challenge

19. Auth Server โ†’ ChatGPT: Access token
    {
      "access_token": "eyJhbG...",
      "token_type": "Bearer",
      "expires_in": 3600,
      "refresh_token": "eyJhbG...",
      "scope": "read:repos write:issues"
    }

Authenticated Request:

20. ChatGPT โ†’ MCP: GET /tools/list_repositories
    Headers: Authorization: Bearer eyJhbG...

21. MCP: Validates JWT
    - Verify signature with public key
    - Check expiration
    - Validate issuer, audience
    - Check scopes

22. MCP: Extracts github_token from JWT claims

23. MCP โ†’ GitHub API: GET /user/repos
    Headers: Authorization: Bearer gho_xxx

24. GitHub โ†’ MCP: Repository list

25. MCP โ†’ ChatGPT: Tool result + widget
    {
      "structuredContent": { "repositories": [...] },
      "uiComponent": { "type": "iframe", "url": "..." }
    }

26. ChatGPT: Renders widget with repository list

3.4 Database Schema

Clients Table (for DCR):

CREATE TABLE oauth_clients (
  client_id VARCHAR(255) PRIMARY KEY,
  client_name VARCHAR(255) NOT NULL,
  client_secret VARCHAR(255),  -- NULL for public clients
  redirect_uris TEXT[] NOT NULL,
  token_endpoint_auth_method VARCHAR(50) NOT NULL,
  grant_types TEXT[] NOT NULL,
  response_types TEXT[] NOT NULL,
  scope VARCHAR(500) NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Authorization Codes Table (temporary):

CREATE TABLE authorization_codes (
  code VARCHAR(255) PRIMARY KEY,
  client_id VARCHAR(255) NOT NULL REFERENCES oauth_clients(client_id),
  user_id VARCHAR(255) NOT NULL,
  redirect_uri VARCHAR(500) NOT NULL,
  scope VARCHAR(500) NOT NULL,
  code_challenge VARCHAR(255) NOT NULL,
  code_challenge_method VARCHAR(10) NOT NULL,
  expires_at TIMESTAMP NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_expires_at (expires_at)
);

Access Tokens Table:

CREATE TABLE access_tokens (
  token_id VARCHAR(255) PRIMARY KEY,
  client_id VARCHAR(255) NOT NULL REFERENCES oauth_clients(client_id),
  user_id VARCHAR(255) NOT NULL,
  scope VARCHAR(500) NOT NULL,
  github_token VARCHAR(500) NOT NULL,  -- Encrypted
  expires_at TIMESTAMP NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_user_id (user_id),
  INDEX idx_expires_at (expires_at)
);

Refresh Tokens Table:

CREATE TABLE refresh_tokens (
  token_id VARCHAR(255) PRIMARY KEY,
  client_id VARCHAR(255) NOT NULL REFERENCES oauth_clients(client_id),
  user_id VARCHAR(255) NOT NULL,
  scope VARCHAR(500) NOT NULL,
  github_refresh_token VARCHAR(500),  -- Encrypted
  expires_at TIMESTAMP NOT NULL,
  revoked BOOLEAN DEFAULT FALSE,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_user_id (user_id),
  INDEX idx_revoked (revoked)
);

Users Table:

CREATE TABLE users (
  user_id VARCHAR(255) PRIMARY KEY,
  github_user_id VARCHAR(255) UNIQUE NOT NULL,
  github_username VARCHAR(255) NOT NULL,
  email VARCHAR(255),
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

3.5 File Structure

oauth-protected-app/
โ”œโ”€โ”€ packages/
โ”‚   โ”œโ”€โ”€ auth-server/              # Authorization server
โ”‚   โ”‚   โ”œโ”€โ”€ src/
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ server.ts         # Express app
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ routes/
โ”‚   โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ authorize.ts  # GET /oauth/authorize
โ”‚   โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ token.ts      # POST /oauth/token
โ”‚   โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ register.ts   # POST /oauth/register
โ”‚   โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ jwks.ts       # GET /.well-known/jwks.json
โ”‚   โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ metadata.ts   # GET /.well-known/oauth-authorization-server
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ middleware/
โ”‚   โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ validatePKCE.ts
โ”‚   โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ rateLimit.ts
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ services/
โ”‚   โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ tokenService.ts
โ”‚   โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ githubService.ts
โ”‚   โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ cryptoService.ts
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ models/
โ”‚   โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ Client.ts
โ”‚   โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ AuthorizationCode.ts
โ”‚   โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ AccessToken.ts
โ”‚   โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ RefreshToken.ts
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ views/
โ”‚   โ”‚   โ”‚       โ”œโ”€โ”€ login.ejs
โ”‚   โ”‚   โ”‚       โ””โ”€โ”€ consent.ejs
โ”‚   โ”‚   โ”œโ”€โ”€ keys/
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ private.key       # RSA private key
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ public.key        # RSA public key
โ”‚   โ”‚   โ”œโ”€โ”€ package.json
โ”‚   โ”‚   โ””โ”€โ”€ tsconfig.json
โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€ mcp-server/               # MCP resource server
โ”‚   โ”‚   โ”œโ”€โ”€ src/
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ server.ts         # FastMCP server
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ middleware/
โ”‚   โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ validateToken.ts
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ tools/
โ”‚   โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ listRepositories.ts
โ”‚   โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ getIssues.ts
โ”‚   โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ createIssue.ts
โ”‚   โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ createPullRequest.ts
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ routes/
โ”‚   โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ wellKnown.ts  # /.well-known/oauth-protected-resource
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ services/
โ”‚   โ”‚   โ”‚       โ””โ”€โ”€ githubClient.ts
โ”‚   โ”‚   โ”œโ”€โ”€ package.json
โ”‚   โ”‚   โ””โ”€โ”€ tsconfig.json
โ”‚   โ”‚
โ”‚   โ””โ”€โ”€ widget/                   # UI widget
โ”‚       โ”œโ”€โ”€ src/
โ”‚       โ”‚   โ”œโ”€โ”€ App.tsx           # Main component
โ”‚       โ”‚   โ”œโ”€โ”€ components/
โ”‚       โ”‚   โ”‚   โ”œโ”€โ”€ ConnectButton.tsx
โ”‚       โ”‚   โ”‚   โ”œโ”€โ”€ RepositoryList.tsx
โ”‚       โ”‚   โ”‚   โ”œโ”€โ”€ IssueList.tsx
โ”‚       โ”‚   โ”‚   โ””โ”€โ”€ CreateIssueForm.tsx
โ”‚       โ”‚   โ””โ”€โ”€ main.tsx
โ”‚       โ”œโ”€โ”€ index.html
โ”‚       โ”œโ”€โ”€ vite.config.ts
โ”‚       โ”œโ”€โ”€ package.json
โ”‚       โ””โ”€โ”€ tsconfig.json
โ”‚
โ”œโ”€โ”€ docker-compose.yml            # Local development
โ”œโ”€โ”€ .env.example                  # Environment variables template
โ””โ”€โ”€ README.md

4. Step-by-Step Implementation Guide

Step 1: Project Setup

1.1 Initialize the Monorepo:

mkdir oauth-protected-app
cd oauth-protected-app

# Initialize root package.json
npm init -y

# Create workspaces
mkdir -p packages/{auth-server,mcp-server,widget}

# Set up workspace in root package.json

Root package.json:

{
  "name": "oauth-protected-app",
  "private": true,
  "workspaces": [
    "packages/*"
  ],
  "scripts": {
    "dev:auth": "npm run dev --workspace=auth-server",
    "dev:mcp": "npm run dev --workspace=mcp-server",
    "dev:widget": "npm run dev --workspace=widget",
    "dev": "concurrently \"npm:dev:*\"",
    "build": "npm run build --workspaces"
  },
  "devDependencies": {
    "concurrently": "^8.2.2",
    "typescript": "^5.3.3"
  }
}

1.2 Generate RSA Keys for JWT Signing:

cd packages/auth-server
mkdir keys

# Generate private key
openssl genpkey -algorithm RSA -out keys/private.key -pkeyopt rsa_keygen_bits:2048

# Extract public key
openssl rsa -pubout -in keys/private.key -out keys/public.key

# Add keys to .gitignore
echo "keys/*.key" >> ../../.gitignore

1.3 Set Up Environment Variables:

.env.example:

# Auth Server
AUTH_SERVER_PORT=3000
AUTH_SERVER_URL=https://auth.localhost:3000
GITHUB_CLIENT_ID=your_github_oauth_app_client_id
GITHUB_CLIENT_SECRET=your_github_oauth_app_secret
GITHUB_CALLBACK_URL=https://auth.localhost:3000/github/callback
SESSION_SECRET=your_random_session_secret
DATABASE_URL=postgresql://user:pass@localhost:5432/oauth_app

# MCP Server
MCP_SERVER_PORT=3001
MCP_SERVER_URL=https://mcp.localhost:3001
AUTH_SERVER_JWKS_URL=https://auth.localhost:3000/.well-known/jwks.json

# Widget
VITE_MCP_SERVER_URL=https://mcp.localhost:3001

1.4 Create GitHub OAuth App:

  1. Go to GitHub Settings โ†’ Developer settings โ†’ OAuth Apps
  2. Click โ€œNew OAuth Appโ€
  3. Fill in:
    • Application name: โ€œChatGPT GitHub Integration (Dev)โ€
    • Homepage URL: https://auth.localhost:3000
    • Authorization callback URL: https://auth.localhost:3000/github/callback
  4. Copy Client ID and Client Secret to .env

Step 2: Build the Authorization Server

2.1 Initialize Auth Server:

cd packages/auth-server
npm init -y
npm install express express-session ejs
npm install passport passport-github2
npm install jsonwebtoken jose
npm install pg drizzle-orm
npm install dotenv cors
npm install --save-dev @types/express @types/node @types/passport tsx nodemon

package.json:

{
  "name": "auth-server",
  "scripts": {
    "dev": "nodemon --watch src --exec tsx src/server.ts",
    "build": "tsc",
    "start": "node dist/server.js"
  }
}

2.2 Create Database Schema:

src/db/schema.ts:

import { pgTable, varchar, text, timestamp, boolean, pgEnum } from 'drizzle-orm/pg-core';

export const clients = pgTable('oauth_clients', {
  clientId: varchar('client_id', { length: 255 }).primaryKey(),
  clientName: varchar('client_name', { length: 255 }).notNull(),
  clientSecret: varchar('client_secret', { length: 255 }),
  redirectUris: text('redirect_uris').array().notNull(),
  tokenEndpointAuthMethod: varchar('token_endpoint_auth_method', { length: 50 }).notNull(),
  grantTypes: text('grant_types').array().notNull(),
  responseTypes: text('response_types').array().notNull(),
  scope: varchar('scope', { length: 500 }).notNull(),
  createdAt: timestamp('created_at').defaultNow()
});

export const authorizationCodes = pgTable('authorization_codes', {
  code: varchar('code', { length: 255 }).primaryKey(),
  clientId: varchar('client_id', { length: 255 }).notNull(),
  userId: varchar('user_id', { length: 255 }).notNull(),
  redirectUri: varchar('redirect_uri', { length: 500 }).notNull(),
  scope: varchar('scope', { length: 500 }).notNull(),
  codeChallenge: varchar('code_challenge', { length: 255 }).notNull(),
  codeChallengeMethod: varchar('code_challenge_method', { length: 10 }).notNull(),
  expiresAt: timestamp('expires_at').notNull(),
  createdAt: timestamp('created_at').defaultNow()
});

export const accessTokens = pgTable('access_tokens', {
  tokenId: varchar('token_id', { length: 255 }).primaryKey(),
  clientId: varchar('client_id', { length: 255 }).notNull(),
  userId: varchar('user_id', { length: 255 }).notNull(),
  scope: varchar('scope', { length: 500 }).notNull(),
  githubToken: varchar('github_token', { length: 500 }).notNull(),
  expiresAt: timestamp('expires_at').notNull(),
  createdAt: timestamp('created_at').defaultNow()
});

export const refreshTokens = pgTable('refresh_tokens', {
  tokenId: varchar('token_id', { length: 255 }).primaryKey(),
  clientId: varchar('client_id', { length: 255 }).notNull(),
  userId: varchar('user_id', { length: 255 }).notNull(),
  scope: varchar('scope', { length: 500 }).notNull(),
  githubRefreshToken: varchar('github_refresh_token', { length: 500 }),
  expiresAt: timestamp('expires_at').notNull(),
  revoked: boolean('revoked').default(false),
  createdAt: timestamp('created_at').defaultNow()
});

export const users = pgTable('users', {
  userId: varchar('user_id', { length: 255 }).primaryKey(),
  githubUserId: varchar('github_user_id', { length: 255 }).unique().notNull(),
  githubUsername: varchar('github_username', { length: 255 }).notNull(),
  email: varchar('email', { length: 255 }),
  createdAt: timestamp('created_at').defaultNow(),
  updatedAt: timestamp('updated_at').defaultNow()
});

2.3 Token Service:

src/services/tokenService.ts:

import * as jose from 'jose';
import crypto from 'crypto';
import fs from 'fs';
import path from 'path';

const PRIVATE_KEY = fs.readFileSync(path.join(__dirname, '../../keys/private.key'), 'utf-8');
const PUBLIC_KEY = fs.readFileSync(path.join(__dirname, '../../keys/public.key'), 'utf-8');

export class TokenService {
  // Generate access token (JWT)
  async generateAccessToken(payload: {
    userId: string;
    clientId: string;
    scope: string;
    githubToken: string;
  }): Promise<string> {
    const privateKey = await jose.importPKCS8(PRIVATE_KEY, 'RS256');

    const jwt = await new jose.SignJWT({
      sub: payload.userId,
      scope: payload.scope,
      github_token: payload.githubToken
    })
      .setProtectedHeader({ alg: 'RS256', kid: 'key-2025-01' })
      .setIssuer(process.env.AUTH_SERVER_URL!)
      .setAudience(process.env.MCP_SERVER_URL!)
      .setExpirationTime('1h')
      .setIssuedAt()
      .sign(privateKey);

    return jwt;
  }

  // Generate refresh token (opaque)
  generateRefreshToken(): string {
    return crypto.randomBytes(32).toString('base64url');
  }

  // Validate JWT
  async validateAccessToken(token: string) {
    const publicKey = await jose.importSPKI(PUBLIC_KEY, 'RS256');

    const { payload } = await jose.jwtVerify(token, publicKey, {
      issuer: process.env.AUTH_SERVER_URL,
      audience: process.env.MCP_SERVER_URL
    });

    return payload;
  }

  // Get JWKS (public keys for verification)
  async getJWKS() {
    const publicKey = await jose.importSPKI(PUBLIC_KEY, 'RS256');
    const jwk = await jose.exportJWK(publicKey);

    return {
      keys: [
        {
          ...jwk,
          kid: 'key-2025-01',
          use: 'sig',
          alg: 'RS256'
        }
      ]
    };
  }
}

2.4 PKCE Validation Middleware:

src/middleware/validatePKCE.ts:

import crypto from 'crypto';

export function validatePKCE(codeVerifier: string, codeChallenge: string, method: string): boolean {
  if (method !== 'S256') {
    throw new Error('Only S256 code_challenge_method supported');
  }

  // Compute SHA-256 hash of verifier
  const hash = crypto
    .createHash('sha256')
    .update(codeVerifier)
    .digest('base64url');

  // Compare with stored challenge
  return hash === codeChallenge;
}

2.5 Dynamic Client Registration Endpoint:

src/routes/register.ts:

import { Router } from 'express';
import crypto from 'crypto';
import { db } from '../db';
import { clients } from '../db/schema';

const router = Router();

router.post('/oauth/register', async (req, res) => {
  try {
    const {
      client_name,
      redirect_uris,
      token_endpoint_auth_method = 'none',
      grant_types = ['authorization_code', 'refresh_token'],
      response_types = ['code'],
      scope
    } = req.body;

    // Validate request
    if (!client_name || !redirect_uris || !Array.isArray(redirect_uris)) {
      return res.status(400).json({ error: 'invalid_request' });
    }

    // Generate client_id
    const clientId = `chatgpt_${crypto.randomUUID()}`;

    // For public clients, no client_secret
    const clientSecret = token_endpoint_auth_method === 'none'
      ? null
      : crypto.randomBytes(32).toString('base64url');

    // Store client
    const client = {
      clientId,
      clientName: client_name,
      clientSecret,
      redirectUris: redirect_uris,
      tokenEndpointAuthMethod: token_endpoint_auth_method,
      grantTypes: grant_types,
      responseTypes: response_types,
      scope
    };

    await db.insert(clients).values(client);

    res.json({
      client_id: clientId,
      client_secret: clientSecret,
      client_name: client_name,
      redirect_uris,
      token_endpoint_auth_method,
      grant_types,
      response_types,
      scope
    });
  } catch (error) {
    console.error('DCR error:', error);
    res.status(500).json({ error: 'server_error' });
  }
});

export default router;

2.6 Authorization Endpoint:

src/routes/authorize.ts:

import { Router } from 'express';
import crypto from 'crypto';
import { db } from '../db';
import { clients, authorizationCodes } from '../db/schema';
import { eq } from 'drizzle-orm';

const router = Router();

router.get('/oauth/authorize', async (req, res) => {
  const {
    response_type,
    client_id,
    redirect_uri,
    scope,
    state,
    code_challenge,
    code_challenge_method
  } = req.query;

  // Validate parameters
  if (response_type !== 'code') {
    return res.redirect(`${redirect_uri}?error=unsupported_response_type&state=${state}`);
  }

  if (!code_challenge || code_challenge_method !== 'S256') {
    return res.redirect(`${redirect_uri}?error=invalid_request&state=${state}`);
  }

  // Verify client_id and redirect_uri
  const [client] = await db.select().from(clients).where(eq(clients.clientId, client_id as string));

  if (!client || !client.redirectUris.includes(redirect_uri as string)) {
    return res.status(400).send('Invalid client_id or redirect_uri');
  }

  // Check if user is authenticated (via GitHub OAuth)
  if (!req.isAuthenticated()) {
    // Store OAuth params in session and redirect to GitHub login
    req.session.oauthParams = {
      response_type,
      client_id,
      redirect_uri,
      scope,
      state,
      code_challenge,
      code_challenge_method
    };

    return res.redirect('/auth/github');
  }

  // User is authenticated - show consent screen
  res.render('consent', {
    client,
    scope: (scope as string).split(' '),
    user: req.user
  });
});

// Consent handler
router.post('/oauth/authorize/consent', async (req, res) => {
  const { allow } = req.body;
  const oauthParams = req.session.oauthParams;

  if (!allow) {
    return res.redirect(`${oauthParams.redirect_uri}?error=access_denied&state=${oauthParams.state}`);
  }

  // Generate authorization code
  const code = crypto.randomBytes(32).toString('base64url');

  // Store authorization code with PKCE challenge
  await db.insert(authorizationCodes).values({
    code,
    clientId: oauthParams.client_id,
    userId: req.user.id,
    redirectUri: oauthParams.redirect_uri,
    scope: oauthParams.scope,
    codeChallenge: oauthParams.code_challenge,
    codeChallengeMethod: oauthParams.code_challenge_method,
    expiresAt: new Date(Date.now() + 10 * 60 * 1000) // 10 minutes
  });

  // Redirect back to client with code
  res.redirect(`${oauthParams.redirect_uri}?code=${code}&state=${oauthParams.state}`);
});

export default router;

src/views/consent.ejs:

<!DOCTYPE html>
<html>
<head>
  <title>Authorize Application</title>
  <style>
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      max-width: 500px;
      margin: 50px auto;
      padding: 20px;
    }
    .app-info {
      background: #f5f5f5;
      padding: 20px;
      border-radius: 8px;
      margin-bottom: 20px;
    }
    .permissions {
      list-style: none;
      padding: 0;
    }
    .permissions li {
      padding: 10px;
      border-bottom: 1px solid #eee;
    }
    .permissions li:before {
      content: "โœ“ ";
      color: green;
    }
    button {
      padding: 12px 24px;
      margin: 10px 5px;
      border: none;
      border-radius: 5px;
      cursor: pointer;
      font-size: 16px;
    }
    .allow {
      background: #0066cc;
      color: white;
    }
    .deny {
      background: #666;
      color: white;
    }
  </style>
</head>
<body>
  <div class="app-info">
    <h2><%= client.clientName %> wants to access your account</h2>
    <p>This will allow <strong><%= client.clientName %></strong> to:</p>
    <ul class="permissions">
      <% scope.forEach(function(permission) { %>
        <li><%= permission %></li>
      <% }); %>
    </ul>
  </div>

  <form method="POST" action="/oauth/authorize/consent">
    <button type="submit" name="allow" value="true" class="allow">
      Authorize
    </button>
    <button type="submit" name="allow" value="false" class="deny">
      Deny
    </button>
  </form>
</body>
</html>

2.7 Token Endpoint:

src/routes/token.ts:

import { Router } from 'express';
import { db } from '../db';
import { authorizationCodes, accessTokens, refreshTokens, clients } from '../db/schema';
import { eq, and, gt } from 'drizzle-orm';
import { TokenService } from '../services/tokenService';
import { validatePKCE } from '../middleware/validatePKCE';

const router = Router();
const tokenService = new TokenService();

router.post('/oauth/token', async (req, res) => {
  const { grant_type } = req.body;

  if (grant_type === 'authorization_code') {
    return handleAuthorizationCodeGrant(req, res);
  } else if (grant_type === 'refresh_token') {
    return handleRefreshTokenGrant(req, res);
  } else {
    return res.status(400).json({ error: 'unsupported_grant_type' });
  }
});

async function handleAuthorizationCodeGrant(req, res) {
  const {
    code,
    redirect_uri,
    client_id,
    code_verifier
  } = req.body;

  // Verify client
  const [client] = await db.select().from(clients).where(eq(clients.clientId, client_id));
  if (!client) {
    return res.status(401).json({ error: 'invalid_client' });
  }

  // Get authorization code
  const [authCode] = await db
    .select()
    .from(authorizationCodes)
    .where(
      and(
        eq(authorizationCodes.code, code),
        eq(authorizationCodes.clientId, client_id),
        gt(authorizationCodes.expiresAt, new Date())
      )
    );

  if (!authCode) {
    return res.status(400).json({ error: 'invalid_grant' });
  }

  // Validate PKCE
  try {
    const isValid = validatePKCE(
      code_verifier,
      authCode.codeChallenge,
      authCode.codeChallengeMethod
    );

    if (!isValid) {
      return res.status(400).json({ error: 'invalid_grant' });
    }
  } catch (error) {
    return res.status(400).json({ error: 'invalid_request' });
  }

  // Validate redirect_uri
  if (redirect_uri !== authCode.redirectUri) {
    return res.status(400).json({ error: 'invalid_grant' });
  }

  // Delete authorization code (one-time use)
  await db.delete(authorizationCodes).where(eq(authorizationCodes.code, code));

  // Get user's GitHub token from session or database
  const githubToken = req.user.githubAccessToken; // Stored during GitHub OAuth

  // Generate access token (JWT)
  const accessToken = await tokenService.generateAccessToken({
    userId: authCode.userId,
    clientId: client_id,
    scope: authCode.scope,
    githubToken
  });

  // Generate refresh token
  const refreshToken = tokenService.generateRefreshToken();

  // Store tokens
  const tokenId = crypto.randomUUID();
  await db.insert(accessTokens).values({
    tokenId,
    clientId: client_id,
    userId: authCode.userId,
    scope: authCode.scope,
    githubToken,
    expiresAt: new Date(Date.now() + 60 * 60 * 1000) // 1 hour
  });

  await db.insert(refreshTokens).values({
    tokenId: crypto.randomUUID(),
    clientId: client_id,
    userId: authCode.userId,
    scope: authCode.scope,
    githubRefreshToken: null, // GitHub doesn't provide refresh tokens for web flow
    expiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000), // 90 days
    revoked: false
  });

  res.json({
    access_token: accessToken,
    token_type: 'Bearer',
    expires_in: 3600,
    refresh_token: refreshToken,
    scope: authCode.scope
  });
}

async function handleRefreshTokenGrant(req, res) {
  const { refresh_token, client_id } = req.body;

  // Verify client
  const [client] = await db.select().from(clients).where(eq(clients.clientId, client_id));
  if (!client) {
    return res.status(401).json({ error: 'invalid_client' });
  }

  // Get refresh token
  const [storedRefreshToken] = await db
    .select()
    .from(refreshTokens)
    .where(
      and(
        eq(refreshTokens.tokenId, refresh_token),
        eq(refreshTokens.clientId, client_id),
        eq(refreshTokens.revoked, false),
        gt(refreshTokens.expiresAt, new Date())
      )
    );

  if (!storedRefreshToken) {
    return res.status(400).json({ error: 'invalid_grant' });
  }

  // Revoke old refresh token (token rotation)
  await db
    .update(refreshTokens)
    .set({ revoked: true })
    .where(eq(refreshTokens.tokenId, refresh_token));

  // Generate new tokens
  const newAccessToken = await tokenService.generateAccessToken({
    userId: storedRefreshToken.userId,
    clientId: client_id,
    scope: storedRefreshToken.scope,
    githubToken: '...' // Retrieve from database
  });

  const newRefreshToken = tokenService.generateRefreshToken();

  // Store new tokens
  // ... (similar to authorization_code grant)

  res.json({
    access_token: newAccessToken,
    token_type: 'Bearer',
    expires_in: 3600,
    refresh_token: newRefreshToken,
    scope: storedRefreshToken.scope
  });
}

export default router;

2.8 Metadata Endpoints:

src/routes/metadata.ts:

import { Router } from 'express';
import { TokenService } from '../services/tokenService';

const router = Router();
const tokenService = new TokenService();

// Authorization Server Metadata
router.get('/.well-known/oauth-authorization-server', (req, res) => {
  res.json({
    issuer: process.env.AUTH_SERVER_URL,
    authorization_endpoint: `${process.env.AUTH_SERVER_URL}/oauth/authorize`,
    token_endpoint: `${process.env.AUTH_SERVER_URL}/oauth/token`,
    registration_endpoint: `${process.env.AUTH_SERVER_URL}/oauth/register`,
    jwks_uri: `${process.env.AUTH_SERVER_URL}/.well-known/jwks.json`,
    scopes_supported: ['read:repos', 'write:issues', 'read:user'],
    response_types_supported: ['code'],
    grant_types_supported: ['authorization_code', 'refresh_token'],
    token_endpoint_auth_methods_supported: ['none', 'client_secret_post'],
    code_challenge_methods_supported: ['S256']
  });
});

// JWKS Endpoint
router.get('/.well-known/jwks.json', async (req, res) => {
  const jwks = await tokenService.getJWKS();
  res.json(jwks);
});

export default router;

2.9 GitHub OAuth Integration:

src/config/passport.ts:

import passport from 'passport';
import { Strategy as GitHubStrategy } from 'passport-github2';
import { db } from '../db';
import { users } from '../db/schema';
import { eq } from 'drizzle-orm';

passport.use(new GitHubStrategy({
    clientID: process.env.GITHUB_CLIENT_ID!,
    clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    callbackURL: process.env.GITHUB_CALLBACK_URL!
  },
  async (accessToken, refreshToken, profile, done) => {
    try {
      // Find or create user
      let [user] = await db
        .select()
        .from(users)
        .where(eq(users.githubUserId, profile.id));

      if (!user) {
        [user] = await db.insert(users).values({
          userId: crypto.randomUUID(),
          githubUserId: profile.id,
          githubUsername: profile.username,
          email: profile.emails?.[0]?.value
        }).returning();
      }

      // Store GitHub access token in user session
      user.githubAccessToken = accessToken;

      done(null, user);
    } catch (error) {
      done(error);
    }
  }
));

passport.serializeUser((user: any, done) => {
  done(null, user.userId);
});

passport.deserializeUser(async (userId: string, done) => {
  try {
    const [user] = await db.select().from(users).where(eq(users.userId, userId));
    done(null, user);
  } catch (error) {
    done(error);
  }
});

src/routes/auth.ts:

import { Router } from 'express';
import passport from 'passport';

const router = Router();

router.get('/auth/github', passport.authenticate('github', {
  scope: ['user:email', 'repo']
}));

router.get('/github/callback',
  passport.authenticate('github', { failureRedirect: '/login' }),
  (req, res) => {
    // Redirect back to authorization endpoint
    const params = req.session.oauthParams;
    const query = new URLSearchParams(params).toString();
    res.redirect(`/oauth/authorize?${query}`);
  }
);

export default router;

2.10 Main Server:

src/server.ts:

import express from 'express';
import session from 'express-session';
import passport from 'passport';
import cors from 'cors';
import dotenv from 'dotenv';

import registerRouter from './routes/register';
import authorizeRouter from './routes/authorize';
import tokenRouter from './routes/token';
import metadataRouter from './routes/metadata';
import authRouter from './routes/auth';
import './config/passport';

dotenv.config();

const app = express();

app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));

app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.use(session({
  secret: process.env.SESSION_SECRET!,
  resave: false,
  saveUninitialized: false,
  cookie: { secure: process.env.NODE_ENV === 'production' }
}));

app.use(passport.initialize());
app.use(passport.session());

// Routes
app.use(registerRouter);
app.use(authorizeRouter);
app.use(tokenRouter);
app.use(metadataRouter);
app.use(authRouter);

const PORT = process.env.AUTH_SERVER_PORT || 3000;
app.listen(PORT, () => {
  console.log(`Auth server running on ${process.env.AUTH_SERVER_URL}`);
});

Step 3: Build the MCP Server

3.1 Initialize MCP Server:

cd packages/mcp-server
npm init -y
npm install fastmcp
npm install jose
npm install @octokit/rest
npm install dotenv
npm install --save-dev @types/node tsx nodemon typescript

3.2 Token Validation Middleware:

src/middleware/validateToken.ts:

import * as jose from 'jose';
import { Request, Response, NextFunction } from 'express';

let cachedJWKS: jose.JSONWebKeySet | null = null;

export async function validateToken(req: Request, res: Response, next: NextFunction) {
  const authHeader = req.headers.authorization;

  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({
      error: 'unauthorized',
      _meta: {
        'mcp/www_authenticate': {
          scheme: 'Bearer',
          realm: 'github-integration'
        }
      }
    });
  }

  const token = authHeader.substring(7);

  try {
    // Fetch JWKS if not cached
    if (!cachedJWKS) {
      const response = await fetch(process.env.AUTH_SERVER_JWKS_URL!);
      cachedJWKS = await response.json();
    }

    const JWKS = jose.createRemoteJWKSet(new URL(process.env.AUTH_SERVER_JWKS_URL!));

    const { payload } = await jose.jwtVerify(token, JWKS, {
      issuer: process.env.AUTH_SERVER_URL,
      audience: process.env.MCP_SERVER_URL
    });

    // Check expiration
    if (payload.exp && payload.exp < Date.now() / 1000) {
      return res.status(401).json({
        error: 'token_expired',
        _meta: {
          'mcp/www_authenticate': {
            scheme: 'Bearer',
            realm: 'github-integration',
            error: 'invalid_token',
            error_description: 'Token expired'
          }
        }
      });
    }

    // Attach user info to request
    req.user = {
      userId: payload.sub as string,
      scope: (payload.scope as string).split(' '),
      githubToken: payload.github_token as string
    };

    next();
  } catch (error) {
    console.error('Token validation error:', error);
    return res.status(401).json({
      error: 'invalid_token',
      _meta: {
        'mcp/www_authenticate': {
          scheme: 'Bearer',
          realm: 'github-integration',
          error: 'invalid_token'
        }
      }
    });
  }
}

// Scope validation middleware
export function requireScope(...requiredScopes: string[]) {
  return (req: Request, res: Response, next: NextFunction) => {
    const userScopes = req.user?.scope || [];

    const hasScope = requiredScopes.every(scope => userScopes.includes(scope));

    if (!hasScope) {
      return res.status(403).json({
        error: 'insufficient_scope',
        required_scope: requiredScopes.join(' ')
      });
    }

    next();
  };
}

3.3 GitHub Client Service:

src/services/githubClient.ts:

import { Octokit } from '@octokit/rest';

export class GitHubClient {
  private octokit: Octokit;

  constructor(accessToken: string) {
    this.octokit = new Octokit({ auth: accessToken });
  }

  async listRepositories() {
    const { data } = await this.octokit.repos.listForAuthenticatedUser({
      sort: 'updated',
      per_page: 100
    });

    return data.map(repo => ({
      id: repo.id,
      name: repo.name,
      full_name: repo.full_name,
      description: repo.description,
      language: repo.language,
      stargazers_count: repo.stargazers_count,
      forks_count: repo.forks_count,
      open_issues_count: repo.open_issues_count,
      updated_at: repo.updated_at,
      html_url: repo.html_url,
      private: repo.private
    }));
  }

  async getRepositoryIssues(owner: string, repo: string, state: 'open' | 'closed' | 'all' = 'open') {
    const { data } = await this.octokit.issues.listForRepo({
      owner,
      repo,
      state,
      per_page: 50
    });

    return data.map(issue => ({
      id: issue.id,
      number: issue.number,
      title: issue.title,
      body: issue.body,
      state: issue.state,
      user: issue.user?.login,
      labels: issue.labels.map((label: any) => label.name),
      created_at: issue.created_at,
      updated_at: issue.updated_at,
      html_url: issue.html_url
    }));
  }

  async createIssue(owner: string, repo: string, title: string, body?: string, labels?: string[]) {
    const { data } = await this.octokit.issues.create({
      owner,
      repo,
      title,
      body,
      labels
    });

    return {
      id: data.id,
      number: data.number,
      title: data.title,
      html_url: data.html_url
    };
  }

  async createPullRequest(
    owner: string,
    repo: string,
    title: string,
    head: string,
    base: string,
    body?: string
  ) {
    const { data } = await this.octokit.pulls.create({
      owner,
      repo,
      title,
      head,
      base,
      body
    });

    return {
      id: data.id,
      number: data.number,
      title: data.title,
      html_url: data.html_url,
      state: data.state
    };
  }
}

3.4 Protected MCP Tools:

src/tools/listRepositories.ts:

import { FastMCP } from 'fastmcp';
import { GitHubClient } from '../services/githubClient';

export function registerListRepositoriesTool(mcp: FastMCP) {
  mcp.tool({
    name: 'list_repositories',
    description: 'Use this when user wants to see their GitHub repositories',
    annotations: {
      readOnlyHint: true,
      securitySchemes: ['oauth2']
    }
  }, async (params, context) => {
    const githubToken = context.user.githubToken;
    const client = new GitHubClient(githubToken);

    const repositories = await client.listRepositories();

    return {
      structuredContent: {
        repositories,
        count: repositories.length
      },
      uiComponent: {
        type: 'iframe',
        url: `${process.env.WIDGET_URL}/repositories.html`
      }
    };
  });
}

src/tools/getIssues.ts:

export function registerGetIssues Tool(mcp: FastMCP) {
  mcp.tool({
    name: 'get_repository_issues',
    description: 'Use this when user wants to see issues for a specific repository',
    parameters: {
      type: 'object',
      properties: {
        owner: {
          type: 'string',
          description: 'Repository owner username'
        },
        repo: {
          type: 'string',
          description: 'Repository name'
        },
        state: {
          type: 'string',
          enum: ['open', 'closed', 'all'],
          description: 'Issue state filter',
          default: 'open'
        }
      },
      required: ['owner', 'repo']
    },
    annotations: {
      readOnlyHint: true,
      securitySchemes: ['oauth2']
    }
  }, async (params, context) => {
    const { owner, repo, state = 'open' } = params;
    const githubToken = context.user.githubToken;
    const client = new GitHubClient(githubToken);

    const issues = await client.getRepositoryIssues(owner, repo, state as any);

    return {
      structuredContent: {
        issues,
        repository: `${owner}/${repo}`,
        count: issues.length
      },
      uiComponent: {
        type: 'iframe',
        url: `${process.env.WIDGET_URL}/issues.html`
      }
    };
  });
}

src/tools/createIssue.ts:

export function registerCreateIssueTool(mcp: FastMCP) {
  mcp.tool({
    name: 'create_issue',
    description: 'Use this when user wants to create a new issue in a repository',
    parameters: {
      type: 'object',
      properties: {
        owner: { type: 'string' },
        repo: { type: 'string' },
        title: { type: 'string' },
        body: { type: 'string' },
        labels: {
          type: 'array',
          items: { type: 'string' },
          description: 'Issue labels'
        }
      },
      required: ['owner', 'repo', 'title']
    },
    annotations: {
      readOnlyHint: false,
      destructiveHint: false,
      securitySchemes: ['oauth2']
    }
  }, async (params, context) => {
    const { owner, repo, title, body, labels } = params;
    const githubToken = context.user.githubToken;
    const client = new GitHubClient(githubToken);

    const issue = await client.createIssue(owner, repo, title, body, labels);

    return {
      structuredContent: {
        success: true,
        issue,
        message: `Issue #${issue.number} created successfully`
      }
    };
  });
}

3.5 Well-Known Endpoint for MCP Server:

src/routes/wellKnown.ts:

import { Router } from 'express';

const router = Router();

router.get('/.well-known/oauth-protected-resource', (req, res) => {
  res.json({
    resource: process.env.MCP_SERVER_URL,
    authorization_servers: [
      process.env.AUTH_SERVER_URL
    ],
    bearer_methods_supported: ['header'],
    scopes_supported: [
      'read:repos',
      'write:issues',
      'read:user'
    ]
  });
});

export default router;

3.6 MCP Server Main:

src/server.ts:

import { FastMCP } from 'fastmcp';
import express from 'express';
import dotenv from 'dotenv';
import { validateToken, requireScope } from './middleware/validateToken';
import wellKnownRouter from './routes/wellKnown';
import { registerListRepositoriesTool } from './tools/listRepositories';
import { registerGetIssuesTool } from './tools/getIssues';
import { registerCreateIssueTool } from './tools/createIssue';
import { registerCreatePullRequestTool } from './tools/createPullRequest';

dotenv.config();

const mcp = new FastMCP('GitHub Integration');

// Register protected tools
registerListRepositoriesTool(mcp);
registerGetIssuesTool(mcp);
registerCreateIssueTool(mcp);
registerCreatePullRequestTool(mcp);

const app = express();

// Well-known endpoint (no auth required)
app.use(wellKnownRouter);

// Apply token validation to all MCP tool routes
app.use('/tools', validateToken);

// Mount MCP server
app.use(mcp.middleware());

const PORT = process.env.MCP_SERVER_PORT || 3001;
app.listen(PORT, () => {
  console.log(`MCP server running on ${process.env.MCP_SERVER_URL}`);
});

Step 4: Build the Widget

4.1 Initialize Widget:

cd packages/widget
npm init -y
npm install react react-dom
npm install @openai/apps-sdk-ui
npm install --save-dev vite @vitejs/plugin-react typescript
npm install --save-dev vite-plugin-singlefile

vite.config.ts:

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { viteSingleFile } from 'vite-plugin-singlefile';

export default defineConfig({
  plugins: [react(), viteSingleFile()],
  build: {
    outDir: 'dist',
    rollupOptions: {
      output: {
        inlineDynamicImports: true
      }
    }
  }
});

4.2 Repository List Component:

src/components/RepositoryList.tsx:

import { useEffect, useState } from 'react';
import { Button, Badge } from '@openai/apps-sdk-ui';

interface Repository {
  id: number;
  name: string;
  full_name: string;
  description: string;
  language: string;
  stargazers_count: number;
  open_issues_count: number;
  updated_at: string;
  html_url: string;
  private: boolean;
}

export function RepositoryList() {
  const [repositories, setRepositories] = useState<Repository[]>([]);

  useEffect(() => {
    const data = window.openai?.toolOutput;
    if (data?.repositories) {
      setRepositories(data.repositories);
    }
  }, []);

  const handleViewIssues = (repo: Repository) => {
    const [owner, repoName] = repo.full_name.split('/');
    window.openai?.callTool('get_repository_issues', {
      owner,
      repo: repoName,
      state: 'open'
    });
  };

  const handleCreateIssue = (repo: Repository) => {
    window.openai?.sendFollowUpMessage(
      `Create a new issue in ${repo.full_name}`
    );
  };

  return (
    <div className="p-4 space-y-4">
      <div className="flex items-center justify-between">
        <h2 className="text-xl font-bold">
          ๐Ÿ“ฆ Your Repositories ({repositories.length})
        </h2>
      </div>

      <div className="space-y-3">
        {repositories.map(repo => (
          <div key={repo.id} className="border rounded-lg p-4 hover:shadow-md transition">
            <div className="flex items-start justify-between">
              <div className="flex-1">
                <div className="flex items-center gap-2">
                  <h3 className="font-semibold text-lg">
                    {repo.private ? '๐Ÿ”’' : '๐Ÿ“‚'} {repo.name}
                  </h3>
                  {repo.language && (
                    <Badge variant="secondary">{repo.language}</Badge>
                  )}
                </div>

                {repo.description && (
                  <p className="text-sm text-gray-600 mt-1">
                    {repo.description}
                  </p>
                )}

                <div className="flex items-center gap-4 mt-2 text-sm text-gray-500">
                  <span>โญ {repo.stargazers_count}</span>
                  <span>๐Ÿ› {repo.open_issues_count} issues</span>
                  <span>
                    Updated {new Date(repo.updated_at).toLocaleDateString()}
                  </span>
                </div>
              </div>
            </div>

            <div className="flex gap-2 mt-3">
              <Button
                size="sm"
                variant="secondary"
                onClick={() => window.open(repo.html_url, '_blank')}
              >
                View on GitHub
              </Button>
              <Button
                size="sm"
                onClick={() => handleViewIssues(repo)}
              >
                Show Issues
              </Button>
              <Button
                size="sm"
                onClick={() => handleCreateIssue(repo)}
              >
                Create Issue
              </Button>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

4.3 Connect Button Component:

src/components/ConnectButton.tsx:

export function ConnectButton() {
  return (
    <div className="p-8 text-center">
      <div className="max-w-md mx-auto bg-gray-50 rounded-lg p-6">
        <h2 className="text-2xl font-bold mb-4">๐Ÿ”— Connect to GitHub</h2>

        <p className="text-gray-600 mb-6">
          To access your GitHub repositories, you'll need to connect your account.
        </p>

        <div className="bg-white border rounded-lg p-4 mb-6 text-left">
          <p className="font-semibold mb-2">This app needs access to:</p>
          <ul className="space-y-1 text-sm">
            <li>โœ“ View your repositories</li>
            <li>โœ“ Read and write issues</li>
            <li>โœ“ Create pull requests</li>
          </ul>
        </div>

        <p className="text-xs text-gray-500">
          ChatGPT will redirect you to authorize this connection.
          The OAuth flow will handle authentication automatically.
        </p>
      </div>
    </div>
  );
}

4.4 Main App Component:

src/App.tsx:

import { useEffect, useState } from 'react';
import { ConnectButton } from './components/ConnectButton';
import { RepositoryList } from './components/RepositoryList';

function App() {
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  const [data, setData] = useState(null);

  useEffect(() => {
    const toolOutput = window.openai?.toolOutput;

    if (toolOutput) {
      setIsAuthenticated(true);
      setData(toolOutput);
    } else {
      setIsAuthenticated(false);
    }
  }, []);

  if (!isAuthenticated) {
    return <ConnectButton />;
  }

  return <RepositoryList />;
}

export default App;

5. Testing & Validation

5.1 Local Testing Setup

Using ngrok for HTTPS Tunnels:

# Install ngrok
brew install ngrok

# Start your servers
npm run dev:auth   # Port 3000
npm run dev:mcp    # Port 3001
npm run dev:widget # Port 5173

# Create tunnels
ngrok http 3000 --domain=auth.your-domain.ngrok.app
ngrok http 3001 --domain=mcp.your-domain.ngrok.app
ngrok http 5173 --domain=widget.your-domain.ngrok.app

Update .env with ngrok URLs:

AUTH_SERVER_URL=https://auth.your-domain.ngrok.app
MCP_SERVER_URL=https://mcp.your-domain.ngrok.app
WIDGET_URL=https://widget.your-domain.ngrok.app

5.2 Testing OAuth Flow

Test 1: Well-Known Discovery:

# Test MCP server metadata
curl https://mcp.your-domain.ngrok.app/.well-known/oauth-protected-resource

# Expected response:
{
  "resource": "https://mcp.your-domain.ngrok.app",
  "authorization_servers": ["https://auth.your-domain.ngrok.app"],
  "bearer_methods_supported": ["header"],
  "scopes_supported": ["read:repos", "write:issues", "read:user"]
}

# Test auth server metadata
curl https://auth.your-domain.ngrok.app/.well-known/oauth-authorization-server

# Should return authorization endpoints

Test 2: Dynamic Client Registration:

curl -X POST https://auth.your-domain.ngrok.app/oauth/register \
  -H "Content-Type: application/json" \
  -d '{
    "client_name": "Test Client",
    "redirect_uris": ["https://example.com/callback"],
    "token_endpoint_auth_method": "none",
    "grant_types": ["authorization_code", "refresh_token"],
    "response_types": ["code"],
    "scope": "read:repos write:issues"
  }'

# Save the returned client_id for next tests

Test 3: Unauthenticated Request:

curl https://mcp.your-domain.ngrok.app/tools/list_repositories

# Expected: 401 with WWW-Authenticate
{
  "error": "unauthorized",
  "_meta": {
    "mcp/www_authenticate": {
      "scheme": "Bearer",
      "realm": "github-integration"
    }
  }
}

Test 4: Full OAuth Flow (manual browser test):

  1. Open browser to authorization endpoint:
    https://auth.your-domain.ngrok.app/oauth/authorize?
      response_type=code&
      client_id=YOUR_CLIENT_ID&
      redirect_uri=https://example.com/callback&
      scope=read:repos%20write:issues&
      state=random_state&
      code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
      code_challenge_method=S256
    
  2. Log in with GitHub
  3. Grant permissions
  4. Copy authorization code from redirect
  5. Exchange for token:
    curl -X POST https://auth.your-domain.ngrok.app/oauth/token \
      -H "Content-Type: application/x-www-form-urlencoded" \
      -d "grant_type=authorization_code" \
      -d "code=AUTHORIZATION_CODE" \
      -d "redirect_uri=https://example.com/callback" \
      -d "client_id=YOUR_CLIENT_ID" \
      -d "code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
    

Test 5: Authenticated Request:

curl https://mcp.your-domain.ngrok.app/tools/list_repositories \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

# Should return repository list

Test 6: Token Refresh:

curl -X POST https://auth.your-domain.ngrok.app/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token" \
  -d "refresh_token=YOUR_REFRESH_TOKEN" \
  -d "client_id=YOUR_CLIENT_ID"

# Should return new access and refresh tokens

5.3 Testing with ChatGPT

  1. Configure App in ChatGPT:
    • Go to ChatGPT Apps dashboard
    • Create new app
    • Set MCP Server URL: https://mcp.your-domain.ngrok.app
  2. Test in ChatGPT:
    User: "Show my GitHub repositories"
    
    • Should trigger OAuth flow
    • User sees connect button
    • After authorizing, sees repository list
  3. Test Protected Operations:
    User: "Show issues in my awesome-project repository"
    User: "Create an issue titled 'Test issue' in awesome-project"
    

5.4 Automated Tests

packages/auth-server/tests/token.test.ts:

import { describe, it, expect } from 'vitest';
import { TokenService } from '../src/services/tokenService';

describe('TokenService', () => {
  it('should generate valid JWT', async () => {
    const service = new TokenService();
    const token = await service.generateAccessToken({
      userId: 'user_123',
      clientId: 'client_123',
      scope: 'read:repos',
      githubToken: 'gho_xxx'
    });

    expect(token).toBeTruthy();
    expect(token.split('.')).toHaveLength(3);
  });

  it('should validate JWT correctly', async () => {
    const service = new TokenService();
    const token = await service.generateAccessToken({
      userId: 'user_123',
      clientId: 'client_123',
      scope: 'read:repos',
      githubToken: 'gho_xxx'
    });

    const payload = await service.validateAccessToken(token);
    expect(payload.sub).toBe('user_123');
    expect(payload.scope).toBe('read:repos');
  });

  it('should reject expired tokens', async () => {
    // Create token with past expiration
    // ... test implementation
  });
});

packages/auth-server/tests/pkce.test.ts:

import { describe, it, expect } from 'vitest';
import { validatePKCE } from '../src/middleware/validatePKCE';
import crypto from 'crypto';

describe('PKCE Validation', () => {
  it('should validate correct code_verifier', () => {
    const verifier = 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk';
    const challenge = crypto
      .createHash('sha256')
      .update(verifier)
      .digest('base64url');

    const isValid = validatePKCE(verifier, challenge, 'S256');
    expect(isValid).toBe(true);
  });

  it('should reject invalid code_verifier', () => {
    const verifier = 'wrong_verifier';
    const challenge = 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM';

    const isValid = validatePKCE(verifier, challenge, 'S256');
    expect(isValid).toBe(false);
  });
});

6. Common Pitfalls & Debugging

6.1 Common Errors

Error: โ€œInvalid redirect_uriโ€

Cause: Redirect URI doesnโ€™t match registered URIs exactly.

Solution:

// WRONG - Pattern matching
if (redirectUri.startsWith(client.redirect_uris[0])) { ... }

// RIGHT - Exact match
if (client.redirect_uris.includes(redirectUri)) { ... }

Error: โ€œInvalid code_verifierโ€

Cause: PKCE validation failing.

Debug:

console.log('Received verifier:', codeVerifier);
console.log('Stored challenge:', storedCodeChallenge);

const computed = crypto.createHash('sha256')
  .update(codeVerifier)
  .digest('base64url');
console.log('Computed challenge:', computed);
console.log('Match:', computed === storedCodeChallenge);

Error: โ€œToken signature verification failedโ€

Cause: Using wrong public key or algorithm mismatch.

Solution:

  • Verify JWKS endpoint returns correct public key
  • Ensure algorithm is RS256 (not HS256)
  • Check key IDs (kid) match

Error: โ€œCORS blockedโ€

Cause: Missing CORS headers.

Solution:

app.use(cors({
  origin: [
    'https://chatgpt.com',
    'https://widget.your-domain.ngrok.app'
  ],
  credentials: true
}));

6.2 Debugging Tools

JWT Debugger:

# Decode JWT without verification
node -e "console.log(JSON.parse(Buffer.from(process.argv[1].split('.')[1], 'base64url')))" YOUR_JWT

Online Tools:

Logging Middleware:

app.use((req, res, next) => {
  console.log(`${req.method} ${req.path}`);
  console.log('Headers:', req.headers);
  console.log('Body:', req.body);
  next();
});

6.3 Database Query Debugging

// Enable Drizzle query logging
import { drizzle } from 'drizzle-orm/node-postgres';

const db = drizzle(pool, {
  logger: {
    logQuery(query, params) {
      console.log('SQL:', query);
      console.log('Params:', params);
    }
  }
});

7. Performance Optimization

7.1 JWKS Caching

Instead of fetching JWKS on every request:

import NodeCache from 'node-cache';

const jwksCache = new NodeCache({ stdTTL: 3600 }); // 1 hour

async function getJWKS() {
  let jwks = jwksCache.get('jwks');

  if (!jwks) {
    const response = await fetch(process.env.AUTH_SERVER_JWKS_URL!);
    jwks = await response.json();
    jwksCache.set('jwks', jwks);
  }

  return jwks;
}

7.2 Database Connection Pooling

import { Pool } from 'pg';

const pool = new Pool({
  host: process.env.DB_HOST,
  port: parseInt(process.env.DB_PORT || '5432'),
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,
  max: 20, // Maximum pool size
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000
});

7.3 Token Cleanup Job

Remove expired tokens periodically:

import cron from 'node-cron';

// Run every hour
cron.schedule('0 * * * *', async () => {
  await db.delete(authorizationCodes)
    .where(lt(authorizationCodes.expiresAt, new Date()));

  await db.delete(accessTokens)
    .where(lt(accessTokens.expiresAt, new Date()));

  await db.delete(refreshTokens)
    .where(
      and(
        eq(refreshTokens.revoked, true),
        lt(refreshTokens.expiresAt, new Date())
      )
    );

  console.log('Cleaned up expired tokens');
});

8. Security Considerations

8.1 Environment Variables

Never commit secrets:

# .gitignore
.env
.env.local
.env.production
keys/*.key

Use secrets management in production:

  • AWS Secrets Manager
  • HashiCorp Vault
  • Azure Key Vault

8.2 Rate Limiting

Prevent abuse:

import rateLimit from 'express-rate-limit';

const tokenLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 5, // 5 requests per window
  message: { error: 'too_many_requests' },
  standardHeaders: true,
  legacyHeaders: false
});

app.use('/oauth/token', tokenLimiter);

const authorizeLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 10
});

app.use('/oauth/authorize', authorizeLimiter);

8.3 SQL Injection Prevention

Always use parameterized queries (Drizzle handles this):

// SAFE - Drizzle uses parameterized queries
await db.select().from(users).where(eq(users.userId, userId));

// UNSAFE - Never do this
await db.execute(`SELECT * FROM users WHERE user_id = '${userId}'`);

8.4 XSS Prevention

Sanitize user input in consent screens:

import DOMPurify from 'isomorphic-dompurify';

res.render('consent', {
  client: {
    clientName: DOMPurify.sanitize(client.clientName)
  }
});

9. Production Deployment

9.1 Docker Setup

Dockerfile (Auth Server):

FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .
RUN npm run build

EXPOSE 3000

CMD ["node", "dist/server.js"]

docker-compose.yml:

version: '3.8'

services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: oauth_app
      POSTGRES_USER: oauth_user
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

  auth-server:
    build: ./packages/auth-server
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgresql://oauth_user:${DB_PASSWORD}@postgres:5432/oauth_app
      GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID}
      GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET}
    depends_on:
      - postgres

  mcp-server:
    build: ./packages/mcp-server
    ports:
      - "3001:3001"
    environment:
      AUTH_SERVER_JWKS_URL: https://auth.yourdomain.com/.well-known/jwks.json
    depends_on:
      - auth-server

volumes:
  postgres_data:

9.2 Deploy to Railway/Render

Railway:

# Install Railway CLI
npm i -g @railway/cli

# Login
railway login

# Initialize project
railway init

# Deploy
railway up

Render:

  1. Connect GitHub repo
  2. Create Blueprint:

render.yaml:

services:
  - type: web
    name: auth-server
    env: node
    buildCommand: cd packages/auth-server && npm install && npm run build
    startCommand: cd packages/auth-server && npm start
    envVars:
      - key: DATABASE_URL
        fromDatabase:
          name: oauth-db
          property: connectionString
      - key: GITHUB_CLIENT_ID
        sync: false
      - key: GITHUB_CLIENT_SECRET
        sync: false

  - type: web
    name: mcp-server
    env: node
    buildCommand: cd packages/mcp-server && npm install && npm run build
    startCommand: cd packages/mcp-server && npm start

databases:
  - name: oauth-db
    databaseName: oauth_app
    user: oauth_user

9.3 SSL/TLS Setup

Use Letโ€™s Encrypt with Caddy:

Caddyfile:

auth.yourdomain.com {
  reverse_proxy localhost:3000
}

mcp.yourdomain.com {
  reverse_proxy localhost:3001
}

10. Variations & Extensions

10.1 Multi-Provider Support

Support multiple OAuth providers (GitHub, Google, etc.):

// Add provider selection to consent screen
const providers = ['github', 'google', 'azure'];

app.get('/oauth/authorize', (req, res) => {
  res.render('select-provider', { providers });
});

app.post('/oauth/authorize/provider', (req, res) => {
  const { provider } = req.body;
  req.session.selectedProvider = provider;
  res.redirect(`/auth/${provider}`);
});

10.2 Token Introspection Endpoint

Allow resource servers to validate tokens:

app.post('/oauth/introspect', async (req, res) => {
  const { token } = req.body;

  try {
    const payload = await tokenService.validateAccessToken(token);

    res.json({
      active: true,
      sub: payload.sub,
      scope: payload.scope,
      exp: payload.exp
    });
  } catch {
    res.json({ active: false });
  }
});

10.3 Token Revocation

Allow users to revoke access:

app.post('/oauth/revoke', async (req, res) => {
  const { token, token_type_hint } = req.body;

  if (token_type_hint === 'access_token') {
    await db.delete(accessTokens).where(eq(accessTokens.tokenId, token));
  } else if (token_type_hint === 'refresh_token') {
    await db.update(refreshTokens)
      .set({ revoked: true })
      .where(eq(refreshTokens.tokenId, token));
  }

  res.status(200).send();
});

10.4 Scoped Permissions UI

Show detailed permission descriptions:

const scopeDescriptions = {
  'read:repos': {
    title: 'View your repositories',
    description: 'Read metadata, code, and commit history'
  },
  'write:issues': {
    title: 'Manage issues and pull requests',
    description: 'Create, edit, and comment on issues and PRs'
  },
  'read:user': {
    title: 'Access your profile information',
    description: 'Read your username, email, and avatar'
  }
};

11. Additional Resources

11.1 Official Documentation

11.2 Books

  • โ€œOAuth 2 in Actionโ€ by Justin Richer & Antonio Sanso
  • โ€œAPI Security in Actionโ€ by Neil Madden
  • โ€œDesigning Secure Softwareโ€ by Loren Kohnfelder

11.3 Tools & Libraries

  • jose: JWT library for Node.js
  • Passport.js: Authentication middleware
  • Auth0: Managed OAuth provider
  • Okta: Enterprise identity platform

11.4 Security Resources


12. Exercises & Challenges

Exercise 1: Implement Device Flow

Add OAuth 2.0 Device Authorization Grant for CLI tools:

// POST /oauth/device/code
{
  "device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS",
  "user_code": "WDJB-MJHT",
  "verification_uri": "https://example.com/device",
  "expires_in": 900,
  "interval": 5
}

Build a user dashboard to:

  • View all authorized apps
  • Revoke access per app
  • See granted scopes
  • View last access time

Exercise 3: Implement Token Binding

Bind tokens to specific devices/browsers using:

  • TLS channel binding
  • Device fingerprinting
  • Certificate-bound access tokens

Exercise 4: Add Audit Logging

Log all OAuth events:

  • Authorization requests
  • Token grants
  • Token refreshes
  • Token revocations
  • Failed authentication attempts

Exercise 5: Build Admin Dashboard

Create admin UI to:

  • Monitor active sessions
  • View OAuth metrics
  • Manage registered clients
  • Investigate security incidents

13. Summary & Next Steps

What Youโ€™ve Learned

  1. OAuth 2.1 authorization flow with PKCE
  2. Dynamic Client Registration (DCR)
  3. JWT generation and validation
  4. Token lifecycle management
  5. Secure credential storage
  6. WWW-Authenticate responses
  7. Well-known endpoint discovery
  8. Integration with third-party OAuth providers

Key Takeaways

  • Always use PKCE: Protects against code interception
  • Validate everything: Never trust input, always verify tokens
  • Use HTTPS everywhere: OAuth requires secure connections
  • Rotate refresh tokens: Implement token rotation for security
  • Monitor and log: Track OAuth events for security audits
  • Follow specs: OAuth 2.1 and related RFCs are your guide

Production Checklist

  • All secrets in environment variables
  • HTTPS enabled on all endpoints
  • Rate limiting configured
  • JWKS caching implemented
  • Token cleanup job running
  • Database backups configured
  • Monitoring and alerting set up
  • Audit logging enabled
  • Security headers configured
  • CORS properly restricted
  • Input validation on all endpoints
  • Error messages donโ€™t leak sensitive info

Next Steps

  1. Complete Project 7: Real-Time Dashboard App
    • Build on OAuth knowledge with live data
    • Implement WebSocket authentication
  2. Explore Advanced OAuth:
    • Rich Authorization Requests (RAR)
    • Demonstrating Proof of Possession (DPoP)
    • JWT-Secured Authorization Requests (JAR)
  3. Add More Integrations:
    • Google Calendar
    • Salesforce
    • Slack
    • Notion
  4. Study Security:
    • Common OAuth vulnerabilities
    • Penetration testing OAuth flows
    • Security auditing techniques

Congratulations!

Youโ€™ve built a production-ready OAuth 2.1 protected ChatGPT app! This is advanced material that many developers struggle with. You now understand:

  • How modern authentication works
  • How to secure API endpoints
  • How ChatGPT apps handle user identity
  • How to integrate with third-party services

This knowledge is valuable for any web application, not just ChatGPT apps. OAuth is used by virtually every major platform (GitHub, Google, Facebook, Twitter, etc.), and you now know how to implement it correctly.

Ready to publish your app? Move on to Project 9: Full App Store Submission to take this to 800+ million ChatGPT users!