Skip to main content
Glama
cameronsjo

MCP Server Template

by cameronsjo
jwt.ts6.69 kB
/** * JWT validation utilities with JWKS support * * Implements OAuth 2.1 / OpenID Connect token validation: * - JWKS key fetching and caching * - JWT signature verification * - Claims validation (iss, aud, exp, nbf) * - Scope verification */ import * as jose from 'jose'; import { createLogger } from './logger.js'; import { AuthenticationError, AuthorizationError } from './errors.js'; const logger = createLogger('jwt'); /** * JWT validation configuration */ export interface JwtConfig { /** JWKS endpoint URL */ jwksUri: string; /** Expected token issuer */ issuer: string; /** Expected audience (resource server identifier) */ audience: string; /** Clock tolerance in seconds for exp/nbf validation */ clockTolerance?: number; /** JWKS cache TTL in milliseconds */ jwksCacheTtl?: number; } /** * Validated JWT claims */ export interface ValidatedClaims { /** Subject (user/client identifier) */ sub: string; /** Issuer */ iss: string; /** Audience */ aud: string | string[]; /** Expiration time */ exp: number; /** Issued at time */ iat: number; /** JWT ID */ jti?: string; /** Scopes (space-separated or array) */ scope?: string | string[]; /** Client ID (for client credentials flow) */ client_id?: string; /** All other claims */ [key: string]: unknown; } /** * JWKS cache entry */ interface JwksCache { jwks: jose.JSONWebKeySet; fetchedAt: number; } const DEFAULT_CLOCK_TOLERANCE = 30; // seconds const DEFAULT_JWKS_CACHE_TTL = 3600_000; // 1 hour /** * JWT Validator with JWKS support */ export class JwtValidator { private readonly config: Required<JwtConfig>; private jwksCache: JwksCache | null = null; private getKey: jose.GetKeyFunction<jose.JWSHeaderParameters, jose.FlattenedJWSInput> | null = null; constructor(config: JwtConfig) { this.config = { clockTolerance: DEFAULT_CLOCK_TOLERANCE, jwksCacheTtl: DEFAULT_JWKS_CACHE_TTL, ...config, }; } /** * Validate a JWT and return the verified claims */ async validate(token: string): Promise<ValidatedClaims> { try { // Get or initialize JWKS const keyGetter = await this.getKeyGetter(); // Verify the token const { payload } = await jose.jwtVerify(token, keyGetter, { issuer: this.config.issuer, audience: this.config.audience, clockTolerance: this.config.clockTolerance, }); // Validate required claims if (!payload.sub) { throw new AuthenticationError('Token missing subject claim'); } logger.debug('JWT validated successfully', { sub: payload.sub, iss: payload.iss, exp: payload.exp, }); return payload as ValidatedClaims; } catch (error) { if (error instanceof jose.errors.JWTExpired) { logger.warning('JWT expired', { error: error.message }); throw new AuthenticationError('Token expired'); } if (error instanceof jose.errors.JWTClaimValidationFailed) { logger.warning('JWT claim validation failed', { error: error.message }); throw new AuthenticationError(`Token validation failed: ${error.message}`); } if (error instanceof jose.errors.JWSSignatureVerificationFailed) { logger.warning('JWT signature verification failed'); throw new AuthenticationError('Invalid token signature'); } if (error instanceof AuthenticationError) { throw error; } logger.error('JWT validation error', { error: String(error) }); throw new AuthenticationError('Token validation failed'); } } /** * Validate token and check for required scopes */ async validateWithScopes(token: string, requiredScopes: string[]): Promise<ValidatedClaims> { const claims = await this.validate(token); if (requiredScopes.length === 0) { return claims; } const tokenScopes = this.extractScopes(claims); const missingScopes = requiredScopes.filter((s) => !tokenScopes.includes(s)); if (missingScopes.length > 0) { logger.warning('Insufficient scopes', { required: requiredScopes, actual: tokenScopes, missing: missingScopes, }); throw new AuthorizationError( `Missing required scopes: ${missingScopes.join(', ')}`, requiredScopes ); } return claims; } /** * Extract scopes from claims (handles both string and array formats) */ private extractScopes(claims: ValidatedClaims): string[] { const scopeClaim = claims.scope; if (!scopeClaim) { return []; } if (Array.isArray(scopeClaim)) { return scopeClaim; } return scopeClaim.split(' ').filter(Boolean); } /** * Get or create the JWKS key getter */ private async getKeyGetter(): Promise<jose.GetKeyFunction<jose.JWSHeaderParameters, jose.FlattenedJWSInput>> { const now = Date.now(); // Check if cache is valid if (this.getKey && this.jwksCache) { const cacheAge = now - this.jwksCache.fetchedAt; if (cacheAge < this.config.jwksCacheTtl) { return this.getKey; } logger.debug('JWKS cache expired, refreshing'); } // Fetch JWKS logger.debug('Fetching JWKS', { uri: this.config.jwksUri }); this.getKey = jose.createRemoteJWKSet(new URL(this.config.jwksUri)); // We can't easily cache the actual JWKS with createRemoteJWKSet, // but the library handles caching internally this.jwksCache = { jwks: { keys: [] }, // Placeholder, actual caching done by jose fetchedAt: now, }; return this.getKey; } /** * Clear the JWKS cache (useful for testing or key rotation) */ clearCache(): void { this.jwksCache = null; this.getKey = null; logger.debug('JWKS cache cleared'); } } /** * Extract Bearer token from Authorization header */ export function extractBearerToken(authHeader: string | undefined): string | null { if (!authHeader?.startsWith('Bearer ')) { return null; } return authHeader.slice(7).trim() || null; } /** * Create a JWT validator from environment variables */ export function createJwtValidatorFromEnv(): JwtValidator | null { const jwksUri = process.env['MCP_SERVER_JWKS_URI']; const issuer = process.env['MCP_SERVER_TOKEN_ISSUER']; const audience = process.env['MCP_SERVER_TOKEN_AUDIENCE']; if (!jwksUri || !issuer || !audience) { logger.debug('JWT validation not configured (missing env vars)'); return null; } return new JwtValidator({ jwksUri, issuer, audience, clockTolerance: parseInt(process.env['MCP_SERVER_JWT_CLOCK_TOLERANCE'] ?? '30', 10), }); }

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/cameronsjo/mcp-server-template'

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