UNPKG

mcp-framework

Version:

Framework for building Model Context Protocol (MCP) servers in Typescript

131 lines (130 loc) 5.74 kB
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); }); }); } }