180 lines
5.1 KiB
JavaScript
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,
|
|
};
|