138 lines
4.3 KiB
JavaScript
138 lines
4.3 KiB
JavaScript
/**
|
|
* Authentication middleware - uses Redis session store.
|
|
* Keycloak is used only for login/refresh; session validation is done via Redis.
|
|
*/
|
|
import config, { getEnvironment } from './config.js';
|
|
import log4js from 'log4js';
|
|
import NodeCache from 'node-cache';
|
|
import bcrypt from 'bcrypt';
|
|
import { userModel } from './database/schemas/management/user.schema.js';
|
|
import { getObject } from './database/database.js';
|
|
import { hostModel } from './database/schemas/management/host.schema.js';
|
|
import { getSession, lookupUserByToken } from './services/misc/auth.js';
|
|
|
|
const logger = log4js.getLogger('Keycloak');
|
|
logger.level = config.server.logLevel || 'info';
|
|
|
|
const userCache = new NodeCache({ stdTTL: 300 });
|
|
|
|
userCache.on('expired', (key, value) => {
|
|
logger.debug(`Cache entry expired: ${key}`);
|
|
});
|
|
|
|
userCache.on('flush', () => {
|
|
logger.info('Cache flushed');
|
|
});
|
|
|
|
const lookupUser = async (preferredUsername) => {
|
|
try {
|
|
const cachedUser = userCache.get(preferredUsername);
|
|
if (cachedUser) {
|
|
logger.debug(`User found in cache: ${preferredUsername}`);
|
|
return cachedUser;
|
|
}
|
|
|
|
logger.debug(`User not in cache, querying database: ${preferredUsername}`);
|
|
const user = await userModel.findOne({ username: preferredUsername });
|
|
|
|
if (user) {
|
|
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;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Middleware to check if the user is authenticated.
|
|
* Supports: 1) Bearer token (Redis session), 2) Bearer token (email-render JWT for Puppeteer),
|
|
* 3) HTTP Basic Auth (username + app password), 4) x-host-id + x-auth-code (host auth)
|
|
*/
|
|
const isAuthenticated = async (req, res, next) => {
|
|
const authHeader = req.headers.authorization || req.headers.Authorization;
|
|
if (authHeader && authHeader.startsWith('Bearer ')) {
|
|
const token = authHeader.substring(7);
|
|
|
|
try {
|
|
const session = await getSession(token);
|
|
if (session && session.expiresAt > Date.now()) {
|
|
req.user = session.user;
|
|
req.session = session;
|
|
return next();
|
|
}
|
|
|
|
// Try email-render JWT (short-lived token for Puppeteer email notifications)
|
|
const user = await lookupUserByToken(token);
|
|
if (user) {
|
|
req.user = user;
|
|
req.session = { user };
|
|
return next();
|
|
}
|
|
} catch (error) {
|
|
logger.error('Session lookup error:', error.message);
|
|
}
|
|
}
|
|
|
|
// Try HTTP Basic Auth (username + app password)
|
|
if (authHeader && authHeader.startsWith('Basic ')) {
|
|
try {
|
|
logger.debug('Basic auth header:', authHeader);
|
|
const base64Credentials = authHeader.substring(6);
|
|
const credentials = Buffer.from(base64Credentials, 'base64').toString('utf-8');
|
|
const [username, password] = credentials.split(':');
|
|
logger.debug('Basic auth credentials:', { username, password });
|
|
if (username && password) {
|
|
const user = await userModel.findOne({ username }).select('+appPasswordHash');
|
|
if (user?.appPasswordHash && (await bcrypt.compare(password, user.appPasswordHash))) {
|
|
user.appPasswordHash = undefined; // don't expose hash downstream
|
|
req.user = user;
|
|
req.session = { user };
|
|
return next();
|
|
}
|
|
}
|
|
} catch (error) {
|
|
logger.error('Basic auth error:', error.message);
|
|
}
|
|
}
|
|
|
|
const hostId = req.headers['x-host-id'];
|
|
const authCode = req.headers['x-auth-code'];
|
|
if (hostId && authCode) {
|
|
const host = await getObject({ model: hostModel, id: hostId });
|
|
if (host && host.authCode === authCode) {
|
|
return next();
|
|
}
|
|
}
|
|
logger.debug('Not authenticated', { hostId, authCode }, 'req.headers', req.headers);
|
|
return res.status(401).json({ error: 'Not Authenticated', code: 'UNAUTHORIZED' });
|
|
};
|
|
|
|
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 {
|
|
isAuthenticated,
|
|
lookupUser,
|
|
clearUserCache,
|
|
getUserCacheStats,
|
|
removeUserFromCache,
|
|
getEnvironment,
|
|
};
|