mcp-framework
Version:
Framework for building Model Context Protocol (MCP) servers in Typescript
131 lines (130 loc) • 5.74 kB
JavaScript
import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';
import { logger } from '../../core/Logger.js';
export class JWTValidator {
jwksClient;
config;
constructor(config) {
this.config = {
algorithms: config.algorithms || ['RS256', 'ES256'],
cacheTTL: config.cacheTTL || 900000,
rateLimit: config.rateLimit ?? true,
cacheMaxEntries: config.cacheMaxEntries || 5,
...config,
};
this.jwksClient = jwksClient({
jwksUri: this.config.jwksUri,
cache: true,
cacheMaxEntries: this.config.cacheMaxEntries,
cacheMaxAge: this.config.cacheTTL,
rateLimit: this.config.rateLimit,
jwksRequestsPerMinute: this.config.rateLimit ? 10 : undefined,
});
logger.debug(`JWTValidator initialized with JWKS URI: ${this.config.jwksUri}, audience: ${this.config.audience}`);
}
async validate(token) {
try {
logger.debug('Starting JWT validation');
const decoded = jwt.decode(token, { complete: true });
if (!decoded || typeof decoded === 'string') {
throw new Error('Invalid token format: unable to decode');
}
logger.debug(`Token decoded, kid: ${decoded.header.kid}, alg: ${decoded.header.alg}`);
if (!decoded.header.kid) {
throw new Error('Invalid token: missing kid in header');
}
if (!this.config.algorithms.includes(decoded.header.alg)) {
throw new Error(`Invalid token algorithm: ${decoded.header.alg}. Expected one of: ${this.config.algorithms.join(', ')}`);
}
const key = await this.getSigningKey(decoded.header.kid);
logger.debug('Verifying token signature and claims');
const verified = await this.verifyToken(token, key);
logger.debug('JWT validation successful');
return verified;
}
catch (error) {
if (error instanceof Error) {
logger.error(`JWT validation failed: ${error.message}`);
throw error;
}
throw new Error('JWT validation failed: Unknown error');
}
}
async getSigningKey(kid) {
try {
logger.debug(`Fetching signing key for kid: ${kid}`);
const key = await this.jwksClient.getSigningKey(kid);
const publicKey = key.getPublicKey();
logger.debug('Signing key retrieved successfully');
return publicKey;
}
catch (error) {
if (error instanceof Error) {
logger.error(`Failed to fetch signing key: ${error.message}`);
throw new Error(`Failed to fetch signing key: ${error.message}`);
}
throw new Error('Failed to fetch signing key: Unknown error');
}
}
async verifyToken(token, publicKey) {
return new Promise((resolve, reject) => {
const options = {
algorithms: this.config.algorithms,
issuer: this.config.issuer,
complete: false,
};
// Only validate audience if not set to wildcard
if (this.config.audience !== '*') {
options.audience = this.config.audience;
}
jwt.verify(token, publicKey, options, (err, decoded) => {
if (err) {
if (err.name === 'TokenExpiredError') {
logger.warn('Token has expired');
reject(new Error('Token has expired'));
}
else if (err.name === 'JsonWebTokenError') {
logger.warn(`Token verification failed: ${err.message}`);
reject(new Error(`Token verification failed: ${err.message}`));
}
else if (err.name === 'NotBeforeError') {
logger.warn('Token not yet valid (nbf claim)');
reject(new Error('Token not yet valid'));
}
else {
logger.error(`Token verification error: ${err.message}`);
reject(new Error(`Token verification error: ${err.message}`));
}
return;
}
if (!decoded || typeof decoded === 'string') {
reject(new Error('Invalid token payload'));
return;
}
const claims = decoded;
if (!claims.sub) {
reject(new Error('Token missing required claim: sub'));
return;
}
if (!claims.iss) {
reject(new Error('Token missing required claim: iss'));
return;
}
// Only require aud claim if not set to wildcard
if (this.config.audience !== '*' && !claims.aud) {
reject(new Error('Token missing required claim: aud'));
return;
}
if (!claims.exp) {
reject(new Error('Token missing required claim: exp'));
return;
}
const audInfo = claims.aud
? `aud: ${Array.isArray(claims.aud) ? claims.aud.join(', ') : claims.aud}`
: 'aud: <not present - wildcard mode>';
logger.debug(`Token claims validated - sub: ${claims.sub}, iss: ${claims.iss}, ${audInfo}`);
resolve(claims);
});
});
}
}