import * as fs from 'fs/promises'; import * as crypto from 'crypto'; import * as path from 'path'; import { Request, Response, NextFunction } from 'express'; import { Logger } from '../utils/logger'; import { Config } from '../utils/config'; interface AuthSession { client_key: string; server_key: string; created_at: number; workspace_path: string; } export class AuthValidator { private logger: Logger; private sessionsCache: Map = new Map(); constructor() { this.logger = new Logger('AuthValidator'); } public async validateWebSocketAuth(sessionId: string, signature: string): Promise { try { const session = await this.loadSession(sessionId); if (!session) { this.logger.warn(`Session not found: ${sessionId}`); return false; } // For WebSocket, we'll validate a simple signature // In production, this should include timestamp and nonce const expectedSignature = crypto .createHmac('sha256', session.server_key) .update(sessionId) .digest('hex'); return signature === expectedSignature; } catch (error) { this.logger.error('WebSocket auth validation failed:', error); return false; } } public authMiddleware() { return async (req: Request, res: Response, next: NextFunction) => { const sessionId = req.headers['x-session-id'] as string; const signature = req.headers['x-auth-signature'] as string; const timestamp = req.headers['x-timestamp'] as string; if (!sessionId || !signature || !timestamp) { res.status(401).json({ error: 'Missing authentication headers', code: 401 }); return; } try { const session = await this.loadSession(sessionId); if (!session) { res.status(401).json({ error: 'Session not found', code: 401, recoverable: true, recovery: 'Create new session via POST /_ide/service/auth/create' }); return; } // Validate signature const method = req.method; const path = req.path; const body = JSON.stringify(req.body || {}); const signatureBase = `${method}\n${path}\n${timestamp}\n${body}`; const expectedSignature = crypto .createHmac('sha256', session.server_key) .update(signatureBase) .digest('hex'); if (signature !== expectedSignature) { this.logger.warn('Invalid signature for session:', sessionId); res.status(401).json({ error: 'Invalid signature', code: 401 }); return; } // Check timestamp to prevent replay attacks (5 minute window) const requestTime = parseInt(timestamp, 10); const currentTime = Math.floor(Date.now() / 1000); if (Math.abs(currentTime - requestTime) > 300) { res.status(401).json({ error: 'Request timestamp expired', code: 401 }); return; } // Attach session to request for use in routes (req as any).session = session; (req as any).sessionId = sessionId; next(); } catch (error) { this.logger.error('Auth middleware error:', error); res.status(500).json({ error: 'Authentication error', code: 500 }); } }; } private async loadSession(sessionId: string): Promise { // Check cache first if (this.sessionsCache.has(sessionId)) { return this.sessionsCache.get(sessionId)!; } try { const sessionPath = path.join(Config.AUTH_SESSION_PATH, `auth-${sessionId}.json`); const data = await fs.readFile(sessionPath, 'utf8'); const session = JSON.parse(data) as AuthSession; // Cache the session this.sessionsCache.set(sessionId, session); return session; } catch (error) { if ((error as any).code === 'ENOENT') { return null; // Session file doesn't exist } throw error; } } public clearCache(): void { this.sessionsCache.clear(); } }