mcp-framework
Version:
Framework for building Model Context Protocol (MCP) servers in Typescript
111 lines (110 loc) • 4.18 kB
JavaScript
import { JWTValidator } from '../validators/jwt-validator.js';
import { IntrospectionValidator, } from '../validators/introspection-validator.js';
import { logger } from '../../core/Logger.js';
export class OAuthAuthProvider {
config;
validator;
constructor(config) {
this.config = {
headerName: 'Authorization',
...config,
};
if (this.config.validation.type === 'jwt') {
if (!this.config.validation.jwksUri) {
throw new Error('OAuth JWT validation requires jwksUri');
}
const jwtConfig = {
jwksUri: this.config.validation.jwksUri,
audience: this.config.validation.audience,
issuer: this.config.validation.issuer,
algorithms: this.config.validation.algorithms || ['RS256', 'ES256'],
};
this.validator = new JWTValidator(jwtConfig);
logger.info('OAuthAuthProvider initialized with JWT validation');
}
else {
if (!this.config.validation.introspection) {
throw new Error('OAuth introspection validation requires introspection config');
}
this.validator = new IntrospectionValidator(this.config.validation.introspection);
logger.info('OAuthAuthProvider initialized with introspection validation');
}
logger.debug(`OAuthAuthProvider config - resource: ${this.config.resource}, auth servers: ${this.config.authorizationServers.join(', ')}`);
}
async authenticate(req) {
try {
logger.debug('OAuth authentication started');
const token = this.extractToken(req);
if (!token) {
logger.warn('No Bearer token found in Authorization header');
return false;
}
this.validateTokenNotInQueryString(req);
const claims = await this.validator.validate(token);
logger.info('OAuth authentication successful');
logger.debug(`Token claims - sub: ${claims.sub}, scope: ${claims.scope || 'N/A'}`);
return {
data: claims,
};
}
catch (error) {
if (error instanceof Error) {
logger.error(`OAuth authentication failed: ${error.message}`);
}
return false;
}
}
getAuthError() {
return {
status: 401,
message: 'Unauthorized',
};
}
getWWWAuthenticateHeader(error, errorDescription) {
let header = `Bearer realm="MCP Server", resource="${this.config.resource}"`;
if (error) {
header += `, error="${error}"`;
}
if (errorDescription) {
header += `, error_description="${errorDescription}"`;
}
return header;
}
getAuthorizationServers() {
return this.config.authorizationServers;
}
getResource() {
return this.config.resource;
}
extractToken(req) {
const authHeader = req.headers[this.config.headerName.toLowerCase()];
if (!authHeader) {
return null;
}
const headerValue = Array.isArray(authHeader) ? authHeader[0] : authHeader;
if (!headerValue) {
return null;
}
const parts = headerValue.split(' ');
if (parts.length !== 2 || parts[0] !== 'Bearer') {
logger.warn(`Invalid Authorization header format: expected 'Bearer <token>'`);
return null;
}
const token = parts[1];
if (!token || token.trim() === '') {
logger.warn('Empty token in Authorization header');
return null;
}
return token;
}
validateTokenNotInQueryString(req) {
if (!req.url) {
return;
}
const url = new URL(req.url, `http://${req.headers.host}`);
if (url.searchParams.has('access_token') || url.searchParams.has('token')) {
logger.error('Security violation: token found in query string');
throw new Error('Tokens in query strings are not allowed');
}
}
}