mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-21 14:21:46 +00:00
feat: migrate OAuth public endpoints to MCP SDK auth handlers
Fixes issue #959 — two bugs causing ChatGPT's custom MCP connector to fail: 1. RFC 9728 path-based PRM: ChatGPT requests /.well-known/oauth-protected-resource/mcp (path-aware URL per RFC 9728 §5). The old TREK handler only registered the base path; requests for the path variant fell through to the SPA catch-all and returned HTML. mcpAuthMetadataRouter registers the path-aware URL automatically. 2. DCR without scope: ChatGPT never sends scope during Dynamic Client Registration (RFC 7591 makes it optional). The old handler returned 400 for missing scope. clientRegistrationHandler accepts it; trekClientsStore.registerClient defaults to ALL_SCOPES when absent, and the user still grants only what they approve at the consent UI (scopeSelectable=true for DCR clients is unchanged). Hybrid approach: SDK handles /.well-known, /oauth/authorize (redirect to consent SPA), and /oauth/register. TREK keeps its own /oauth/token and /oauth/revoke because SDK clientAuth does plain-text secret comparison while TREK uses SHA-256 hashing — incompatible without a full clientAuth rewrite. SPA consent page renamed /oauth/authorize → /oauth/consent to avoid routing conflict with the SDK's backend authorize handler now mounted at that path. Existing URL paths (/oauth/token etc.) are unchanged so active Claude.ai connections are unaffected. Other: lazy-init SDK metadata router so getAppUrl() (DB query) is not called at createApp() time; path-aware mcpAddonGate so only /.well-known returns 404 when MCP is disabled (previously a blanket middleware blocked all routes including static files); /api/oauth mounted before the SDK middleware chain so SPA-facing routes with their own 403 gates are reached correctly.
This commit is contained in:
@@ -103,12 +103,48 @@ describe('GET /.well-known/oauth-authorization-server', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Issue #959 regression tests
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('RFC 9728 — path-based protected resource metadata (issue #959 bug 1)', () => {
|
||||
it('OAUTH-959A — /.well-known/oauth-protected-resource/mcp returns JSON (not SPA HTML)', async () => {
|
||||
const res = await request(app).get('/.well-known/oauth-protected-resource/mcp');
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers['content-type']).toMatch(/json/);
|
||||
expect(res.body.resource).toContain('/mcp');
|
||||
expect(Array.isArray(res.body.authorization_servers)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DCR scope optional — ChatGPT compatibility (issue #959 bug 2)', () => {
|
||||
it('OAUTH-959B — POST /oauth/register without scope field returns 201 with default scopes', async () => {
|
||||
const res = await request(app)
|
||||
.post('/oauth/register')
|
||||
.set('Content-Type', 'application/json')
|
||||
.send({ redirect_uris: ['https://chatgpt.example.com/cb'], token_endpoint_auth_method: 'none' });
|
||||
expect(res.status).toBe(201);
|
||||
expect(res.body.client_id).toBeDefined();
|
||||
expect(typeof res.body.scope).toBe('string');
|
||||
expect(res.body.scope.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('OAUTH-959C — POST /oauth/register with explicit scope registers only requested scopes', async () => {
|
||||
const res = await request(app)
|
||||
.post('/oauth/register')
|
||||
.set('Content-Type', 'application/json')
|
||||
.send({ redirect_uris: ['https://example.com/cb'], token_endpoint_auth_method: 'none', scope: 'trips:read' });
|
||||
expect(res.status).toBe(201);
|
||||
expect(res.body.scope).toBe('trips:read');
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// POST /oauth/token — authorization_code grant
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('POST /oauth/token — authorization_code grant', () => {
|
||||
it('OAUTH-002 — missing client_id/client_secret returns 401 invalid_client', async () => {
|
||||
it('OAUTH-002 — missing client_id returns 401 invalid_client', async () => {
|
||||
const res = await request(app)
|
||||
.post('/oauth/token')
|
||||
.send({ grant_type: 'authorization_code', code: 'x', redirect_uri: 'https://example.com/cb', code_verifier: 'y' });
|
||||
@@ -116,13 +152,12 @@ describe('POST /oauth/token — authorization_code grant', () => {
|
||||
expect(res.body.error).toBe('invalid_client');
|
||||
});
|
||||
|
||||
it('OAUTH-003 — MCP addon disabled returns 403 mcp_disabled', async () => {
|
||||
it('OAUTH-003 — MCP addon disabled returns 404', async () => {
|
||||
isAddonEnabledMock.mockReturnValue(false);
|
||||
const res = await request(app)
|
||||
.post('/oauth/token')
|
||||
.send({ grant_type: 'authorization_code', client_id: 'x', client_secret: 'y', code: 'z', redirect_uri: 'https://r.example.com/cb', code_verifier: 'v' });
|
||||
expect(res.status).toBe(403);
|
||||
expect(res.body.error).toBe('mcp_disabled');
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
|
||||
it('OAUTH-004 — missing code/redirect_uri/code_verifier returns 400 invalid_request', async () => {
|
||||
@@ -211,7 +246,7 @@ describe('POST /oauth/token — authorization_code grant', () => {
|
||||
expect(res.body.error).toBe('invalid_grant');
|
||||
});
|
||||
|
||||
it('OAUTH-008 — wrong client_secret returns 401 invalid_client', async () => {
|
||||
it('OAUTH-008 — wrong client_secret returns 401 invalid_client (timing-safe check)', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const r = createOAuthClient(user.id, 'App', ['https://app.example.com/cb'], ['trips:read']);
|
||||
const { verifier, challenge } = makePkce();
|
||||
@@ -909,7 +944,6 @@ describe('M1 — Cache-Control headers on /oauth/token', () => {
|
||||
.post('/oauth/token')
|
||||
.send({ grant_type: 'authorization_code', client_id: 'x', client_secret: 'y', code: 'z', redirect_uri: 'https://r.example.com/cb', code_verifier: 'v' });
|
||||
expect(res.headers['cache-control']).toBe('no-store');
|
||||
expect(res.headers['pragma']).toBe('no-cache');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user