Skip to main content
Glama
TokenValidator.ts8.76 kB
/** * db-mcp - Token Validator * * JWT access token validation using JWKS for signature verification. * Supports RSA and EC algorithms commonly used with OAuth 2.0. */ import * as jose from 'jose'; import type { TokenValidationResult, TokenClaims, TokenValidatorConfig } from './types.js'; import { InvalidTokenError, TokenExpiredError, InvalidSignatureError, JwksFetchError } from './errors.js'; import { parseScopes } from './scopes.js'; import { createModuleLogger, ERROR_CODES } from '../utils/logger.js'; const logger = createModuleLogger('AUTH'); // ============================================================================= // Token Validator // ============================================================================= /** * JWT Token Validator * * Validates OAuth 2.0 access tokens using JWKS for signature verification. */ export class TokenValidator { /** Resolved configuration with all defaults applied */ private readonly jwksUri: string; private readonly issuer: string; private readonly audience: string; private readonly clockTolerance: number; private readonly jwksCacheTtl: number; private jwks: jose.JWTVerifyGetKey | null = null; private jwksExpiry = 0; constructor(config: TokenValidatorConfig) { this.jwksUri = config.jwksUri; this.issuer = config.issuer; this.audience = config.audience; this.clockTolerance = config.clockTolerance ?? 60; this.jwksCacheTtl = config.jwksCacheTtl ?? 3600; logger.info('INIT', `Token Validator initialized for issuer: ${this.issuer}`); } /** * Validate an access token * * @param token - The JWT access token * @returns Validation result with claims or error */ async validate(token: string): Promise<TokenValidationResult> { try { // Get or refresh JWKS const jwks = this.getJwks(); // Verify the token const { payload } = await jose.jwtVerify(token, jwks, { issuer: this.issuer, audience: this.audience, clockTolerance: this.clockTolerance }); // Extract and normalize claims const claims = this.extractClaims(payload); logger.info('TOKEN_VALID', `Token validated for subject: ${claims.sub}`, { context: { sub: claims.sub, scopes: claims.scopes.length, exp: new Date(claims.exp * 1000).toISOString() } }); return { valid: true, claims }; } catch (error) { return this.handleValidationError(error); } } /** * Get or refresh the JWKS */ private getJwks(): jose.JWTVerifyGetKey { // Check if JWKS is cached and valid if (this.jwks && Date.now() < this.jwksExpiry) { return this.jwks; } logger.info('JWKS_FETCH', `Fetching JWKS from: ${this.jwksUri}`); try { // Create JWKS remote key set this.jwks = jose.createRemoteJWKSet(new URL(this.jwksUri), { cooldownDuration: 30000, // 30 seconds between retries cacheMaxAge: this.jwksCacheTtl * 1000 }); this.jwksExpiry = Date.now() + (this.jwksCacheTtl * 1000); logger.info('JWKS_CACHED', `JWKS cached for ${String(this.jwksCacheTtl)}s`); return this.jwks; } catch (error) { const cause = error instanceof Error ? error : new Error(String(error)); logger.error( ERROR_CODES.AUTH.JWKS_FETCH_FAILED, `Failed to fetch JWKS: ${this.jwksUri}`, { error: cause } ); throw new JwksFetchError(this.jwksUri, cause); } } /** * Extract and normalize token claims */ private extractClaims(payload: jose.JWTPayload): TokenClaims { // Get scopes from 'scope' claim (space-delimited) or 'scopes' claim (array) let scopes: string[] = []; if (typeof payload['scope'] === 'string') { scopes = parseScopes(payload['scope']); } else if (Array.isArray(payload['scopes'])) { scopes = payload['scopes'].filter((s): s is string => typeof s === 'string'); } else if (Array.isArray(payload['scope'])) { scopes = payload['scope'].filter((s): s is string => typeof s === 'string'); } return { sub: payload.sub ?? 'unknown', scopes, exp: payload.exp ?? 0, iat: payload.iat ?? 0, iss: payload.iss, aud: payload.aud, nbf: payload.nbf ?? undefined, jti: payload.jti, client_id: payload['client_id'] as string | undefined, // Include all other claims ...payload }; } /** * Handle validation errors and convert to TokenValidationResult */ private handleValidationError(error: unknown): TokenValidationResult { // Handle jose-specific errors if (error instanceof jose.errors.JWTExpired) { logger.warning( ERROR_CODES.AUTH.TOKEN_EXPIRED, 'Token has expired', { error: error as Error } ); return { valid: false, error: 'Token has expired', errorCode: ERROR_CODES.AUTH.TOKEN_EXPIRED.full }; } if (error instanceof jose.errors.JWTClaimValidationFailed) { logger.warning( ERROR_CODES.AUTH.TOKEN_INVALID, `Token claim validation failed: ${error.message}`, { error } ); return { valid: false, error: `Token claim validation failed: ${error.message}`, errorCode: ERROR_CODES.AUTH.TOKEN_INVALID.full }; } if (error instanceof jose.errors.JWSSignatureVerificationFailed) { logger.warning( ERROR_CODES.AUTH.SIGNATURE_INVALID, 'Token signature verification failed', { error } ); return { valid: false, error: 'Token signature verification failed', errorCode: ERROR_CODES.AUTH.SIGNATURE_INVALID.full }; } if (error instanceof jose.errors.JWKSNoMatchingKey) { logger.warning( ERROR_CODES.AUTH.TOKEN_INVALID, 'No matching key found in JWKS', { error } ); return { valid: false, error: 'No matching key found in JWKS', errorCode: ERROR_CODES.AUTH.TOKEN_INVALID.full }; } // Handle other errors const message = error instanceof Error ? error.message : String(error); logger.error( ERROR_CODES.AUTH.TOKEN_INVALID, `Token validation failed: ${message}`, { error: error instanceof Error ? error : undefined } ); return { valid: false, error: `Token validation failed: ${message}`, errorCode: ERROR_CODES.AUTH.TOKEN_INVALID.full }; } /** * Refresh the JWKS cache */ refreshJwks(): void { this.jwks = null; this.jwksExpiry = 0; this.getJwks(); logger.info('JWKS_REFRESHED', 'JWKS cache refreshed'); } /** * Clear the JWKS cache */ clearCache(): void { this.jwks = null; this.jwksExpiry = 0; logger.info('CACHE_CLEARED', 'Token validator cache cleared'); } /** * Convert a validation error to the appropriate OAuth error class */ static toOAuthError(result: TokenValidationResult): InvalidTokenError | TokenExpiredError | InvalidSignatureError { if (result.errorCode === ERROR_CODES.AUTH.TOKEN_EXPIRED.full) { return new TokenExpiredError(); } if (result.errorCode === ERROR_CODES.AUTH.SIGNATURE_INVALID.full) { return new InvalidSignatureError(); } return new InvalidTokenError(result.error); } } // ============================================================================= // Factory Function // ============================================================================= /** * Create a Token Validator instance */ export function createTokenValidator(config: TokenValidatorConfig): TokenValidator { return new TokenValidator(config); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/neverinfamous/db-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server