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();