farmcontrol-api/src/keycloak.js

180 lines
5.1 KiB
JavaScript

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,
};