import Keycloak from 'keycloak-connect'; import session from 'express-session'; import dotenv from 'dotenv'; import axios from 'axios'; import jwt from 'jsonwebtoken'; import log4js from 'log4js'; import NodeCache from 'node-cache'; import { userModel } from './schemas/management/user.schema.js'; dotenv.config(); const logger = log4js.getLogger('Keycloak'); logger.level = process.env.LOG_LEVEL || 'info'; // Initialize NodeCache with 5-minute TTL const userCache = new NodeCache({ stdTTL: 300 }); // 300 seconds = 5 minutes // Cache event listeners for monitoring userCache.on('expired', (key, value) => { logger.debug(`Cache entry expired: ${key}`); }); userCache.on('flush', () => { logger.info('Cache flushed'); }); // User lookup function with caching const lookupUser = async (preferredUsername) => { try { // Check cache first const cachedUser = userCache.get(preferredUsername); if (cachedUser) { logger.debug(`User found in cache: ${preferredUsername}`); return cachedUser; } // If not in cache, query database logger.debug(`User not in cache, querying database: ${preferredUsername}`); const user = await userModel.findOne({ username: preferredUsername }); if (user) { // Store in cache userCache.set(preferredUsername, user); logger.debug(`User stored in cache: ${preferredUsername}`); return user; } logger.warn(`User not found in database: ${preferredUsername}`); return null; } catch (error) { logger.error(`Error looking up user ${preferredUsername}:`, error.message); return null; } }; // Initialize Keycloak const keycloakConfig = { realm: process.env.KEYCLOAK_REALM || 'farm-control', 'auth-server-url': process.env.KEYCLOAK_URL || 'http://localhost:8080/auth', 'ssl-required': process.env.NODE_ENV === 'production' ? 'external' : 'none', resource: process.env.KEYCLOAK_CLIENT_ID || 'farmcontrol-client', 'confidential-port': 0, 'bearer-only': true, 'public-client': false, 'use-resource-role-mappings': true, 'verify-token-audience': true, credentials: { secret: process.env.KEYCLOAK_CLIENT_SECRET, }, }; const memoryStore = new session.MemoryStore(); var expressSession = session({ secret: process.env.SESSION_SECRET || 'n00Dl3s23!', resave: false, saveUninitialized: true, // Set this to true to ensure session is initialized store: memoryStore, cookie: { maxAge: 1800000, // 30 minutes }, }); var keycloak = new Keycloak({ store: memoryStore }, keycloakConfig); // Custom middleware to check if the user is authenticated const isAuthenticated = async (req, res, next) => { let token = null; const authHeader = req.headers.authorization || req.headers.Authorization; if (authHeader && authHeader.startsWith('Bearer ')) { token = authHeader.substring(7); try { // Verify token with Keycloak introspection endpoint const response = await axios.post( `${process.env.KEYCLOAK_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/token/introspect`, new URLSearchParams({ token: token, client_id: process.env.KEYCLOAK_CLIENT_ID, client_secret: process.env.KEYCLOAK_CLIENT_SECRET, }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, } ); const introspection = response.data; if (!introspection.active) { logger.info('Token is not active'); logger.debug('Token:', token); return res.status(401).json({ error: 'Session Inactive', code: 'UNAUTHORIZED' }); } return next(); } catch (error) { logger.error('Token verification error:', error.message); return res.status(401).json({ error: 'Verification Error', code: 'UNAUTHORIZED' }); } } // Fallback to session-based authentication console.log('Using session token'); if (req.session && req.session['keycloak-token']) { const sessionToken = req.session['keycloak-token']; if (sessionToken.expires_at > new Date().getTime()) { return next(); } } return res.status(401).json({ error: 'Not Authenticated', code: 'UNAUTHORIZED' }); }; // Helper function to extract roles from token function 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; } // Cache management utility functions const clearUserCache = () => { userCache.flushAll(); logger.info('User cache cleared'); }; const getUserCacheStats = () => { return userCache.getStats(); }; const removeUserFromCache = (username) => { userCache.del(username); logger.debug(`User removed from cache: ${username}`); }; export { keycloak, expressSession, isAuthenticated, lookupUser, clearUserCache, getUserCacheStats, removeUserFromCache, };