Skip to main content
Glama

athenahealth MCP Server

security.tsโ€ข10.3 kB
import crypto from 'crypto'; import { logger } from './logger.js'; // Security configuration const ENCRYPTION_ALGORITHM = 'aes-256-gcm'; const KEY_LENGTH = 32; // 256 bits const IV_LENGTH = 12; // 96 bits (recommended for GCM) const TAG_LENGTH = 16; // 128 bits export interface EncryptionResult { encryptedData: string; iv: string; tag: string; } export interface SecurityConfig { encryptionKey?: string; maxSessionDuration?: number; requireMFA?: boolean; ipWhitelist?: string[]; rateLimit?: { windowMs: number; max: number; }; } export class SecurityManager { private readonly encryptionKey: Buffer; private readonly config: SecurityConfig; constructor(config: SecurityConfig = {}) { this.config = { maxSessionDuration: 8 * 60 * 60 * 1000, // 8 hours requireMFA: false, ipWhitelist: [], rateLimit: { windowMs: 60 * 1000, // 1 minute max: 100, // 100 requests per minute }, ...config, }; // Initialize encryption key this.encryptionKey = this.initializeEncryptionKey(config.encryptionKey); } private initializeEncryptionKey(providedKey?: string): Buffer { if (providedKey) { if (providedKey.length !== KEY_LENGTH * 2) { // Hex string should be 64 chars throw new Error('Invalid encryption key length'); } return Buffer.from(providedKey, 'hex'); } // Generate a random key if none provided (for development only) if (process.env.NODE_ENV === 'development') { logger.warn('Using randomly generated encryption key for development'); return crypto.randomBytes(KEY_LENGTH); } throw new Error('Encryption key must be provided in production'); } /** * Encrypt sensitive data for storage or transmission */ encrypt(data: string): EncryptionResult { try { const iv = crypto.randomBytes(IV_LENGTH); const cipher = crypto.createCipheriv(ENCRYPTION_ALGORITHM, this.encryptionKey, iv); const encryptedBuffer = Buffer.concat([ cipher.update(data, 'utf8'), cipher.final(), ]); const authTag = cipher.getAuthTag(); return { encryptedData: encryptedBuffer.toString('hex'), iv: iv.toString('hex'), tag: authTag.toString('hex'), }; } catch (error) { logger.error('Encryption failed', { error }); throw new Error('Failed to encrypt data'); } } /** * Decrypt sensitive data */ decrypt(encryptionResult: EncryptionResult): string { try { const { encryptedData, iv, tag } = encryptionResult; const ivBuffer = Buffer.from(iv, 'hex'); const tagBuffer = Buffer.from(tag, 'hex'); const encryptedBuffer = Buffer.from(encryptedData, 'hex'); if (ivBuffer.length !== IV_LENGTH) { throw new Error('Invalid IV length'); } if (tagBuffer.length !== TAG_LENGTH) { throw new Error('Invalid authentication tag length'); } const decipher = crypto.createDecipheriv(ENCRYPTION_ALGORITHM, this.encryptionKey, ivBuffer); decipher.setAuthTag(tagBuffer); const decryptedBuffer = Buffer.concat([ decipher.update(encryptedBuffer), decipher.final(), ]); return decryptedBuffer.toString('utf8'); } catch (error) { logger.error('Decryption failed', { error }); throw new Error('Failed to decrypt data'); } } /** * Hash sensitive data for storage (one-way) */ hash(data: string, salt?: string): string { const actualSalt = salt || crypto.randomBytes(16).toString('hex'); const hash = crypto.pbkdf2Sync(data, actualSalt, 10000, 64, 'sha512'); return `${actualSalt}:${hash.toString('hex')}`; } /** * Verify hashed data */ verifyHash(data: string, hashedData: string): boolean { try { const [salt, hash] = hashedData.split(':'); if (!salt || !hash) { return false; } const dataHash = crypto.pbkdf2Sync(data, salt, 10000, 64, 'sha512'); return hash === dataHash.toString('hex'); } catch (error) { logger.error('Hash verification failed', { error }); return false; } } /** * Generate secure random token */ generateSecureToken(length: number = 32): string { return crypto.randomBytes(length).toString('hex'); } /** * Validate IP address against whitelist */ validateIPAddress(ip: string): boolean { if (!this.config.ipWhitelist || this.config.ipWhitelist.length === 0) { return true; // No whitelist configured } return this.config.ipWhitelist.includes(ip); } /** * Sanitize data for logging (remove sensitive information) */ sanitizeForLogging(data: any): any { if (typeof data === 'string') { // Remove common sensitive patterns return data .replace(/\b\d{3}-\d{2}-\d{4}\b/g, '[SSN]') .replace(/\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g, '[CARD]') .replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, '[EMAIL]') .replace(/\b\d{10,}\b/g, '[PHONE]') .replace(/\b\d{1,2}\/\d{1,2}\/\d{2,4}\b/g, '[DATE]') .replace(/\b\d{4}-\d{2}-\d{2}\b/g, '[DATE]'); } if (Array.isArray(data)) { return data.map(item => this.sanitizeForLogging(item)); } if (data && typeof data === 'object') { const sanitized: any = {}; for (const [key, value] of Object.entries(data)) { // Always sanitize keys that commonly contain sensitive data if (this.isSensitiveKey(key)) { sanitized[key] = '[REDACTED]'; } else { sanitized[key] = this.sanitizeForLogging(value); } } return sanitized; } return data; } private isSensitiveKey(key: string): boolean { const sensitiveKeys = [ 'ssn', 'social_security_number', 'tax_id', 'password', 'passwd', 'pwd', 'token', 'access_token', 'refresh_token', 'api_key', 'secret', 'client_secret', 'authorization', 'auth', 'dob', 'date_of_birth', 'birthdate', 'phone', 'mobile', 'telephone', 'email', 'email_address', 'address', 'street', 'home_address', 'credit_card', 'card_number', 'account_number', 'bank_account', 'routing_number', 'patient_id', 'medical_record_number', 'mrn', ]; return sensitiveKeys.some(sensitiveKey => key.toLowerCase().includes(sensitiveKey) ); } /** * Validate session token */ validateSessionToken(token: string, sessionData: any): boolean { try { // Check if session has expired if (sessionData.expiresAt && Date.now() > sessionData.expiresAt) { return false; } // Verify token integrity const expectedToken = this.generateSessionToken(sessionData.userId, sessionData.createdAt); return token === expectedToken; } catch (error) { logger.error('Session validation failed', { error }); return false; } } /** * Generate session token */ generateSessionToken(userId: string, timestamp: number): string { const data = `${userId}:${timestamp}`; return crypto.createHmac('sha256', this.encryptionKey).update(data).digest('hex'); } /** * Create secure session */ createSession(userId: string, additionalData: any = {}): { token: string; expiresAt: number; sessionData: any; } { const createdAt = Date.now(); const expiresAt = createdAt + (this.config.maxSessionDuration || 8 * 60 * 60 * 1000); const token = this.generateSessionToken(userId, createdAt); const sessionData = { userId, createdAt, expiresAt, ...additionalData, }; return { token, expiresAt, sessionData, }; } /** * Validate API request for security */ validateRequest(request: { ip?: string; userAgent?: string; timestamp?: number; signature?: string; }): { valid: boolean; reason?: string } { // Check IP whitelist if (request.ip && !this.validateIPAddress(request.ip)) { return { valid: false, reason: 'IP address not whitelisted' }; } // Check timestamp for replay attacks (5 minute window) if (request.timestamp) { const now = Date.now(); const timeDiff = Math.abs(now - request.timestamp); if (timeDiff > 5 * 60 * 1000) { return { valid: false, reason: 'Request timestamp too old' }; } } // Additional security checks can be added here return { valid: true }; } /** * Generate secure API key */ generateAPIKey(prefix: string = 'ak'): string { const randomPart = crypto.randomBytes(24).toString('base64url'); return `${prefix}_${randomPart}`; } /** * Validate API key format */ validateAPIKeyFormat(apiKey: string): boolean { const apiKeyRegex = /^[a-zA-Z0-9_-]+$/; return apiKeyRegex.test(apiKey) && apiKey.length >= 20; } } // Export singleton instance export const securityManager = new SecurityManager({ encryptionKey: process.env.ENCRYPTION_KEY, maxSessionDuration: parseInt(process.env.MAX_SESSION_DURATION || '28800000'), // 8 hours requireMFA: process.env.REQUIRE_MFA === 'true', ipWhitelist: process.env.IP_WHITELIST?.split(',') || [], rateLimit: { windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '60000'), max: parseInt(process.env.RATE_LIMIT_MAX || '100'), }, }); // Utility functions export const encryptSensitiveData = (data: string): EncryptionResult => { return securityManager.encrypt(data); }; export const decryptSensitiveData = (encryptionResult: EncryptionResult): string => { return securityManager.decrypt(encryptionResult); }; export const sanitizeForLogging = (data: any): any => { return securityManager.sanitizeForLogging(data); }; export const generateSecureToken = (length?: number): string => { return securityManager.generateSecureToken(length); }; export const validateIPAddress = (ip: string): boolean => { return securityManager.validateIPAddress(ip); }; export const createSecureSession = (userId: string, additionalData?: any) => { return securityManager.createSession(userId, additionalData); }; export const validateSessionToken = (token: string, sessionData: any): boolean => { return securityManager.validateSessionToken(token, sessionData); };

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/ophydami/Athenahealth-MCP'

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