// auth.js - Redis session authentication (shared with API) import log4js from 'log4js'; import { loadConfig } from '../config.js'; import { redisServer } from '../database/redis.js'; import { editObject, getObject, listObjects } from '../database/database.js'; import { hostModel } from '../database/schemas/management/host.schema.js'; import { userModel } from '../database/schemas/management/user.schema.js'; import { formatTraceData, generateAuthCode } from '../utils.js'; const config = loadConfig(); const logger = log4js.getLogger('Auth'); logger.level = config.server.logLevel; const SESSION_KEY_PREFIX = 'session:'; /** * SessionAuth - validates tokens by looking up Redis session (created by API after Keycloak login). * Same session key format as API: session:{sessionToken} */ export class SessionAuth { constructor() { this.config = config.auth; } async verifyToken(token) { if (!token) return { valid: false }; try { const key = SESSION_KEY_PREFIX + token; const session = await redisServer.getKey(key); if (!session || !session.user) { logger.info('Session not found or invalid'); return { valid: false }; } if (session.expiresAt && session.expiresAt <= Date.now()) { logger.info('Session expired'); return { valid: false }; } if (this.config.requiredRoles && this.config.requiredRoles.length > 0) { const roles = session.user?.roles || []; const hasRole = this.config.requiredRoles.some((r) => roles.includes(r)); if (!hasRole) { logger.info("User doesn't have required roles"); return { valid: false }; } } return { valid: true, user: session.user }; } catch (error) { logger.error('Session verification error:', error.message); return { valid: false }; } } } /** @deprecated Use SessionAuth - kept for backward compatibility */ export class KeycloakAuth extends SessionAuth {} export class CodeAuth { async verifyCode(id, authCode) { try { logger.trace(`Verifying code: ${formatTraceData({ id, authCode })}`); const host = await getObject({ model: hostModel, id, cached: true }); logger.trace(`Host retrieved: ${formatTraceData(host)}`); if (host == undefined) { const error = 'Host not found.'; logger.warn(error, 'Host:', id); return { valid: false, error: error }; } if (host.active == false) { const error = 'Host not active.'; logger.warn(error, 'Host:', id); return { valid: false, error: error }; } if (host.authCode == undefined || host.authCode == '') { const error = 'No authCode on database.'; logger.warn(error, 'Host:', id); return { valid: false, error: error }; } if (host.authCode != authCode) { const error = 'authCode does not match.'; logger.warn(error, 'Host:', id); return { valid: false, error: error }; } logger.trace(`Code verification successful: ${formatTraceData(host)}`); return { valid: true, host: host }; } catch (error) { logger.error('Code verification error:', error.message); return { valid: false }; } } async verifyOtp(otp) { try { const hosts = await listObjects({ model: hostModel, filter: { otp: otp }, cached: false }); const host = hosts[0]; if (host == undefined) { const error = `No host found with OTP: ${otp}`; logger.warn(error); return { valid: false, error: error }; } const id = host._id.toString(); if (host.active == false) { const error = 'Host is not active.'; logger.warn(error, 'Host:', id); return { valid: false, error: error }; } if (host.otp == undefined) { const error = 'No OTP on database.'; logger.warn(error, 'Host:', id); return { valid: false, error: error }; } if (host.otpExpiresAt == undefined) { const error = 'No OTP expiry.'; logger.warn(error, 'Host:', id); return { valid: false, error: error }; } if (host.otpExpiresAt < Date.now()) { const error = 'OTP expired.'; logger.warn(error, 'Host:', id); return { valid: false, error: error }; } const authCodeHost = await editObject({ model: hostModel, id: id, updateData: { authCode: generateAuthCode() } }); logger.info('Host found with OTP:', otp); return { valid: true, host: authCodeHost }; } catch (error) { logger.error('OTP verification error:', error.message); return { valid: false, error: error.message }; } } } export function createAuthMiddleware(socketUser) { return async (packet, next) => { const [event] = packet; if (event === 'authenticate') { next(); return; } if (socketUser.authenticated) { next(); return; } return next(new Error('Authentication is required.')); }; }