From 289673813a6581340f82a073c842c81026485767 Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Sun, 1 Mar 2026 17:06:17 +0000 Subject: [PATCH] Implemented redis session storage. --- config.json | 19 +++++-- src/auth/auth.js | 133 +++++++++++++---------------------------------- 2 files changed, 53 insertions(+), 99 deletions(-) diff --git a/config.json b/config.json index 275f89d..4fa1954 100644 --- a/config.json +++ b/config.json @@ -17,7 +17,11 @@ "mongo": { "url": "mongodb://127.0.0.1:27017/farmcontrol" }, - "redis": { "host": "localhost", "port": 6379, "password": "" } + "redis": { + "host": "localhost", + "port": 6379, + "password": "" + } }, "otpExpiryMins": 0.5 }, @@ -39,7 +43,11 @@ "mongo": { "url": "mongodb://127.0.0.1:27017/farmcontrol-test" }, - "redis": { "host": "localhost", "port": 6379, "password": "" } + "redis": { + "host": "localhost", + "port": 6379, + "password": "" + } }, "otpExpiryMins": 0.5 }, @@ -60,7 +68,12 @@ "database": { "mongo": { "url": "mongodb://localhost:27017/farmcontrol" + }, + "redis": { + "host": "localhost", + "port": 6379, + "password": "" } } } -} +} \ No newline at end of file diff --git a/src/auth/auth.js b/src/auth/auth.js index 1b96816..eb31f50 100644 --- a/src/auth/auth.js +++ b/src/auth/auth.js @@ -1,9 +1,7 @@ -// auth.js - Keycloak authentication handler -import axios from 'axios'; -import jwt from 'jsonwebtoken'; +// auth.js - Redis session authentication (shared with API) import log4js from 'log4js'; -// Load configuration 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'; @@ -14,123 +12,69 @@ const config = loadConfig(); const logger = log4js.getLogger('Auth'); logger.level = config.server.logLevel; -export class KeycloakAuth { +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; - this.tokenCache = new Map(); // Cache for verified tokens + this.tokenCache = new Map(); } - // Verify a token with Keycloak server async verifyToken(token) { - // Check cache first + if (!token) return { valid: false }; + if (this.tokenCache.has(token)) { - const cachedInfo = this.tokenCache.get(token); - if (cachedInfo.expiresAt > Date.now()) { - return { valid: true, user: cachedInfo.user }; - } else { - // Token expired, remove from cache - this.tokenCache.delete(token); + const cached = this.tokenCache.get(token); + if (cached.expiresAt > Date.now()) { + return { valid: true, user: cached.user }; } + this.tokenCache.delete(token); } try { - // Verify token with Keycloak introspection endpoint - const response = await axios.post( - `${this.config.keycloak.url}/realms/${this.config.keycloak.realm}/protocol/openid-connect/token/introspect`, - new URLSearchParams({ - token, - client_id: this.config.keycloak.clientId, - client_secret: this.config.keycloak.clientSecret - }), - { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - } - } - ); + const key = SESSION_KEY_PREFIX + token; + const session = await redisServer.getKey(key); - const introspection = response.data; - - if (!introspection.active) { - logger.info('Token is not active'); + 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 }; } - // Verify required roles if configured if (this.config.requiredRoles && this.config.requiredRoles.length > 0) { - const hasRequiredRole = this.checkRoles( - introspection, - this.config.requiredRoles - ); - if (!hasRequiredRole) { + 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 }; } } - // Parse token to extract user info - const decodedToken = jwt.decode(token); - const decodedUser = { - id: decodedToken.sub, - username: decodedToken.preferred_username, - email: decodedToken.email, - name: decodedToken.name, - roles: this.extractRoles(decodedToken) - }; - - const user = await listObjects({ - model: userModel, - filter: { username: decodedUser.username } + this.tokenCache.set(token, { + expiresAt: session.expiresAt, + user: session.user }); - // Cache the verified token - const expiresAt = introspection.exp * 1000; // Convert to milliseconds - this.tokenCache.set(token, { expiresAt, user: user[0] }); - - return { valid: true, user: user[0] }; + return { valid: true, user: session.user }; } catch (error) { - logger.error('Token verification error:', error.message); + logger.error('Session verification error:', error.message); return { valid: false }; } } - - // Extract roles from token - extractRoles(token) { - const roles = []; - - // Extract realm roles - if (token.realm_access && token.realm_access.roles) { - roles.push(...token.realm_access.roles); - } - - // Extract client roles - if (token.resource_access) { - for (const client in token.resource_access) { - if (token.resource_access[client].roles) { - roles.push( - ...token.resource_access[client].roles.map( - role => `${client}:${role}` - ) - ); - } - } - } - - return roles; - } - - // Check if user has required roles - checkRoles(tokenInfo, requiredRoles) { - // Extract roles from token - const userRoles = this.extractRoles(tokenInfo); - - // Check if user has any of the required roles - return requiredRoles.some(role => userRoles.includes(role)); - } } +/** @deprecated Use SessionAuth - kept for backward compatibility */ +export class KeycloakAuth extends SessionAuth {} + export class CodeAuth { - // Verify a code with the database async verifyCode(id, authCode) { try { logger.trace('Verifying code:', { id, authCode }); @@ -212,12 +156,9 @@ export class CodeAuth { } } -// Socket.IO middleware for authentication export function createAuthMiddleware(socketUser) { return async (packet, next) => { - const [event] = packet; // event name is always first element - - // Allow the 'authenticate' event through without checks + const [event] = packet; if (event === 'authenticate') { next();