From 55ef0f3ca971bc1e4e367e89aff963544787de12 Mon Sep 17 00:00:00 2001 From: jubnl Date: Tue, 5 May 2026 14:41:15 +0200 Subject: [PATCH] feat: add OIDC userinfo endpoint for ChatGPT domain claiming ChatGPT enables OIDC when it finds /.well-known/openid-configuration and uses the userinfo endpoint to fetch the authenticated user's email for authorization domain claiming. - Add GET /oauth/userinfo: validates Bearer token, returns sub/email/ email_verified/preferred_username from the OAuth access token - Add userinfo_endpoint to /.well-known/openid-configuration response - Add /oauth/userinfo to open-CORS pre-middleware --- server/src/app.ts | 12 +++++++++--- server/src/routes/oauth.ts | 24 ++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/server/src/app.ts b/server/src/app.ts index 85e06990..38e70c36 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -108,6 +108,7 @@ export function createApp(): express.Application { req.path.startsWith('/.well-known/') || req.path === '/oauth/register' || req.path === '/oauth/authorize' || + req.path === '/oauth/userinfo' || req.path === '/mcp' ) { cors({ origin: '*', credentials: false })(req, _res, next); @@ -424,10 +425,15 @@ export function createApp(): express.Application { }); // ChatGPT (and other OIDC-first clients) bootstrap OAuth discovery via - // /.well-known/openid-configuration. Serve the same typed AS metadata so - // they can find registration_endpoint, authorization_endpoint, token_endpoint. + // /.well-known/openid-configuration. Serve the AS metadata plus the OIDC + // userinfo_endpoint so ChatGPT can fetch the authenticated user's email + // for authorization domain claiming. app.get('/.well-known/openid-configuration', (_req: Request, res: Response) => { - res.json(getOAuthMetadata()); + const meta = getOAuthMetadata(); + res.json({ + ...meta, + userinfo_endpoint: `${meta.issuer}/oauth/userinfo`, + }); }); // SDK authorize handler: validates OAuth params, calls provider.authorize() which redirects diff --git a/server/src/routes/oauth.ts b/server/src/routes/oauth.ts index 5f70d1b3..91558ff0 100644 --- a/server/src/routes/oauth.ts +++ b/server/src/routes/oauth.ts @@ -20,6 +20,7 @@ import { rotateOAuthClientSecret, listOAuthSessions, revokeSession, + getUserByAccessToken, AuthorizeParams, } from '../services/oauthService'; import { writeAudit, getClientIp, logWarn } from '../services/auditLog'; @@ -153,6 +154,29 @@ oauthPublicRouter.post('/oauth/token', tokenLimiter, (req: Request, res: Respons return res.status(400).json({ error: 'unsupported_grant_type', error_description: `Unsupported grant_type: ${grant_type}` }); }); +// OIDC UserInfo endpoint (RFC 9068 / OpenID Connect Core ยง5.3) +// ChatGPT hits this after OAuth to fetch the authenticated user's email for domain claiming. +oauthPublicRouter.get('/oauth/userinfo', (req: Request, res: Response) => { + if (!isAddonEnabled(ADDON_IDS.MCP)) return res.status(404).end(); + const auth = req.headers['authorization']; + if (!auth || !auth.toLowerCase().startsWith('bearer ')) { + res.set('WWW-Authenticate', 'Bearer realm="TREK MCP"'); + return res.status(401).json({ error: 'invalid_token' }); + } + const token = auth.slice(7); + const info = getUserByAccessToken(token); + if (!info) { + res.set('WWW-Authenticate', 'Bearer realm="TREK MCP", error="invalid_token"'); + return res.status(401).json({ error: 'invalid_token' }); + } + return res.json({ + sub: String(info.user.id), + email: info.user.email, + email_verified: true, + preferred_username: info.user.username, + }); +}); + // Token revocation endpoint (RFC 7009) oauthPublicRouter.post('/oauth/revoke', revokeLimiter, (req: Request, res: Response) => { if (!isAddonEnabled(ADDON_IDS.MCP)) return res.status(404).end();