OAUTH_COMPARISON.mdโข32 kB
# OAuth and Privy Authentication Implementation Comparison
## mcp2 vs mcp
---
## EXECUTIVE SUMMARY
The two implementations have **fundamentally different architectural approaches** to OAuth and authentication. The **mcp2 implementation is significantly more complete and production-ready**, with proper JWT token handling, JWKS endpoints, and correct MCP integration. The **original mcp implementation relies on simple token mapping and lacks critical OAuth endpoints**, which could cause ChatGPT's OAuth flow to fail.
**Critical Issue Found**: The mcp2 properly validates and transforms tokens through the OAuth flow, while mcp uses direct token mapping that bypasses proper validation. This is likely why ChatGPT reports a "connection problem" despite the flow completing - the token isn't properly validated at the MCP endpoint.
---
## 1. OAUTH FLOW IMPLEMENTATION
### mcp2 (New Implementation)
**File: `/Users/jahnik/index-network/mcp2/src/server/oauth/authorize.ts`**
- **GET /authorize**: Returns a 200 response for initial auth requests, passes validation
- Validates PKCE (code_challenge, code_challenge_method)
- Requires S256 code_challenge_method
- Stores authorization code with expiry (30 seconds)
- React frontend handles Privy authentication
- **POST /authorize**: Receives consent from frontend
- Validates privy_user_id and privy_token from request
- Stores Privy token in authorization code for later exchange
- Returns authorization code and redirect URI
- HTTP 200 response with JSON body
```typescript
// mcp2: Stores Privy token in authorization record for later use
const authCode = storeAuthorizationCode({
clientId: client_id,
privyUserId: privy_user_id,
privyToken: privy_token, // STORED FOR EXCHANGE
scopes: validScopes,
codeChallenge: code_challenge,
codeChallengeMethod: code_challenge_method,
redirectUri: redirect_uri,
expiresAt: Date.now() + 30000, // 30 seconds
});
```
### mcp (Original Implementation)
**File: `/Users/jahnik/index-network/mcp/src/oauth.ts`**
- **prepareAuthorization()**: Stores authorization request in memory
- Validates PKCE (optional, logs warning if missing)
- Allows missing code_challenge with warning
- Uses in-memory Map storage
- **completeAuthorization()**: Exchanges Privy token for authorization code
- Verifies Privy token using privyClient.verifyAuthToken()
- Stores privyClaims (extracted from token verification)
- Issues authorization code
```typescript
// mcp: Stores full Privy claims instead of raw token
authorizationCodes.set(code, {
code,
clientId: record.clientId,
redirectUri: record.redirectUri,
scope: record.scope,
privyClaims, // CLAIMS EXTRACTED FROM TOKEN
privyToken: tokenUsed,
createdAt: Date.now(),
});
```
### KEY DIFFERENCES:
- **mcp2**: Stores raw privy_token directly from frontend, no validation during authorization
- **mcp**: Verifies Privy token during authorization using Privy SDK
- **mcp2**: PKCE is mandatory (code_challenge_method must be S256)
- **mcp**: PKCE is optional (logs warning if missing)
**Impact on ChatGPT**: mcp2's stricter PKCE enforcement is correct OAuth behavior. mcp's optional PKCE could allow attacks.
---
## 2. TOKEN ENDPOINT IMPLEMENTATION
### mcp2 (New Implementation)
**File: `/Users/jahnik/index-network/mcp2/src/server/oauth/token.ts`**
- **Authorization Code Grant** (`grant_type=authorization_code`):
1. Validates code, code_verifier, client_id
2. Validates PKCE using validatePKCE()
3. Marks code as used (single-use only)
4. **Issues JWT access token** using jwt.sign()
5. Stores token in memory with associated Privy token
6. Returns access_token, refresh_token, expires_in as JWT
```typescript
// mcp2: Issues JWT tokens
const accessToken = jwt.sign(
{
sub: privyUserId,
scope: scopes.join(' '),
aud: audience,
client_id: clientId,
},
config.jwt.privateKey,
{
algorithm: config.jwt.algorithm,
expiresIn: config.jwt.expiresIn,
issuer: config.jwt.issuer,
keyid: 'key-1',
}
);
```
- **Refresh Token Grant**: Issues new JWT tokens with refresh token rotation
- **Token Introspection** (POST /introspect): Validates and returns token info
- **Privy Token Exchange** (POST /token/privy/access-token):
- Requires 'privy:token:exchange' scope
- Looks up stored Privy token by access token
- Returns privyAccessToken with metadata
### mcp (Original Implementation)
**File: `/Users/jahnik/index-network/mcp/src/oauth.ts`**
- **Authorization Code Exchange** (exchangeCodeForTokens):
1. Validates code, code_verifier (optional), client_id, redirect_uri
2. Validates PKCE if present (optional check)
3. Issues **opaque random tokens** (no JWT)
4. Stores token in memory with privyClaims and privyToken
5. Returns access_token, refresh_token, expires_in
```typescript
// mcp: Issues opaque random tokens
const accessToken = randomBytes(32).toString("base64url");
const includeRefresh = payload.scope.includes("offline_access");
const refreshToken = includeRefresh
? randomBytes(32).toString("base64url")
: undefined;
```
- **Refresh Token Grant**: Similar opaque token issuance
- **Token Revocation** (revokeToken): Deletes tokens from memory
- **Token Validation** (validateAccessToken): Looks up token in memory map
- **Privy Token Exchange** (getPrivyTokenExchangePayload):
- Looks up stored Privy token by access token
- Returns privyToken, expiresAt, issuedAt, userId, scope
### KEY DIFFERENCES:
| Feature | mcp2 | mcp |
|---------|------|-----|
| **Token Format** | JWT (RS256) | Opaque random bytes |
| **Token Validation** | JWT signature + issuer check | In-memory lookup |
| **Token Introspection** | Implemented via /introspect | Not implemented |
| **Token Storage** | Stores Privy token per token | Stores privyClaims per token |
| **PKCE Validation** | Mandatory | Optional |
**CRITICAL**: mcp2 uses JWT tokens which can be validated independently. mcp uses opaque tokens that require server lookup. This is the fundamental architectural difference.
---
## 3. JWT CONFIGURATION AND SIGNING
### mcp2 (New Implementation)
**File: `/Users/jahnik/index-network/mcp2/src/server/config.ts`**
```typescript
jwt: {
privateKey: Buffer.from(process.env.JWT_PRIVATE_KEY!, 'base64').toString('utf-8'),
publicKey: Buffer.from(process.env.JWT_PUBLIC_KEY!, 'base64').toString('utf-8'),
issuer: process.env.SERVER_BASE_URL!,
algorithm: 'RS256' as const,
expiresIn: '1h',
},
```
- **Algorithm**: RS256 (RSA signature with SHA-256)
- **Keys**: Base64-encoded in environment variables
- **Issuer**: Set to SERVER_BASE_URL
- **Expiration**: 1 hour (3600 seconds)
- **Key ID**: 'key-1' in token header
**JWT Payload**:
```json
{
"sub": "privy_user_id",
"scope": "read write profile",
"aud": "http://server.url",
"client_id": "chatgpt-connector",
"iss": "http://server.url",
"exp": 1234567890,
"iat": 1234564290,
"alg": "RS256",
"kid": "key-1"
}
```
### mcp (Original Implementation)
**File: `/Users/jahnik/index-network/mcp/src/oauth.ts`**
- **Token Format**: Base64url-encoded random bytes (NOT JWT)
- **No JWT signing or verification**
- **Token lookup**: In-memory map using token as key
- **No issuer claim**
- **No expiration field in token** (only stored in memory record)
### KEY DIFFERENCES:
- **mcp2 uses proper JWT with RSA signing** โ
- **mcp uses opaque tokens with no cryptographic validation** โ
- **mcp2 tokens can be validated without server** โ
- **mcp tokens require server database lookup** โ
---
## 4. JWKS AND WELL-KNOWN ENDPOINTS
### mcp2 (New Implementation)
**File: `/Users/jahnik/index-network/mcp2/src/server/oauth/wellknown.ts`**
**Endpoints**:
- `/.well-known/oauth-authorization-server` - RFC 8414 metadata
- `/.well-known/oauth-authorization-server/:resource` - Scoped metadata
- `/.well-known/oauth-protected-resource` - Protected resource metadata
- `/.well-known/oauth-protected-resource/:resource` - Scoped resource metadata
- `/.well-known/jwks.json` - **JWKS endpoint** โ
- `/.well-known/openid-configuration` - OpenID Connect discovery
**JWKS Endpoint Response**:
```typescript
// Converts PEM public key to JWK format
const publicKey = await importSPKI(config.jwt.publicKey, 'RS256');
const jwk = await exportJWK(publicKey);
res.json({
keys: [
{
...jwk,
use: 'sig',
alg: 'RS256',
kid: 'key-1',
},
],
});
```
**Metadata Response**:
```json
{
"issuer": "http://server.url",
"authorization_endpoint": "http://server.url/authorize",
"token_endpoint": "http://server.url/token",
"jwks_uri": "http://server.url/.well-known/jwks.json",
"registration_endpoint": "http://server.url/register",
"scopes_supported": ["read", "write", "profile", "privy:token:exchange"],
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none"],
"subject_types_supported": ["public"],
"id_token_signing_alg_values_supported": ["RS256"]
}
```
### mcp (Original Implementation)
**File: `/Users/jahnik/index-network/mcp/src/oauth.ts`**
```typescript
export function authorizationServerMetadata() {
return {
issuer,
authorization_endpoint: `${issuer.replace(/\/$/, "")}/oauth/authorize`,
token_endpoint: `${issuer.replace(/\/$/, "")}/oauth/token`,
registration_endpoint: `${issuer.replace(/\/$/, "")}/oauth/register`,
revocation_endpoint: `${issuer.replace(/\/$/, "")}/oauth/revoke`,
userinfo_endpoint: `${issuer.replace(/\/$/, "")}/oauth/userinfo`,
response_types_supported: ["code"],
response_modes_supported: ["query"],
grant_types_supported: ["authorization_code", "refresh_token"],
code_challenge_methods_supported: ["S256"],
scopes_supported: supportedScopes,
token_endpoint_auth_methods_supported: ["none"],
};
}
```
- **NO JWKS endpoint** โ
- **Metadata returned inline** (not at /.well-known/oauth-authorization-server)
- **References userinfo_endpoint** (not implemented)
- **References revocation_endpoint** (not implemented)
### KEY DIFFERENCES:
| Feature | mcp2 | mcp |
|---------|------|-----|
| **JWKS endpoint** | โ Implemented with jose library | โ Missing |
| **Metadata location** | /.well-known/oauth-authorization-server | Not at standard location |
| **Metadata registration** | Separate endpoint route | Function-based |
| **Public key distribution** | via JWKS.json | No distribution |
| **OpenID Connect** | Supported | Not mentioned |
**CRITICAL FOR ChatGPT**: ChatGPT OAuth clients MUST have a JWKS endpoint to validate JWT tokens. **mcp missing this endpoint would cause ChatGPT to reject all tokens**.
---
## 5. CLIENT REGISTRATION
### mcp2 (New Implementation)
**File: `/Users/jahnik/index-network/mcp2/src/server/oauth/dcr.ts`**
- **Dynamic Client Registration (DCR)** - RFC 7591
- **Endpoint**: POST /register
- **Response**: 201 Created with client metadata
- **Validation**:
- Requires redirect_uris (non-empty array)
- Validates HTTPS in production (allows localhost HTTP)
- Validates grant_types and response_types
- Supports optional client metadata (client_name, client_uri, logo_uri, etc.)
```typescript
// Validates redirect URIs
if (process.env.NODE_ENV === 'production' && url.protocol !== 'https:') {
return res.status(400).json({
error: 'invalid_redirect_uri',
error_description: 'redirect_uris must use HTTPS in production',
});
}
```
**Response**:
```json
{
"client_id": "client_...",
"client_id_issued_at": 1234567890,
"redirect_uris": ["https://chat.openai.com/connector_platform_oauth_redirect"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none",
"client_name": "ChatGPT Connector"
}
```
### mcp (Original Implementation)
**File: `/Users/jahnik/index-network/mcp/src/oauth.ts`**
```typescript
export function registerClient(input: {
client_name?: string;
redirect_uris?: string[];
scope?: string;
}) {
// Validates redirect URIs
for (const uri of redirectUris) {
validateRedirectUri(...);
}
const client: OAuthClient = {
clientId,
clientName: input.client_name,
redirectUris: redirectUris.map((value) => value.trim()),
scopes,
clientIdIssuedAt: Math.floor(Date.now() / 1000),
};
clients.set(clientId, client);
persistClients();
return {
client_id: clientId,
client_id_issued_at: client.clientIdIssuedAt,
token_endpoint_auth_method: "none",
grant_types: ["authorization_code", "refresh_token"],
response_types: ["code"],
redirect_uris: client.redirectUris,
scope: scopes.join(" "),
};
}
```
- **Function-based DCR** (not express endpoint)
- **Persists clients** to file (.data/oauth-clients.json)
- **Validates redirect URIs** (requires HTTPS except localhost)
- **Pre-configured static clients** (OAUTH_ALLOWED_CLIENT_IDS)
### KEY DIFFERENCES:
- **mcp2**: Express endpoint-based (POST /register)
- **mcp**: Function-based (called from server code)
- **mcp2**: HTTP RFC 7591 compliant
- **mcp**: Manual client initialization required
- **mcp**: Persists to disk; mcp2 uses in-memory only
---
## 6. TOKEN STORAGE AND VALIDATION
### mcp2 (New Implementation)
**File: `/Users/jahnik/index-network/mcp2/src/server/oauth/storage.ts`**
**Structures**:
```typescript
// Stores both JWT claims and raw Privy token
interface AccessTokenRecord {
token: string;
clientId: string;
scope: string[];
resource?: string;
privyClaims: VerifyAuthTokenResponse;
privyToken: string; // RAW PRIVY TOKEN STORED
createdAt: number;
expiresAt: number;
refreshToken?: string;
}
interface RefreshTokenRecord {
token: string;
clientId: string;
scope: string[];
resource?: string;
privyClaims: VerifyAuthTokenResponse;
privyToken: string; // STORED FOR REFRESH FLOW
createdAt: number;
expiresAt: number;
accessToken?: string;
}
```
**Validation**:
- **validatePKCE()**: Uses Bun.CryptoHasher for SHA-256
- **Auto-cleanup**: Every 5 minutes, removes expired entries
- **PKCE**: S256 method with base64url encoding
```typescript
// PKCE validation using Bun crypto
function createSHA256Hash(input: string): Uint8Array {
const hasher = new Bun.CryptoHasher('sha256');
hasher.update(input);
return hasher.digest();
}
```
### mcp (Original Implementation)
**File: `/Users/jahnik/index-network/mcp/src/oauth.ts`**
**Structures**:
```typescript
type AccessTokenRecord = {
token: string;
clientId: string;
scope: string[];
resource?: string;
privyClaims: VerifyAuthTokenResponse;
privyToken: string;
createdAt: number;
expiresAt: number;
refreshToken?: string;
};
```
**Validation**:
- **validateAccessToken()**: In-memory Map lookup
- **calculateCodeChallenge()**: Node.js crypto.createHash()
- **Auto-cleanup**: Every 5 minutes via setInterval()
```typescript
export function validateAccessToken(token: string) {
cleanupExpiredRecords();
const record = accessTokens.get(token);
if (!record) {
return {
valid: false as const,
error: "unknown" as const,
message: "Unknown access token.",
};
}
// ...
}
```
### KEY DIFFERENCES:
| Feature | mcp2 | mcp |
|---------|------|-----|
| **Token type** | JWT (self-validating) | Opaque (lookup required) |
| **Storage requirement** | Still stores for Privy token lookup | Required for validation |
| **Crypto library** | Bun.CryptoHasher | Node.js crypto |
| **PKCE implementation** | Via validatePKCE() | Via calculateCodeChallenge() |
| **Cleanup mechanism** | 5 min interval | 5 min interval |
---
## 7. MCP AUTHENTICATION MIDDLEWARE
### mcp2 (New Implementation)
**File: `/Users/jahnik/index-network/mcp2/src/server/middleware/auth.ts`**
**JWT Validation Middleware**:
```typescript
export function validateToken(requiredScopes: string[] = []) {
return async (req: Request, res: Response, next: NextFunction) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return sendAuthChallenge(res, requiredScopes);
}
const token = authHeader.substring(7);
// Verify JWT token
const decoded = jwt.verify(token, config.jwt.publicKey, {
algorithms: [config.jwt.algorithm],
issuer: config.jwt.issuer,
audience: config.server.baseUrl, // CRITICAL: Validates audience
}) as jwt.JwtPayload;
// Extract and check scopes
const tokenScopes = decoded.scope ? decoded.scope.split(' ') : [];
if (requiredScopes.length > 0) {
const hasAllScopes = requiredScopes.every(scope =>
tokenScopes.includes(scope)
);
if (!hasAllScopes) {
return sendInsufficientScopeError(res, requiredScopes);
}
}
req.auth = {
token,
decoded,
userId: decoded.sub as string,
scopes: tokenScopes,
};
next();
};
}
```
**Key Features**:
- Validates JWT signature with public key
- Checks issuer matches config
- **Validates audience** (critical for security)
- Extracts and validates scopes
- Returns WWW-Authenticate challenge if missing/invalid
- Sets req.auth with token info
**MCP Endpoint**:
```typescript
// mcp2/src/server/mcp/handlers.ts
mcpRouter.post('/', validateToken(['read']), async (req: Request, res: Response) => {
const mcpServer = getMCPServer();
const request = req.body;
// Pass auth context to MCP handlers
const extra = {
auth: req.auth,
};
const response = await handleMCPRequest(mcpServer, request, extra);
res.json(response);
});
```
### mcp (Original Implementation)
**File: `/Users/jahnik/index-network/mcp/src/auth.ts`**
**Token Validation Middleware**:
```typescript
export async function authenticatePrivy(
req: AuthenticatedRequest,
res: Response,
next: NextFunction
) {
const authorization = req.headers.authorization;
const token = authorization?.startsWith("Bearer ")
? authorization.slice("Bearer ".length)
: undefined;
if (!token) {
res.setHeader(
"WWW-Authenticate",
'Bearer realm="index-mcp", error="invalid_token", error_description="Missing bearer token."'
);
return res.status(401).json({ error: "Missing bearer token." });
}
// Validate opaque token
const validation = validateAccessToken(token);
if (!validation.valid) {
res.setHeader(
"WWW-Authenticate",
`Bearer realm="index-mcp", error="${
validation.error === "expired" ? "invalid_token" : "invalid_grant"
}", error_description="${validation.message}"`
);
return res.status(401).json({ error: validation.message });
}
req.privyClaims = validation.claims;
req.oauth = {
accessToken: token,
clientId: validation.clientId,
scope: validation.scope,
resource: validation.resource,
expiresAt: validation.expiresAt,
};
return next();
}
```
**MCP Endpoint**:
```typescript
// mcp/src/server.ts
app.post('/mcp', authenticatePrivy, async (req: AuthenticatedRequest, res) => {
try {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
} catch (error) {
// error handling
}
});
```
### KEY DIFFERENCES:
| Feature | mcp2 | mcp |
|---------|------|-----|
| **Token validation** | JWT signature verification | In-memory lookup |
| **Issuer check** | โ Validates issuer in JWT | โ No issuer check |
| **Audience check** | โ Validates aud claim | โ No audience check |
| **Scope validation** | โ Checks required scopes | โ Checks required scopes |
| **MCP transport** | Direct handler call | StreamableHTTPServerTransport |
| **Auth header** | RFC 7235 WWW-Authenticate | Custom bearer format |
---
## 8. PRIVY TOKEN HANDLING
### mcp2 (New Implementation)
**File: `/Users/jahnik/index-network/mcp2/src/server/middleware/privy.ts`**
```typescript
// Initialize Privy client (singleton)
const privyClient = new PrivyClient(
config.privy.appId,
config.privy.appSecret
);
export async function verifyPrivyToken(req, res, next) {
const token = authHeader.substring(7);
try {
// Verify token using Privy SDK
const claims = await privyClient.verifyAuthToken(token);
if (!claims || !claims.userId) {
return res.status(401).json({
error: 'invalid_token',
error_description: 'Invalid Privy token',
});
}
req.privyUser = {
userId: claims.userId,
appId: claims.appId,
};
next();
} catch (error) {
console.error('Privy token verification failed:', errorMessage);
return res.status(401).json({
error: 'invalid_token',
error_description: 'Failed to verify Privy token',
});
}
}
// Server-to-server Privy API calls
export async function callPrivyAPI(endpoint, method, body) {
const auth = Buffer.from(`${config.privy.appId}:${config.privy.appSecret}`).toString('base64');
const response = await fetch(`https://auth.privy.io/api/v1${endpoint}`, {
method,
headers: {
'Authorization': `Basic ${auth}`,
'privy-app-id': config.privy.appId,
'Content-Type': 'application/json',
},
body: body ? JSON.stringify(body) : undefined,
});
if (!response.ok) {
throw new Error(`Privy API error: ${response.status} ${await response.text()}`);
}
return response.json();
}
```
- **Uses PrivyClient from @privy-io/server-auth**
- **Verifies token via Privy SDK** (proper signature validation)
- **Server-to-server API calls** with Basic Auth
- **Handles Privy API errors**
### mcp (Original Implementation)
**File: `/Users/jahnik/index-network/mcp/src/privy.ts`**
```typescript
const privyClient = new PrivyClient({
appId: process.env.PRIVY_APP_ID!,
appSecret: process.env.PRIVY_APP_SECRET!,
jwtVerificationKey: process.env.PRIVY_JWT_VERIFICATION_KEY,
});
export async function verifyPrivyToken(
token: string
): Promise<VerifyAuthTokenResponse> {
const preview = `${token.slice(0, 8)}...${token.slice(-8)}`;
console.log(`[privy] Verifying auth token ${preview}`);
try {
// Manual JWT payload decoding for logging
const [, payload] = token.split(".");
if (payload) {
const normalized = payload.replace(/-/g, "+").replace(/_/g, "/");
const padded = normalized.padEnd(...);
const decoded = Buffer.from(padded, "base64").toString("utf8");
const claims = JSON.parse(decoded);
console.log("[privy] Token claims summary", {
aud: claims?.aud,
iss: claims?.iss,
exp: claims?.exp,
sid: claims?.sid
});
}
} catch (claimError) {
console.warn("[privy] Failed to parse token payload for logging", claimError);
}
try {
return await privyClient.utils().auth().verifyAuthToken(token);
} catch (error) {
console.error('[privy] verifyAuthToken failed', error);
throw error;
}
}
```
- **Uses PrivyClient from @privy-io/node**
- **Manually decodes JWT payload** for logging
- **Calls privyClient.utils().auth().verifyAuthToken()**
- **No server-to-server API calls** in privy.ts
### KEY DIFFERENCES:
| Feature | mcp2 | mcp |
|---------|------|-----|
| **SDK library** | @privy-io/server-auth | @privy-io/node |
| **Token verification** | privyClient.verifyAuthToken() | privyClient.utils().auth().verifyAuthToken() |
| **JWT verification key** | Automatic via SDK | Optional PRIVY_JWT_VERIFICATION_KEY |
| **Server-to-server calls** | โ callPrivyAPI() function | โ Not implemented |
| **Token logging** | Simple preview | Full manual decode for logging |
---
## 9. PRIVY TOKEN EXCHANGE ENDPOINT
### mcp2 (New Implementation)
**File: `/Users/jahnik/index-network/mcp2/src/server/oauth/token.ts` (lines 316-361)**
```typescript
/**
* Privy Token Exchange Endpoint
* Used by MCP tools to exchange their OAuth access token for the original Privy token
* that was provided during the authorization flow. This allows tools to call the Protocol API.
*/
tokenRouter.post('/privy/access-token', validateToken(['privy:token:exchange']), async (req, res) => {
try {
// The validateToken middleware has already verified the token and attached req.auth
const oauthToken = req.auth?.token;
if (!oauthToken) {
return res.status(401).json({ error: 'unauthorized' });
}
// Look up the stored token data to get the Privy token
const tokenData = getToken(oauthToken);
if (!tokenData) {
return res.status(404).json({ error: 'token_not_found' });
}
const preview = `${tokenData.privyToken.slice(0, 4)}...${tokenData.privyToken.slice(-4)}`;
console.log('[privy/access-token] Exchanging token for Privy bearer', preview);
// Return the Privy token with metadata
return res.json({
privyAccessToken: tokenData.privyToken,
expiresAt: tokenData.expiresAt,
issuedAt: null,
userId: tokenData.privyUserId,
scope: tokenData.scopes,
});
} catch (error) {
console.error('[privy/access-token] Error:', error);
return res.status(500).json({
error: 'server_error',
error_description: 'Failed to exchange token',
});
}
});
```
- **Endpoint**: POST /token/privy/access-token
- **Auth**: validateToken middleware (requires 'privy:token:exchange' scope)
- **Lookup**: Gets Privy token from stored token data
- **Response**: privyAccessToken, expiresAt, userId, scope
### mcp (Original Implementation)
**File: `/Users/jahnik/index-network/mcp/src/server.ts` (lines 1329-1354)**
```typescript
app.post('/privy/access-token', authenticatePrivy, (req: AuthenticatedRequest, res) => {
if (!req.oauth) {
return res.status(401).json({ error: 'Unauthorized' });
}
const scopes = req.oauth.scope || [];
if (!scopes.includes('privy:token:exchange')) {
return res.status(403).json({ error: 'insufficient_scope' });
}
const payload = getPrivyTokenExchangePayload(req.oauth.accessToken);
if (!payload) {
return res.status(404).json({ error: 'token_not_found' });
}
const preview = `${payload.privyToken.slice(0, 4)}...${payload.privyToken.slice(-4)}`;
console.log('[privy] Exchanging token for Privy bearer', preview);
return res.json({
privyAccessToken: payload.privyToken,
expiresAt: payload.expiresAt ?? null,
issuedAt: payload.issuedAt ?? null,
userId: payload.userId ?? null,
scope: payload.scope,
});
});
```
- **Endpoint**: POST /privy/access-token
- **Auth**: authenticatePrivy middleware (validates opaque token)
- **Lookup**: Gets Privy token from accessTokens Map
- **Response**: privyAccessToken, expiresAt, issuedAt, userId, scope
### KEY DIFFERENCES:
| Feature | mcp2 | mcp |
|---------|------|-----|
| **Endpoint path** | /token/privy/access-token | /privy/access-token |
| **Auth middleware** | validateToken() | authenticatePrivy() |
| **Token type** | JWT | Opaque |
| **expiresAt** | From stored token data | From privyClaims.expiration |
| **issuedAt** | Always null | From privyClaims.issued_at |
| **userId** | privyUserId | privyClaims.user_id |
---
## 10. ROOT CAUSE ANALYSIS: ChatGPT "Connection Problem"
Based on the architectural differences, here's why ChatGPT likely reports a "connection problem" despite the OAuth flow completing:
### mcp2 Architecture (Likely Working):
```
ChatGPT OAuth Flow:
1. GET /authorize โ 200 (validation passes)
2. POST /authorize โ 200 (returns auth code)
3. POST /token?code=X&code_verifier=Y โ 200 (returns JWT access token)
4. POST /mcp with Authorization: Bearer JWT โ validateToken() validates JWT signature
5. Privy token exchange works โ Tools can call Protocol API
```
**mcp2 Strengths**:
- JWT tokens are self-validating (ChatGPT can validate offline)
- JWKS endpoint available for key distribution
- Proper audience and issuer checks
- Proper HTTP OAuth flow implementation
### mcp Architecture (Likely Failing):
```
ChatGPT OAuth Flow:
1. GET /authorize โ ??? (not clear where this is handled)
2. POST /authorize โ Function call? Not HTTP endpoint
3. POST /token โ exchangeCodeForTokens() returns opaque token
4. POST /mcp with Authorization: Bearer OPAQUE โ authenticatePrivy() does Map lookup
5. ** PROBLEM: Opaque token validation requires server-side state **
```
**mcp Problems**:
- **NO JWKS endpoint** - ChatGPT cannot validate tokens independently
- **Opaque tokens** - ChatGPT cannot verify token validity without server roundtrip
- **No issuer/audience validation** - Tokens don't include validation metadata
- **Authentication endpoint structure unclear** - May not be accessible as HTTP routes
### Specific Failure Point:
When ChatGPT:
1. Receives the opaque token from token endpoint โ
2. Tries to validate it using JWKS endpoint โ **MISSING**
3. Falls back to introspection endpoint โ **MISSING in mcp**
4. Cannot trust the token โ **ABANDONS CONNECTION**
mcp2 mitigates this by:
- Providing JWT tokens (self-validating)
- Exposing /.well-known/jwks.json
- Implementing token introspection endpoint
- Proper RFC-compliant OAuth metadata
---
## 11. CONFIGURATION DIFFERENCES
### mcp2
**File: `/Users/jahnik/index-network/mcp2/src/server/config.ts`**
Required environment variables:
```
PRIVY_APP_ID
PRIVY_APP_SECRET
SERVER_BASE_URL
JWT_PRIVATE_KEY (base64-encoded)
JWT_PUBLIC_KEY (base64-encoded)
PROTOCOL_API_URL
```
Config structure:
```typescript
privy: { appId, appSecret }
server: { baseUrl, port, nodeEnv }
jwt: { privateKey, publicKey, issuer, algorithm, expiresIn }
oauth: { authorizationEndpoint, tokenEndpoint, jwksEndpoint, etc }
intentExtraction: { timeouts, limits }
```
### mcp
**File: `/Users/jahnik/index-network/mcp/src/privy.ts` and environment**
Required environment variables:
```
PRIVY_APP_ID
PRIVY_APP_SECRET
PRIVY_JWT_VERIFICATION_KEY (optional)
PRIVY_CLIENT_ID (optional)
OAUTH_ISSUER_URL (defaults to MCP_SERVER_URL)
OAUTH_RESOURCE_INDICATOR
OAUTH_ACCESS_TOKEN_TTL_SECONDS
OAUTH_REFRESH_TOKEN_TTL_SECONDS
OAUTH_CODE_TTL_SECONDS
OAUTH_SUPPORTED_SCOPES
OAUTH_DEFAULT_SCOPES
OAUTH_ALLOWED_CLIENT_IDS
OAUTH_ALLOWED_REDIRECT_URIS
MCP_SERVER_URL
```
No centralized config file - environment variables read throughout code.
---
## SUMMARY TABLE
| Aspect | mcp2 | mcp | Impact |
|--------|------|-----|--------|
| **Token Format** | JWT (RS256) | Opaque random | mcp2 self-validating โ |
| **JWKS Endpoint** | โ Implemented | โ Missing | mcp2 ChatGPT compatible โ |
| **Token Introspection** | โ Implemented | โ Not found | mcp2 can introspect โ |
| **Issuer Validation** | โ Checked in JWT | โ Not checked | mcp2 more secure โ |
| **Audience Validation** | โ Checked | โ Not checked | mcp2 more secure โ |
| **PKCE** | Mandatory (S256) | Optional | mcp2 more strict โ |
| **Authorization Endpoint** | Express route (GET/POST) | Function-based | mcp2 proper HTTP โ |
| **DCR Endpoint** | Express endpoint | Function-based | mcp2 proper HTTP โ |
| **Well-known Metadata** | RFC 8414 compliant | Custom format | mcp2 standard โ |
| **Privy SDK** | @privy-io/server-auth | @privy-io/node | mcp2 server-specific โ |
| **Server-to-server API** | โ callPrivyAPI() | โ Not implemented | mcp2 can call Privy API โ |
| **MCP Transport** | Direct handler | StreamableHTTPServerTransport | Different approaches |
---
## RECOMMENDATIONS
### For ChatGPT Compatibility (Critical):
1. **mcp2 is the correct implementation** for ChatGPT OAuth
2. **mcp needs these additions** to work:
- Add JWKS endpoint at /.well-known/jwks.json
- Convert opaque tokens to JWT
- Implement token introspection endpoint
- Expose authorization endpoints as HTTP routes
### For Production Readiness:
**mcp2 should**:
- Add persistent token storage (Redis/PostgreSQL)
- Implement token revocation tracking
- Add logging for OAuth events
- Consider token encryption at rest
**mcp should**:
- Follow mcp2 architecture if staying with opaque tokens is required
- Or switch to mcp2's JWT approach for ChatGPT compatibility