Skip to content

Commit 3ac5fc7

Browse files
committed
feat(auth): implement shared authentication handler and OAuth metadata initialization for transport layers
1 parent 7aae521 commit 3ac5fc7

File tree

5 files changed

+207
-207
lines changed

5 files changed

+207
-207
lines changed

src/auth/providers/oauth.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,14 @@ export class OAuthAuthProvider implements AuthProvider {
112112
return header;
113113
}
114114

115+
getAuthorizationServers(): string[] {
116+
return this.config.authorizationServers;
117+
}
118+
119+
getResource(): string {
120+
return this.config.resource;
121+
}
122+
115123
private extractToken(req: IncomingMessage): string | null {
116124
const authHeader = req.headers[this.config.headerName!.toLowerCase()];
117125

src/transports/http/server.ts

Lines changed: 54 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,9 @@ import { JSONRPCMessage, isInitializeRequest } from '@modelcontextprotocol/sdk/t
55
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
66
import { HttpStreamTransportConfig } from './types.js';
77
import { logger } from '../../core/Logger.js';
8-
import { APIKeyAuthProvider } from '../../auth/providers/apikey.js';
9-
import { DEFAULT_AUTH_ERROR } from '../../auth/types.js';
10-
import { getRequestHeader } from '../../utils/headers.js';
11-
import { OAuthAuthProvider } from '../../auth/providers/oauth.js';
128
import { ProtectedResourceMetadata } from '../../auth/metadata/protected-resource.js';
9+
import { handleAuthentication } from '../utils/auth-handler.js';
10+
import { initializeOAuthMetadata } from '../utils/oauth-metadata.js';
1311

1412
export class HttpStreamTransport extends AbstractTransport {
1513
readonly type = 'http-stream';
@@ -31,14 +29,8 @@ export class HttpStreamTransport extends AbstractTransport {
3129
this._endpoint = config.endpoint || '/mcp';
3230
this._enableJsonResponse = config.responseMode === 'batch';
3331

34-
if (this._config.auth?.provider instanceof OAuthAuthProvider) {
35-
const oauthProvider = this._config.auth.provider as OAuthAuthProvider;
36-
this._oauthMetadata = new ProtectedResourceMetadata({
37-
authorizationServers: (oauthProvider as any).config.authorizationServers,
38-
resource: (oauthProvider as any).config.resource,
39-
});
40-
logger.debug('OAuth metadata endpoint enabled for HTTP Stream transport');
41-
}
32+
// Initialize OAuth metadata if OAuth provider is configured
33+
this._oauthMetadata = initializeOAuthMetadata(this._config.auth, 'HTTP Stream');
4234

4335
logger.debug(
4436
`HttpStreamTransport configured with: ${JSON.stringify({
@@ -114,84 +106,73 @@ export class HttpStreamTransport extends AbstractTransport {
114106
const sessionId = req.headers['mcp-session-id'] as string | undefined;
115107
let transport: StreamableHTTPServerTransport;
116108

117-
if (sessionId && this._transports[sessionId]) {
118-
if (this._config.auth?.endpoints?.messages !== false) {
119-
const isAuthenticated = await this.handleAuthentication(req, res, 'message');
120-
if (!isAuthenticated) return;
121-
}
109+
// Determine if this is an initialize request (needs body parsing)
110+
const body = req.method === 'POST' ? await this.readRequestBody(req) : null;
111+
const isInitialize = !sessionId && body && isInitializeRequest(body);
112+
113+
// Perform authentication check once at the beginning
114+
const authEndpoint = isInitialize ? 'sse' : 'messages';
115+
if (this._config.auth?.endpoints?.[authEndpoint] !== false) {
116+
const isAuthenticated = await handleAuthentication(
117+
req,
118+
res,
119+
this._config.auth,
120+
isInitialize ? 'initialize' : 'message'
121+
);
122+
if (!isAuthenticated) return;
123+
}
122124

125+
// Handle different request scenarios
126+
if (sessionId && this._transports[sessionId]) {
127+
// Existing session
123128
transport = this._transports[sessionId];
124129
logger.debug(`Reusing existing session: ${sessionId}`);
125-
} else if (!sessionId && req.method === 'POST') {
126-
const body = await this.readRequestBody(req);
130+
} else if (isInitialize) {
131+
// New session initialization
132+
logger.info('Creating new session for initialization request');
133+
134+
transport = new StreamableHTTPServerTransport({
135+
sessionIdGenerator: () => randomUUID(),
136+
onsessioninitialized: (sessionId: string) => {
137+
logger.info(`Session initialized: ${sessionId}`);
138+
this._transports[sessionId] = transport;
139+
},
140+
enableJsonResponse: this._enableJsonResponse,
141+
});
127142

128-
if (isInitializeRequest(body)) {
129-
if (this._config.auth?.endpoints?.sse) {
130-
const isAuthenticated = await this.handleAuthentication(req, res, 'initialize');
131-
if (!isAuthenticated) return;
143+
transport.onclose = () => {
144+
if (transport.sessionId) {
145+
logger.info(`Transport closed for session: ${transport.sessionId}`);
146+
delete this._transports[transport.sessionId];
132147
}
148+
};
133149

134-
logger.info('Creating new session for initialization request');
135-
136-
transport = new StreamableHTTPServerTransport({
137-
sessionIdGenerator: () => randomUUID(),
138-
onsessioninitialized: (sessionId: string) => {
139-
logger.info(`Session initialized: ${sessionId}`);
140-
this._transports[sessionId] = transport;
141-
},
142-
enableJsonResponse: this._enableJsonResponse,
143-
});
144-
145-
transport.onclose = () => {
146-
if (transport.sessionId) {
147-
logger.info(`Transport closed for session: ${transport.sessionId}`);
148-
delete this._transports[transport.sessionId];
149-
}
150-
};
151-
152-
transport.onerror = (error) => {
153-
logger.error(`Transport error for session: ${error}`);
154-
if (transport.sessionId) {
155-
delete this._transports[transport.sessionId];
156-
}
157-
};
150+
transport.onerror = (error) => {
151+
logger.error(`Transport error for session: ${error}`);
152+
if (transport.sessionId) {
153+
delete this._transports[transport.sessionId];
154+
}
155+
};
158156

159-
transport.onmessage = async (message: JSONRPCMessage) => {
160-
if (this._onmessage) {
161-
await this._onmessage(message);
162-
}
163-
};
164-
165-
await transport.handleRequest(req, res, body);
166-
return;
167-
} else {
168-
if (this._config.auth?.endpoints?.messages !== false) {
169-
const isAuthenticated = await this.handleAuthentication(req, res, 'message');
170-
if (!isAuthenticated) return;
157+
transport.onmessage = async (message: JSONRPCMessage) => {
158+
if (this._onmessage) {
159+
await this._onmessage(message);
171160
}
161+
};
172162

173-
this.sendError(res, 400, -32000, 'Bad Request: No valid session ID provided');
174-
return;
175-
}
163+
await transport.handleRequest(req, res, body);
164+
return;
176165
} else if (!sessionId) {
177-
if (this._config.auth?.endpoints?.messages !== false) {
178-
const isAuthenticated = await this.handleAuthentication(req, res, 'message');
179-
if (!isAuthenticated) return;
180-
}
181-
166+
// No session ID and not an initialize request
182167
this.sendError(res, 400, -32000, 'Bad Request: No valid session ID provided');
183168
return;
184169
} else {
185-
if (this._config.auth?.endpoints?.messages !== false) {
186-
const isAuthenticated = await this.handleAuthentication(req, res, 'message');
187-
if (!isAuthenticated) return;
188-
}
189-
170+
// Session ID provided but not found
190171
this.sendError(res, 404, -32001, 'Session not found');
191172
return;
192173
}
193174

194-
const body = await this.readRequestBody(req);
175+
// Existing session - handle request
195176
await transport.handleRequest(req, res, body);
196177
}
197178

@@ -228,61 +209,6 @@ export class HttpStreamTransport extends AbstractTransport {
228209
);
229210
}
230211

231-
private async handleAuthentication(req: IncomingMessage, res: ServerResponse, context: string): Promise<boolean> {
232-
if (!this._config.auth?.provider) {
233-
return true;
234-
}
235-
236-
const isApiKey = this._config.auth.provider instanceof APIKeyAuthProvider;
237-
if (isApiKey) {
238-
const provider = this._config.auth.provider as APIKeyAuthProvider;
239-
const headerValue = getRequestHeader(req.headers, provider.getHeaderName());
240-
241-
if (!headerValue) {
242-
const error = provider.getAuthError?.() || DEFAULT_AUTH_ERROR;
243-
res.setHeader('WWW-Authenticate', `ApiKey realm="MCP Server", header="${provider.getHeaderName()}"`);
244-
res.writeHead(error.status).end(
245-
JSON.stringify({
246-
error: error.message,
247-
status: error.status,
248-
type: 'authentication_error',
249-
})
250-
);
251-
return false;
252-
}
253-
}
254-
255-
const authResult = await this._config.auth.provider.authenticate(req);
256-
if (!authResult) {
257-
const error = this._config.auth.provider.getAuthError?.() || DEFAULT_AUTH_ERROR;
258-
logger.warn(`Authentication failed for ${context}:`);
259-
logger.warn(`- Client IP: ${req.socket.remoteAddress}`);
260-
logger.warn(`- Error: ${error.message}`);
261-
262-
if (isApiKey) {
263-
const provider = this._config.auth.provider as APIKeyAuthProvider;
264-
res.setHeader('WWW-Authenticate', `ApiKey realm="MCP Server", header="${provider.getHeaderName()}"`);
265-
} else if (this._config.auth.provider instanceof OAuthAuthProvider) {
266-
const provider = this._config.auth.provider as OAuthAuthProvider;
267-
res.setHeader('WWW-Authenticate', provider.getWWWAuthenticateHeader('invalid_token', 'Missing or invalid authentication token'));
268-
}
269-
270-
res.writeHead(error.status).end(
271-
JSON.stringify({
272-
error: error.message,
273-
status: error.status,
274-
type: 'authentication_error',
275-
})
276-
);
277-
return false;
278-
}
279-
280-
logger.info(`Authentication successful for ${context}:`);
281-
logger.info(`- Client IP: ${req.socket.remoteAddress}`);
282-
logger.info(`- Auth Type: ${this._config.auth.provider.constructor.name}`);
283-
return true;
284-
}
285-
286212
async send(message: JSONRPCMessage): Promise<void> {
287213
if (!this._isRunning) {
288214
logger.warn('Attempted to send message, but HTTP transport is not running');

0 commit comments

Comments
 (0)