From 356cc3e90fc669d6b34032280afc497114f05724 Mon Sep 17 00:00:00 2001 From: ThisIsDemetrio Date: Fri, 31 Jul 2026 14:26:06 +0200 Subject: [PATCH] fix: remove non-functional OAuth2.1/DCR flow, keep client-credentials and bearer token auth --- README.md | 4 +- default.env | 7 +- package-lock.json | 24 +- package.json | 1 - src/index.ts | 11 - .../auth/clientCredentialsManager.test.ts | 222 --------- src/server/auth/clientCredentialsManager.ts | 134 ------ src/server/auth/oauthRouter.test.ts | 448 ------------------ src/server/auth/oauthRouter.ts | 235 --------- src/server/auth/types.ts | 62 --- src/server/auth/wellKnownRouter.test.ts | 70 --- src/server/auth/wellKnownRouter.ts | 82 ---- src/server/httpserver.test.ts | 2 - src/server/httpserver.ts | 18 +- src/server/utils.ts | 10 - 15 files changed, 9 insertions(+), 1321 deletions(-) delete mode 100644 src/server/auth/clientCredentialsManager.test.ts delete mode 100644 src/server/auth/clientCredentialsManager.ts delete mode 100644 src/server/auth/oauthRouter.test.ts delete mode 100644 src/server/auth/oauthRouter.ts delete mode 100644 src/server/auth/types.ts delete mode 100644 src/server/auth/wellKnownRouter.test.ts delete mode 100644 src/server/auth/wellKnownRouter.ts delete mode 100644 src/server/utils.ts diff --git a/README.md b/README.md index 96f208e..7abf83a 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ To use the Mia-Platform Console MCP Server in your client (such as Visual Studio You may decide to access via: - Service Account to perform machine-2-machine authentication and have full access to the MCP capabilities to perform operations on the Company where the S.A. has been created (for more information, visit [our official documentation on how to create a Mia-Platform Service Account](docs-create-service-account)). If you do so, you need to include the environment variables `MIA_PLATFORM_CLIENT_ID` and `MIA_PLATFORM_CLIENT_SECRET`. -- Using your own credentials: Mia-Platform Console MCP Server follows the [Model Context Protocol specifications on authentication](mcp-specs-auth) using OAuth2.1 and Dynamic Client Registration: clients that follow that specifications will be able to discover the authentication endpoints of the selected Mia-Platform instance you want to access to and guide you to perform the log in. +- Using your own credentials: obtain a valid Mia-Platform Console access token out of band (e.g. from your browser session) and configure your MCP client to send it as a `Bearer` token in the `Authorization` header when calling the `/mcp` endpoint. The server does not perform any authentication flow itself; it forwards the token as-is to the Mia-Platform Console APIs, which validate it. ### How to Run @@ -92,7 +92,6 @@ Environment variables located inside a file named `.env` are automatically inclu | `CONSOLE_HOST` | The host address of the Mia-Platform Console instance | Yes | - | | `MIA_PLATFORM_CLIENT_ID` | Client ID for Service Account authentication | No | - | | `MIA_PLATFORM_CLIENT_SECRET` | Client secret for Service Account authentication | No | - | -| `CLIENT_EXPIRY_DURATION` | Duration in seconds of clients generated with the DCR authentication flow. After this time, the client will be expired and cannot be used anylonger. | No | `300` | ## Local Development @@ -145,7 +144,6 @@ node --test --import tsx [build-svg]: https://img.shields.io/github/actions/workflow/status/mia-platform/console-mcp-server/build-and-test.yaml [license-svg]: https://img.shields.io/github/license/mia-platform/console-mcp-server [mcp-intro]: https://modelcontextprotocol.io/introduction -[mcp-specs-auth]: https://modelcontextprotocol.io/specification/2025-06-18 [Docker]: https://www.docker.com/ [20-setup]: https://docs.mia-platform.eu/docs/products/console/mcp/mcp-server/setup [docs-create-service-account]: https://docs.mia-platform.eu/docs/development_suite/identity-and-access-management/manage-service-accounts diff --git a/default.env b/default.env index 902c402..8428ca4 100644 --- a/default.env +++ b/default.env @@ -8,10 +8,7 @@ CONSOLE_HOST= # If you have a Mia-Platform Service Account on your company, please include # the clientId and clientSecret here to perform M2M authentication. -# If these values are absent, OAuth2 authentication flow will be performed. +# If these values are absent, requests to /mcp must carry a valid bearer token +# in the Authorization header, obtained out of band. MIA_PLATFORM_CLIENT_ID= MIA_PLATFORM_CLIENT_SECRET= - -# In case OAuth2 authentication, you can set the time in seconds in which the client credentials -# generated will expire (optional; default: 300) -CLIENT_EXPIRY_DURATION=300 diff --git a/package-lock.json b/package-lock.json index cf6ba3f..5c57f04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "1.2.2", "license": "Apache-2.0", "dependencies": { - "@fastify/formbody": "^8.0.2", "@mia-platform/console-types": "^0.39.4", "@modelcontextprotocol/sdk": "^1.19.1", "commander": "^14.0.1", @@ -715,26 +714,6 @@ "fast-json-stringify": "^6.0.0" } }, - "node_modules/@fastify/formbody": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@fastify/formbody/-/formbody-8.0.2.tgz", - "integrity": "sha512-84v5J2KrkXzjgBpYnaNRPqwgMsmY7ZDjuj0YVuMR3NXCJRCgKEZy/taSP1wUYGn0onfxJpLyRGDLa+NMaDJtnA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "fast-querystring": "^1.1.2", - "fastify-plugin": "^5.0.0" - } - }, "node_modules/@fastify/forwarded": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.1.tgz", @@ -2448,7 +2427,8 @@ "url": "https://opencollective.com/fastify" } ], - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/fastq": { "version": "1.19.1", diff --git a/package.json b/package.json index 3c4287c..4adae86 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,6 @@ }, "homepage": "https://github.com/mia-platform/console-mcp-server#readme", "dependencies": { - "@fastify/formbody": "^8.0.2", "@mia-platform/console-types": "^0.39.4", "@modelcontextprotocol/sdk": "^1.19.1", "commander": "^14.0.1", diff --git a/src/index.ts b/src/index.ts index d126a01..09d8f81 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,13 +18,10 @@ import { Command, Option } from 'commander' import 'dotenv/config' import Fastify from 'fastify' -import formbody from '@fastify/formbody' import { httpServer } from './server/httpserver' -import { oauthRouter } from './server/auth/oauthRouter' import { runStdioServer } from './server/stdio' import { statusRoutes } from './server/statusRoutes' -import { wellKnownRouter } from './server/auth/wellKnownRouter' import { description, version } from '../package.json' const program = new Command() @@ -45,9 +42,6 @@ program. const clientID = process.env.MIA_PLATFORM_CLIENT_ID || '' const clientSecret = process.env.MIA_PLATFORM_CLIENT_SECRET || '' const logLevel = process.env.LOG_LEVEL || 'info' - const clientExpiryDuration = process.env.CLIENT_EXPIRY_DURATION - ? parseInt(process.env.CLIENT_EXPIRY_DURATION, 10) - : undefined if (stdio) { return runStdioServer(host, clientID, clientSecret).catch((error) => { @@ -61,14 +55,9 @@ program. trustProxy: true, }) - // Register plugins - await fastify.register(formbody) - // Registering routes - fastify.register(wellKnownRouter, { prefix: '/', host }) fastify.register(statusRoutes, { prefix: '/-/' }) fastify.register(httpServer, { prefix: '/console-mcp-server', host, clientID, clientSecret }) - fastify.register(oauthRouter, { prefix: '/console-mcp-server/oauth', host, clientExpiryDuration }) return fastify.listen({ port: parseInt(port, 10), host: serverHost }, function (err) { if (err) { diff --git a/src/server/auth/clientCredentialsManager.test.ts b/src/server/auth/clientCredentialsManager.test.ts deleted file mode 100644 index f25355a..0000000 --- a/src/server/auth/clientCredentialsManager.test.ts +++ /dev/null @@ -1,222 +0,0 @@ -// Copyright Mia srl -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import assert from 'node:assert' -import { afterEach, beforeEach, describe, test } from 'node:test' - -import { ClientCredentialsManager } from './clientCredentialsManager' - -describe('ClientCredentialsManager', () => { - let manager: ClientCredentialsManager - - /** Default implementation of the Date.now, which is going to be mocked at every test */ - const originalDateNow: () => number = Date.now - const fixedTime = 1000000000000 - - beforeEach(() => { - manager = new ClientCredentialsManager() - - Date.now = () => fixedTime - }) - - afterEach(() => { - Date.now = originalDateNow - }) - - test('should generate valid credentials', () => { - const credentials = manager.generateCredentials() - - assert.ok(credentials.clientId) - assert.ok(credentials.clientSecret) - assert.ok(credentials.createdAt) - assert.ok(credentials.expiresAt) - assert.strictEqual(typeof credentials.clientId, 'string') - assert.strictEqual(typeof credentials.clientSecret, 'string') - assert.strictEqual(typeof credentials.createdAt, 'number') - assert.strictEqual(typeof credentials.expiresAt, 'number') - }) - - test('should set expiration time to default value (300 seconds) from creation', () => { - const credentials = manager.generateCredentials() - const currentTime = Date.now() - - // Ensure createdAt is recent - assert.ok(currentTime - credentials.createdAt <= 100) - assert.strictEqual(credentials.expiresAt - credentials.createdAt, 300 * 1000) - }) - - test('should create credentials object from a provided client ID', () => { - const providedClientId = 'custom-client-id' - const credentials = manager.generateCredentials(providedClientId) - - assert.strictEqual(credentials.clientId, providedClientId) - }) - - test('should generate unique credentials each time', () => { - const credentials1 = manager.generateCredentials() - const credentials2 = manager.generateCredentials() - - assert.notStrictEqual(credentials1.clientId, credentials2.clientId) - assert.notStrictEqual(credentials1.clientSecret, credentials2.clientSecret) - }) - - test('should return credentials for valid client ID', () => { - const generated = manager.generateCredentials() - const retrieved = manager.getCredentials(generated.clientId) - - assert.ok(retrieved) - assert.strictEqual(retrieved.clientId, generated.clientId) - assert.strictEqual(retrieved.clientSecret, generated.clientSecret) - }) - - test('should return null for non-existent client ID', () => { - const retrieved = manager.getCredentials('non-existent-id') - - assert.strictEqual(retrieved, null) - }) - - test('should return null for expired credentials', () => { - const generated = manager.generateCredentials() - - // Token should be expired 301 seconds later - Date.now = () => fixedTime + 301 * 1000 - - const retrieved = manager.getCredentials(generated.clientId) - assert.strictEqual(retrieved, null) - }) - - test('should return null for expired client ID if the manager has custom expiry duration', () => { - // Create a manager with 10 seconds expiry duration - const shortLivedManager = new ClientCredentialsManager(10) - const generated = shortLivedManager.generateCredentials() - - // Token should be expired 11 seconds later - Date.now = () => fixedTime + 11 * 1000 - - const retrieved = shortLivedManager.getCredentials(generated.clientId) - assert.strictEqual(retrieved, null) - }) - - test('should add state to existing credentials', () => { - const credentials = manager.generateCredentials() - const state = 'test-state-value' - - const success = manager.addState(credentials.clientId, state) - - assert.strictEqual(success, true) - }) - - test('should return false for non-existent client ID', () => { - const success = manager.addState('non-existent-id', 'test-state') - - assert.strictEqual(success, false) - }) - - test('should return false for expired credentials', () => { - const credentials = manager.generateCredentials() - - // Token should be expired 301 seconds later - Date.now = () => fixedTime + 301 * 1000 - - const success = manager.addState(credentials.clientId, 'test-state') - assert.strictEqual(success, false) - }) - - test('should not update state if called a second time', () => { - const credentials = manager.generateCredentials() - - const success1 = manager.addState(credentials.clientId, 'state-1') - assert.strictEqual(success1, true) - - const success2 = manager.addState(credentials.clientId, 'state-2') - assert.strictEqual(success2, false) - - const storedData = manager.getStoredClientIdAndState(credentials.clientId) - assert.strictEqual(storedData?.state, 'state-1') - }) - - test('should return client ID and state when both exist', () => { - const credentials = manager.generateCredentials() - const state = 'test-state-value' - - manager.addState(credentials.clientId, state) - const retrieved = manager.getStoredClientIdAndState(credentials.clientId) - - assert.strictEqual(retrieved?.clientId, credentials.clientId) - assert.strictEqual(retrieved?.state, state) - }) - - test('should return null for non-existent client ID', () => { - const retrieved = manager.getStoredClientIdAndState('non-existent-id') - - assert.strictEqual(retrieved, null) - }) - - test('should return null when credentials exist but no state', () => { - const credentials = manager.generateCredentials() - const retrieved = manager.getStoredClientIdAndState(credentials.clientId) - - assert.strictEqual(retrieved, null) - }) - - test('should return null for expired credentials', () => { - const credentials = manager.generateCredentials() - manager.addState(credentials.clientId, 'test-state') - - // Token should be expired 301 seconds later - Date.now = () => fixedTime + 301 * 1000 - - const retrieved = manager.getStoredClientIdAndState(credentials.clientId) - - assert.strictEqual(retrieved, null) - }) - - test('should handle mixed expired and valid credentials', () => { - const credentials1 = manager.generateCredentials() - - // Token should be still valid 30 seconds later - Date.now = () => fixedTime + 30 * 1000 - - const credentials2 = manager.generateCredentials() - - // 301 seconds later the first token will be expired, the second still valid - Date.now = () => fixedTime + 301 * 1000 - - const result1 = manager.getCredentials(credentials1.clientId) - assert.strictEqual(result1, null) - - const result2 = manager.getCredentials(credentials2.clientId) - assert.ok(result2) - }) - - test('should clear all stored credentials', () => { - const credentials1 = manager.generateCredentials() - const credentials2 = manager.generateCredentials() - - assert.ok(manager.getCredentials(credentials1.clientId)) - assert.ok(manager.getCredentials(credentials2.clientId)) - - manager.destroy() - - assert.strictEqual(manager.getCredentials(credentials1.clientId), null) - assert.strictEqual(manager.getCredentials(credentials2.clientId), null) - }) - - test('should work with empty credentials store', () => { - assert.doesNotThrow(() => { - manager.destroy() - }) - }) -}) diff --git a/src/server/auth/clientCredentialsManager.ts b/src/server/auth/clientCredentialsManager.ts deleted file mode 100644 index 5f13fb0..0000000 --- a/src/server/auth/clientCredentialsManager.ts +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright Mia srl -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { randomBytes, randomUUID } from 'crypto' - -import { ClientCredentials } from './types' - -/** - * This is a class to handle the client credentials generated during the Dynamic Client Registration flow - * of the OAuth2 authentication process to the Mia-Platform MCP Server. The credentials are stored in memory - * and have a short expiration time (configurable via env var; default: 5 minutes) to enhance security. The manager caches the credentials - * that are going to be used to authenticate using Mia-Platform authentication server. - */ -export class ClientCredentialsManager { - private credentials: Map = new Map() - expiryDuration: number = 300 * 1000 - - constructor (expiryDuration?: number) { - if (expiryDuration) { - this.expiryDuration = expiryDuration * 1000 - } - } - - generateCredentials (providedClientId?: string): ClientCredentials { - const clientId = providedClientId ?? this.generateClientId() - const clientSecret = this.generateClientSecret() - const createdAt = Date.now() - const expiresAt = createdAt + this.expiryDuration - - const clientCredential: ClientCredentials = { - clientId, - clientSecret, - createdAt, - expiresAt, - } - - this.credentials.set(clientId, clientCredential) - this.cleanupExpired() - - return clientCredential - } - - getCredentials (clientId: string): Pick | null { - const credentials = this.credentials.get(clientId) - if (!credentials || this.isExpired(credentials)) { - this.credentials.delete(clientId) - return null - } - - this.resetExpiration(credentials) - return { clientId: credentials.clientId, clientSecret: credentials.clientSecret } - } - - /** - * Add the state to a cached clientId. This is required since Mia-Platform authentication server requires - * the state to perform the `/token` request. - * - * @param clientId the client id to which the state is associated - * @param state the state to be associated with the client id - * @returns `true` if the state was added, `false` otherwise (e.g. if the client id does not exist, is expired or already has a state) - */ - addState (clientId: string, state: string): boolean { - const credentials = this.credentials.get(clientId) - if (!credentials || this.isExpired(credentials)) { - this.cleanupExpired() - return false - } - - if (credentials.state) { - return false - } - - this.resetExpiration(credentials) - credentials.state = state - return true - } - - /** - * Given a clientId, returns the same clientId and the associated state if present. - * - * @param clientId the client id for which to retrieve the stored state - * @returns the client id and the associated state, or `null` if the client id does not exist, is expired or has no state - */ - getStoredClientIdAndState (clientId: string): Pick | null { - const credentials = this.credentials.get(clientId) - if (!credentials || this.isExpired(credentials) || !credentials.state) { - this.cleanupExpired() - return null - } - - this.resetExpiration(credentials) - return { clientId: credentials.clientId, state: credentials.state } - } - - private resetExpiration (credential: ClientCredentials): void { - credential.expiresAt = Date.now() + this.expiryDuration - } - - private generateClientId (): string { - return randomUUID().replace(/-/g, '') - } - - private generateClientSecret (): string { - return randomBytes(32).toString('base64url') - } - - private isExpired (credential: ClientCredentials): boolean { - return Date.now() > credential.expiresAt - } - - private cleanupExpired (): void { - for (const [ clientId, credential ] of this.credentials.entries()) { - if (this.isExpired(credential)) { - this.credentials.delete(clientId) - } - } - } - - destroy (): void { - this.credentials.clear() - } -} diff --git a/src/server/auth/oauthRouter.test.ts b/src/server/auth/oauthRouter.test.ts deleted file mode 100644 index 9d6704a..0000000 --- a/src/server/auth/oauthRouter.test.ts +++ /dev/null @@ -1,448 +0,0 @@ -// Copyright Mia srl -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { afterEach, beforeEach, mock, suite, test } from 'node:test' -import Fastify, { FastifyInstance } from 'fastify' -import { MockAgent, setGlobalDispatcher } from 'undici' - -import { ClientCredentialsManager } from './clientCredentialsManager' -import { oauthRouter } from './oauthRouter' - -suite('OAuth Router', () => { - const testHost = 'https://test.mia-platform.eu' - let fastify: FastifyInstance - let agent: MockAgent - - beforeEach(async () => { - agent = new MockAgent() - agent.disableNetConnect() - setGlobalDispatcher(agent) - - mock.method(ClientCredentialsManager.prototype, 'generateCredentials', () => ({ - clientId: 'test-client-id', - clientSecret: 'test-client-secret', - createdAt: Date.now(), - expiresAt: Date.now() + 300000, - })) - - mock.method(ClientCredentialsManager.prototype, 'getCredentials', () => ({ - clientId: 'test-client-id', - clientSecret: 'test-client-secret', - })) - - mock.method(ClientCredentialsManager.prototype, 'addState', () => true) - - mock.method(ClientCredentialsManager.prototype, 'getStoredClientIdAndState', () => ({ - clientId: 'test-client-id', - state: 'test-state', - })) - - mock.method(ClientCredentialsManager.prototype, 'destroy', () => undefined) - - fastify = Fastify({ logger: false }) - await fastify.register(oauthRouter, { host: testHost }) - }) - - afterEach(async () => { - await fastify.close() - mock.restoreAll() - }) - - suite('POST /register', () => { - test('should register client with minimal required fields', async (t) => { - const response = await fastify.inject({ - method: 'POST', - path: '/register', - payload: { - redirect_uris: [ 'https://example.com/callback' ], - }, - }) - - t.assert.equal(response.statusCode, 201) - const body = response.json() - t.assert.equal(body.client_id, 'test-client-id') - t.assert.equal(body.client_secret, 'test-client-secret') - t.assert.deepEqual(body.redirect_uris, [ 'https://example.com/callback' ]) - }) - - test('should return 400 when redirect_uris is missing', async (t) => { - const response = await fastify.inject({ - method: 'POST', - path: '/register', - payload: {}, - }) - - t.assert.equal(response.statusCode, 400) - const body = response.json() - t.assert.equal(body.error, 'invalid_client_metadata') - t.assert.equal(body.error_description, '"redirect_uris" is required and must be an array') - }) - - test('should return 400 when redirect_uris is not an array', async (t) => { - const response = await fastify.inject({ - method: 'POST', - path: '/register', - payload: { - redirect_uris: 'https://example.com/callback', - }, - }) - - t.assert.equal(response.statusCode, 400) - const body = response.json() - t.assert.equal(body.error, 'invalid_client_metadata') - }) - }) - - suite('GET /authorize', () => { - test('should redirect to auth server when all parameters are valid', async (t) => { - agent.get(testHost).intercept({ - path: '/api/authorize?appId=console-mcp-server&providerId=okta&redirect=https%3A%2F%2Fexample.com%2Fcallback&state=test-state', - method: 'GET', - }).reply(302, '', { headers: { location: 'https://auth.example.com/oauth/authorize' } }) - - const response = await fastify.inject({ - method: 'GET', - path: '/authorize', - query: { - client_id: 'test-client-id', - response_type: 'code', - redirect_uri: 'https://example.com/callback', - scope: 'openid', - state: 'test-state', - code_challenge: 'challenge', - code_challenge_method: 'S256', - }, - }) - - t.assert.equal(response.statusCode, 302) - t.assert.equal(response.headers.location, 'https://auth.example.com/oauth/authorize') - }) - - test('should return 400 when client_id is missing', async (t) => { - const response = await fastify.inject({ - method: 'GET', - path: '/authorize', - query: {}, - }) - - t.assert.equal(response.statusCode, 400) - const body = response.json() - t.assert.equal(body.error, 'invalid_request') - t.assert.equal(body.error_description, 'client_id is required') - }) - - test('should return 400 when client_id is invalid', async (t) => { - mock.method(ClientCredentialsManager.prototype, 'getCredentials', () => null, { times: 1 }) - - const response = await fastify.inject({ - method: 'GET', - path: '/authorize', - query: { - client_id: 'invalid-client-id', - }, - }) - - t.assert.equal(response.statusCode, 400) - const body = response.json() - t.assert.equal(body.error, 'invalid_client') - t.assert.equal(body.error_description, 'Invalid or expired client_id') - }) - - test('should return 400 when state storage fails', async (t) => { - mock.method(ClientCredentialsManager.prototype, 'addState', () => false, { times: 1 }) - - const response = await fastify.inject({ - method: 'GET', - path: '/authorize', - query: { - client_id: 'test-client-id', - state: 'test-state', - }, - }) - - t.assert.equal(response.statusCode, 400) - const body = response.json() - t.assert.equal(body.error, 'invalid_client') - }) - - test('should return 500 when auth server request fails', async (t) => { - agent.get(testHost).intercept({ - path: '/api/authorize?appId=console-mcp-server&providerId=okta', - method: 'GET', - }).replyWithError(new Error('Network error')) - - const response = await fastify.inject({ - method: 'GET', - path: '/authorize', - query: { - client_id: 'test-client-id', - }, - }) - - t.assert.equal(response.statusCode, 500) - const body = response.json() - t.assert.equal(body.error, 'server_error') - t.assert.equal(body.error_description, 'Failed to process authorization request') - }) - }) - - suite('POST /token', () => { - test('should exchange authorization code for a new token', async (t) => { - agent.get(testHost).intercept({ - path: '/api/oauth/token', - method: 'POST', - body: 'code=auth-code&state=test-state', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - }).reply(200, { - accessToken: 'access-token', - refreshToken: 'refresh-token', - expiresAt: 1760900101000, - }) - - const response = await fastify.inject({ - method: 'POST', - path: '/token', - payload: { - grant_type: 'authorization_code', - code: 'auth-code', - client_id: 'test-client-id', - client_secret: 'test-client-secret', - }, - }) - - t.assert.equal(response.statusCode, 200) - const body = response.json() - t.assert.equal(body.access_token, 'access-token') - t.assert.equal(body.refresh_token, 'refresh-token') - t.assert.equal(body.expires_at, 1760900101000) - t.assert.equal(body.token_type, 'Bearer') - }) - - test('should refresh tokens successfully', async (t) => { - agent.get(testHost).intercept({ - path: '/api/refreshToken', - method: 'POST', - body: 'grant_type=refresh_token&refresh_token=old-refresh-token', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - }).reply(200, { - accessToken: 'new-access-token', - refreshToken: 'new-refresh-token', - expiresAt: 1760900101000, - }) - - const response = await fastify.inject({ - method: 'POST', - path: '/token', - payload: { - grant_type: 'refresh_token', - refresh_token: 'old-refresh-token', - }, - }) - - t.assert.equal(response.statusCode, 200) - const body = response.json() - t.assert.equal(body.access_token, 'new-access-token') - t.assert.equal(body.refresh_token, 'new-refresh-token') - t.assert.equal(body.expires_at, 1760900101000) - t.assert.equal(body.token_type, 'Bearer') - }) - - test('should return 401 when client credentials are invalid', async (t) => { - mock.method(ClientCredentialsManager.prototype, 'getCredentials', () => null, { times: 1 }) - - const response = await fastify.inject({ - method: 'POST', - path: '/token', - payload: { - grant_type: 'authorization_code', - code: 'auth-code', - client_id: 'invalid-client-id', - client_secret: 'invalid-secret', - }, - }) - - t.assert.equal(response.statusCode, 401) - const body = response.json() - t.assert.equal(body.error, 'invalid_client') - t.assert.equal(body.error_description, 'Invalid client credentials') - }) - - test('should return 400 if the grant_type is invalid', async (t) => { - const response = await fastify.inject({ - method: 'POST', - path: '/token', - payload: { - code: 'auth-code', - client_id: 'test-client-id', - client_secret: 'test-client-secret', - }, - }) - - t.assert.equal(response.statusCode, 400) - const body = response.json() - t.assert.equal(body.error, 'unsupported_grant_type') - t.assert.equal(body.error_description, 'Only "authorization_code" and "refresh_token" grant types are supported') - }) - - test('should return 400 when client_secret is missing when requesting a new token', async (t) => { - const response = await fastify.inject({ - method: 'POST', - path: '/token', - payload: { - grant_type: 'authorization_code', - code: 'auth-code', - client_id: 'test-client-id', - }, - }) - - t.assert.equal(response.statusCode, 400) - const body = response.json() - t.assert.equal(body.error, 'invalid_request') - t.assert.equal(body.error_description, 'client_id and client_secret are required') - }) - - test('should return 401 when client_secret does not match', async (t) => { - const response = await fastify.inject({ - method: 'POST', - path: '/token', - payload: { - grant_type: 'authorization_code', - code: 'auth-code', - client_id: 'test-client-id', - client_secret: 'wrong-secret', - }, - }) - - t.assert.equal(response.statusCode, 401) - const body = response.json() - t.assert.equal(body.error, 'invalid_client') - t.assert.equal(body.error_description, 'Invalid client credentials') - }) - - test('should return 400 when no state found for client', async (t) => { - mock.method(ClientCredentialsManager.prototype, 'getStoredClientIdAndState', () => null, { times: 1 }) - - const response = await fastify.inject({ - method: 'POST', - path: '/token', - payload: { - grant_type: 'authorization_code', - code: 'auth-code', - client_id: 'test-client-id', - client_secret: 'test-client-secret', - }, - }) - - t.assert.equal(response.statusCode, 400) - const body = response.json() - t.assert.equal(body.error, 'invalid_request') - t.assert.equal(body.error_description, 'No state found for client_id') - }) - - test('should return 400 when no state in stored client data', async (t) => { - mock.method(ClientCredentialsManager.prototype, 'getStoredClientIdAndState', () => ({ - clientId: 'test-client-id', - state: undefined, - }), { times: 1 }) - - const response = await fastify.inject({ - method: 'POST', - path: '/token', - payload: { - grant_type: 'authorization_code', - code: 'auth-code', - client_id: 'test-client-id', - client_secret: 'test-client-secret', - }, - }) - - t.assert.equal(response.statusCode, 400) - const body = response.json() - t.assert.equal(body.error, 'invalid_request') - t.assert.equal(body.error_description, 'No state found for client_id') - }) - - test('should return 400 in case of token exchange failure from auth server side', async (t) => { - agent.get(testHost).intercept({ - path: '/api/oauth/token', - method: 'POST', - }).reply(400, 'Invalid authorization code') - - const response = await fastify.inject({ - method: 'POST', - path: '/token', - payload: { - grant_type: 'authorization_code', - code: 'invalid-code', - client_id: 'test-client-id', - client_secret: 'test-client-secret', - }, - }) - - t.assert.equal(response.statusCode, 400) - const body = response.json() - t.assert.equal(body.error, 'invalid_request') - t.assert.equal(body.error_description, 'Failed to receive token from Authentication Server') - }) - - test('should return 500 when auth server request fails to get a new token', async (t) => { - agent.get(testHost).intercept({ - path: '/api/oauth/token', - method: 'POST', - }).replyWithError(new Error('Network error')) - - const response = await fastify.inject({ - method: 'POST', - path: '/token', - payload: { - grant_type: 'authorization_code', - code: 'auth-code', - client_id: 'test-client-id', - client_secret: 'test-client-secret', - }, - }) - - t.assert.equal(response.statusCode, 500) - const body = response.json() - t.assert.equal(body.error, 'server_error') - t.assert.equal(body.error_description, 'Failed to process token request (grant type: authorization_code)') - }) - - test('should return 500 when auth server request fails to refresh an token', async (t) => { - agent.get(testHost).intercept({ - path: '/api/refreshToken', - method: 'POST', - }).replyWithError(new Error('Network error')) - - const response = await fastify.inject({ - method: 'POST', - path: '/token', - payload: { - grant_type: 'refresh_token', - refresh_token: 'old-refresh-token', - }, - }) - - t.assert.equal(response.statusCode, 500) - const body = response.json() - t.assert.equal(body.error, 'server_error') - t.assert.equal(body.error_description, 'Failed to process token request (grant type: refresh_token)') - }) - }) -}) diff --git a/src/server/auth/oauthRouter.ts b/src/server/auth/oauthRouter.ts deleted file mode 100644 index 89bc70e..0000000 --- a/src/server/auth/oauthRouter.ts +++ /dev/null @@ -1,235 +0,0 @@ -// Copyright Mia srl -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify' - -import { ClientCredentialsManager } from './clientCredentialsManager' -import { type AuthorizeRequest, type RegisterRequest, type TokenRequest } from './types' - -const OAUTH_AUTHORIZE_PATH = '/api/authorize' -const OAUTH_TOKEN_PATH = '/api/oauth/token' -const OAUTH_REFRESH_TOKEN_PATH = '/api/refreshToken' - -export interface OAuthRouterOptions { - - /** Used by fastify. Defines the prefix where the endpoint defined in oauthRouter will be placed. */ - prefix?: string - - /** The base URL of the Mia-Platform installation to contact for the Authentication. */ - host?: string - - /** Duration, in seconds, after which the client credentials created via `/register` route are deleted. */ - clientExpiryDuration?: number -} - - -export async function oauthRouter (fastify: FastifyInstance, options: OAuthRouterOptions) { - const { host = '', clientExpiryDuration } = options - const clientManager = new ClientCredentialsManager(clientExpiryDuration) - - fastify.post('/register', async (request: FastifyRequest, reply: FastifyReply) => { - const body = request.body as RegisterRequest - - fastify.log.debug({ - message: 'POST /register called', - requestBody: body, - }) - - if (!body.redirect_uris || !Array.isArray(body.redirect_uris)) { - return reply.code(400).send({ - error: 'invalid_client_metadata', - error_description: '"redirect_uris" is required and must be an array', - }) - } - - const { clientId, clientSecret, expiresAt, createdAt } = clientManager.generateCredentials() - reply.code(201).send({ - client_id: clientId, - client_secret: clientSecret, - client_id_issued_at: createdAt, - client_secret_expires_at: expiresAt, - redirect_uris: body.redirect_uris, - grant_types: body.grant_types ?? [ 'authorization_code' ], - response_types: body.response_types ?? [ 'code' ], - client_name: body.client_name ?? 'Unknown Client', - token_endpoint_auth_method: body.token_endpoint_auth_method ?? 'client_secret_basic', - scope: body.scope ?? '', - }) - }) - - fastify.get('/authorize', async (request: FastifyRequest, reply: FastifyReply) => { - const query = request.query as AuthorizeRequest - - fastify.log.debug({ message: 'GET /authorize called', requestQuery: query }) - - if (!query.client_id) { - return reply.code(400).send({ - error: 'invalid_request', - error_description: 'client_id is required', - }) - } - - const credentials = clientManager.getCredentials(query.client_id) - if (!credentials) { - fastify.log.debug({ clientId: query.client_id }, 'Invalid or expired client_id') - return reply.code(400).send({ - error: 'invalid_client', - error_description: 'Invalid or expired client_id', - }) - } - - if (query.state) { - const success = clientManager.addState(query.client_id, query.state) - if (!success) { - fastify.log.debug({ clientId: query.client_id }, 'Failed to store state for client_id') - return reply.code(400).send({ - error: 'invalid_client', - error_description: 'Invalid or expired client_id', - }) - } - } - - const authorizeParams = new URLSearchParams() - authorizeParams.set('appId', 'console-mcp-server') - authorizeParams.set('providerId', 'okta') - - if (query.state) authorizeParams.set('state', query.state) - if (query.redirect_uri) authorizeParams.set('redirect', query.redirect_uri) - - const oktaAuthorizeUrl = new URL(`${OAUTH_AUTHORIZE_PATH}?${authorizeParams.toString()}`, host) - - try { - const response = await fetch(oktaAuthorizeUrl, { - method: 'GET', - redirect: 'manual', - }) - - if (response.status === 302) { - const location = response.headers.get('location') - if (location) { - return reply.code(302).header('location', location).send() - } - } - - return reply.code(response.status).send(await response.text()) - } catch (error) { - fastify.log.error({ error }, 'Failed to call Okta authorize endpoint') - return reply.code(500).send({ - error: 'server_error', - error_description: 'Failed to process authorization request', - }) - } - }) - - fastify.post('/token', async (request: FastifyRequest, reply: FastifyReply) => { - const body = request.body as TokenRequest - fastify.log.debug({ message: 'POST /token called', requestBody: body }) - - if (body.grant_type !== 'authorization_code' && body.grant_type !== 'refresh_token') { - return reply.code(400).send({ - error: 'unsupported_grant_type', - error_description: 'Only "authorization_code" and "refresh_token" grant types are supported', - }) - } - - let url = '' - const requestBody = new URLSearchParams() - - if (body.grant_type === 'authorization_code') { - if (!body.client_id || !body.client_secret) { - return reply.code(400).send({ - error: 'invalid_request', - error_description: 'client_id and client_secret are required', - }) - } - - const credentials = clientManager.getCredentials(body.client_id) - if (!credentials || credentials.clientSecret !== body.client_secret) { - return reply.code(401).send({ - error: 'invalid_client', - error_description: 'Invalid client credentials', - }) - } - - if (!body.code) { - return reply.code(400).send({ - error: 'invalid_request', - error_description: 'code is required', - }) - } - - const storedClientData = clientManager.getStoredClientIdAndState(body.client_id) - if (!storedClientData || !storedClientData.state) { - return reply.code(400).send({ - error: 'invalid_request', - error_description: 'No state found for client_id', - }) - } - - requestBody.set('code', body.code) - requestBody.set('state', storedClientData.state) - url = `${host}${OAUTH_TOKEN_PATH}` - } else { - if (!body.refresh_token) { - return reply.code(400).send({ - error: 'invalid_request', - error_description: 'refresh_token is required', - }) - } - - requestBody.set('grant_type', 'refresh_token') - requestBody.set('refresh_token', body.refresh_token) - if (body.client_id) requestBody.set('client_id', body.client_id) - if (body.client_secret) requestBody.set('client_secret', body.client_secret) - url = `${host}${OAUTH_REFRESH_TOKEN_PATH}` - } - - try { - const response = await fetch(url, { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: requestBody.toString(), - }) - - if (!response.ok) { - const errorText = await response.text() - fastify.log.error({ grantType: body.grant_type, status: response.status, error: errorText }, 'Okta token request failed') - return reply.code(response.status).send({ - error: 'invalid_request', - error_description: 'Failed to receive token from Authentication Server', - }) - } - - const tokenData = await response.json() - - return reply.code(200).send({ - access_token: tokenData.accessToken, - refresh_token: tokenData.refreshToken, - expires_at: tokenData.expiresAt, - token_type: 'Bearer', - }) - } catch (error) { - fastify.log.error({ error, grantType: body.grant_type }, 'Failed to receive token from Authentication Server') - return reply.code(500).send({ - error: 'server_error', - error_description: `Failed to process token request (grant type: ${body.grant_type})`, - }) - } - }) - - fastify.addHook('onClose', async () => { - clientManager.destroy() - }) -} diff --git a/src/server/auth/types.ts b/src/server/auth/types.ts deleted file mode 100644 index f56c7a7..0000000 --- a/src/server/auth/types.ts +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright Mia srl -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -export interface ClientCredentials { - clientId: string - clientSecret: string - createdAt: number - expiresAt: number - state?: string -} -export interface RegisterRequest { - client_id: string - client_secret?: string - client_id_issued_at: number - client_secret_expires_at?: number - redirect_uris: string[] - grant_types?: string[] - response_types?: string[] - client_name?: string - token_endpoint_auth_method?: string - scope?: string -} - -export interface AuthorizeRequest { - client_id: string - response_type?: string - redirect_uri?: string - scope?: string - state?: string - code_challenge?: string - code_challenge_method?: string -} - -export type TokenRequest = AuthCodeTokenRequest | RefreshTokenRequest - -export interface AuthCodeTokenRequest { - grant_type: 'authorization_code' - code: string - client_id: string - client_secret: string - redirect_uri?: string - code_verifier?: string -} - -export interface RefreshTokenRequest { - grant_type: 'refresh_token' - refresh_token: string - client_id?: string - client_secret?: string -} diff --git a/src/server/auth/wellKnownRouter.test.ts b/src/server/auth/wellKnownRouter.test.ts deleted file mode 100644 index 485c4da..0000000 --- a/src/server/auth/wellKnownRouter.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright Mia srl -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { beforeEach, suite, test } from 'node:test' -import Fastify, { FastifyInstance } from 'fastify' - -import { wellKnownRouter } from './wellKnownRouter' - - -suite('./well-known routes', () => { - let fastify: FastifyInstance - beforeEach(async () => { - fastify = Fastify({ - logger: false, - }) - - fastify.register(wellKnownRouter, { host: 'https://my-console-host.com' }) - }) - - test('GET /.well-known/oauth-protected-resource/console-mcp-server', async (t) => { - const response = await fastify.inject({ - method: 'GET', - path: '/.well-known/oauth-protected-resource/console-mcp-server', - }) - - t.assert.equal(response.statusCode, 200) - t.assert.equal(response.headers['content-type'], 'application/json; charset=utf-8') - const body = JSON.parse(response.body) - t.assert.equal(body.resource_name, 'Console MCP Server') - t.assert.equal(body.resource, `http://localhost:80/console-mcp-server/mcp`) - t.assert.equal(body.authorization_servers[0], `http://localhost:80/console-mcp-server`) - t.assert.deepEqual(body.scopes_supported, [ 'profile', 'email', 'openid', 'offline-access' ]) - t.assert.deepEqual(body.bearer_methods_supported, [ 'header' ]) - }) - - test('GET /.well-known/oauth-authorization-server/console-mcp-server', async (t) => { - const response = await fastify.inject({ - method: 'GET', - path: '/.well-known/oauth-authorization-server/console-mcp-server', - }) - - t.assert.equal(response.statusCode, 200) - t.assert.equal(response.headers['content-type'], 'application/json; charset=utf-8') - const body = JSON.parse(response.body) - t.assert.equal(body.issuer, 'https://my-console-host.com') - t.assert.equal(body.authorization_endpoint, `http://localhost:80/console-mcp-server/oauth/authorize`) - t.assert.equal(body.token_endpoint, `http://localhost:80/console-mcp-server/oauth/token`) - t.assert.equal(body.registration_endpoint, `http://localhost:80/console-mcp-server/oauth/register`) - t.assert.deepEqual(body.scopes_supported, [ 'profile', 'email', 'openid', 'offline-access' ]) - t.assert.deepEqual(body.response_types_supported, [ 'code' ]) - t.assert.deepEqual(body.code_challenge_methods_supported, [ 'S256' ]) - t.assert.deepEqual(body.response_modes_supported, [ 'query' ]) - t.assert.deepEqual(body.grant_types_supported, [ - 'authorization_code', - 'refresh_token', - ]) - }) -}) diff --git a/src/server/auth/wellKnownRouter.ts b/src/server/auth/wellKnownRouter.ts deleted file mode 100644 index 9826e39..0000000 --- a/src/server/auth/wellKnownRouter.ts +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright Mia srl -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify' - -import { getBaseUrlFromRequest } from '../utils' - -const BASE_PATH = 'console-mcp-server' -const AUTHORIZE_PATH = `${BASE_PATH}/oauth/authorize` -const TOKEN_PATH = `${BASE_PATH}/oauth/token` -const REGISTER_PATH = `${BASE_PATH}/oauth/register` - -export const OAUTH_PROTECTED_RESOURCE_PATH = `/.well-known/oauth-protected-resource/${BASE_PATH}` -const OAUTH_AUTHORIZATION_SERVER_PATH = `/.well-known/oauth-authorization-server/${BASE_PATH}` -const OAUTH_SCOPES = [ 'profile', 'email', 'openid', 'offline-access' ] - -export async function wellKnownRouter (fastify: FastifyInstance, options: { host?: string }) { - const { host = '' } = options - - fastify.get(OAUTH_PROTECTED_RESOURCE_PATH, async (request: FastifyRequest, reply: FastifyReply) => { - const { body, headers } = request - - fastify.log.debug({ - message: `GET ${OAUTH_PROTECTED_RESOURCE_PATH} called`, - requestBody: body, - requestHeaders: headers, - }) - - const baseUrl = getBaseUrlFromRequest(request) - - reply.send({ - resource_name: 'Console MCP Server', - resource: `${baseUrl}/${BASE_PATH}/mcp`, - authorization_servers: [ `${baseUrl}/${BASE_PATH}` ], - scopes_supported: OAUTH_SCOPES, - bearer_methods_supported: [ 'header' ], - }) - }) - - fastify.get(OAUTH_AUTHORIZATION_SERVER_PATH, async (request: FastifyRequest, reply: FastifyReply) => { - const { body, headers } = request - const baseUrl = getBaseUrlFromRequest(request) - - fastify.log.debug({ - message: `GET ${OAUTH_AUTHORIZATION_SERVER_PATH} called`, - requestBody: body, - requestHeaders: headers, - }) - - reply.send({ - issuer: host, - authorization_endpoint: `${baseUrl}/${AUTHORIZE_PATH}`, - token_endpoint: `${baseUrl}/${TOKEN_PATH}`, - registration_endpoint: `${baseUrl}/${REGISTER_PATH}`, - scopes_supported: OAUTH_SCOPES, - response_types_supported: [ 'code' ], - code_challenge_methods_supported: [ 'S256' ], - response_modes_supported: [ 'query' ], - grant_types_supported: [ - 'authorization_code', - 'refresh_token', - ], - token_endpoint_auth_methods_supported: [ - 'client_secret_basic', - 'client_secret_post', - 'none', - ], - }) - }) -} diff --git a/src/server/httpserver.test.ts b/src/server/httpserver.test.ts index 481602e..ac644f5 100644 --- a/src/server/httpserver.test.ts +++ b/src/server/httpserver.test.ts @@ -117,7 +117,6 @@ suite('test http streaming server', () => { t.assert.equal(firstInit.statusCode, 401) t.assert.match(firstInit.headers['www-authenticate'] as string, /Bearer realm="Console MCP Server"/) - t.assert.match(firstInit.headers['www-authenticate'] as string, /\/.well-known\/oauth-protected-resource\/console-mcp-server"/) }) test('open passive SSE stream with GET /mcp (with auth token)', async (t) => { @@ -149,7 +148,6 @@ suite('test http streaming server', () => { t.assert.equal(response.statusCode, 401) t.assert.match(response.headers['www-authenticate'] as string, /Bearer realm="Console MCP Server"/) - t.assert.match(response.headers['www-authenticate'] as string, /\/\.well-known\/oauth-protected-resource\/console-mcp-server"/) }) test('delete request is not allowed for stateless server', async (t) => { diff --git a/src/server/httpserver.ts b/src/server/httpserver.ts index 85c1730..b67a004 100644 --- a/src/server/httpserver.ts +++ b/src/server/httpserver.ts @@ -19,9 +19,7 @@ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/ import { ErrorCode, JSONRPC_VERSION } from '@modelcontextprotocol/sdk/types.js' import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify' -import { getBaseUrlFromRequest } from './utils' import { getMcpServer } from './server' -import { OAUTH_PROTECTED_RESOURCE_PATH } from './auth/wellKnownRouter' export interface HTTPServerOptions { host: string @@ -29,18 +27,10 @@ export interface HTTPServerOptions { clientSecret: string } -/** Build the WWW-Authenticate header used when the access token is missing. */ -const buildAuthenticateHeader = (baseUrl: string) => { - const resourceMetadataUrl = new URL(OAUTH_PROTECTED_RESOURCE_PATH, baseUrl) - return `Bearer realm="Console MCP Server", error="invalid_request", error_description="No access token was provided in this request", resource_metadata="${resourceMetadataUrl}", endpoint="/mcp"` -} - /** Send a 401 response for missing/invalid token. */ -export const sendMissingToken = (request: FastifyRequest, reply: FastifyReply) => { - const baseUrl = getBaseUrlFromRequest(request) - const headerContent = buildAuthenticateHeader(baseUrl) +export const sendMissingToken = (reply: FastifyReply) => { reply. - header('WWW-Authenticate', headerContent). + header('WWW-Authenticate', 'Bearer realm="Console MCP Server", error="invalid_request", error_description="No access token was provided in this request"'). code(401). send({ jsonrpc: JSONRPC_VERSION, @@ -123,7 +113,7 @@ export function httpServer (fastify: FastifyInstance, opts: HTTPServerOptions) { const token = request.headers['Authorization'] ?? request.headers['authorization'] if (!token) { - sendMissingToken(request, reply) + sendMissingToken(reply) return } @@ -141,7 +131,7 @@ export function httpServer (fastify: FastifyInstance, opts: HTTPServerOptions) { const token = request.headers['Authorization'] ?? request.headers['authorization'] if (!token) { - sendMissingToken(request, reply) + sendMissingToken(reply) return } diff --git a/src/server/utils.ts b/src/server/utils.ts deleted file mode 100644 index 9baa8bb..0000000 --- a/src/server/utils.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { FastifyRequest } from 'fastify' - -export const getBaseUrlFromRequest = (req: FastifyRequest) => { - const { hostname = 'localhost', port = process.env.PORT, protocol = 'https' } = req - const url = port - ? `${hostname}:${port}` - : hostname - - return `${protocol}://${url}` -}