Enhance authentication module with code and OTP verification

- Added CodeAuth class for verifying authentication codes and OTPs against the database.
- Implemented methods to check host status, validate codes, and manage OTP expiration.
- Updated KeycloakAuth to retrieve user information from the database.
- Refactored createAuthMiddleware to handle authentication checks for Socket.IO connections.
This commit is contained in:
Tom Butcher 2025-08-18 01:06:55 +01:00
parent ce15d3dbfc
commit 03eb0a61c1

View File

@ -4,6 +4,14 @@ import jwt from 'jsonwebtoken';
import log4js from 'log4js'; import log4js from 'log4js';
// Load configuration // Load configuration
import { loadConfig } from '../config.js'; import { loadConfig } from '../config.js';
import {
editObject,
getObject,
getObjectByFilter
} from '../database/database.js';
import { hostModel } from '../database/schemas/management/host.schema.js';
import { userModel } from '../database/schemas/management/user.schema.js';
import { generateAuthCode } from '../utils.js';
const config = loadConfig(); const config = loadConfig();
@ -11,7 +19,7 @@ const logger = log4js.getLogger('Auth');
logger.level = config.server.logLevel; logger.level = config.server.logLevel;
export class KeycloakAuth { export class KeycloakAuth {
constructor(config) { constructor() {
this.config = config.auth; this.config = config.auth;
this.tokenCache = new Map(); // Cache for verified tokens this.tokenCache = new Map(); // Cache for verified tokens
} }
@ -66,7 +74,7 @@ export class KeycloakAuth {
// Parse token to extract user info // Parse token to extract user info
const decodedToken = jwt.decode(token); const decodedToken = jwt.decode(token);
const user = { const decodedUser = {
id: decodedToken.sub, id: decodedToken.sub,
username: decodedToken.preferred_username, username: decodedToken.preferred_username,
email: decodedToken.email, email: decodedToken.email,
@ -74,6 +82,11 @@ export class KeycloakAuth {
roles: this.extractRoles(decodedToken) roles: this.extractRoles(decodedToken)
}; };
const user = await getObjectByFilter({
model: userModel,
filter: { username: decodedUser.username }
});
// Cache the verified token // Cache the verified token
const expiresAt = introspection.exp * 1000; // Convert to milliseconds const expiresAt = introspection.exp * 1000; // Convert to milliseconds
this.tokenCache.set(token, { expiresAt, user }); this.tokenCache.set(token, { expiresAt, user });
@ -120,28 +133,101 @@ export class KeycloakAuth {
} }
} }
// Socket.IO middleware for authentication export class CodeAuth {
export function createAuthMiddleware(auth) { // Verify a code with the database
return async (socket, next) => { async verifyCode(id, authCode) {
const { token } = socket.handshake.auth;
if (!token) {
return next(new Error('Authentication token is required'));
}
try { try {
const authResult = await auth.verifyToken(token); const host = await getObject({ model: hostModel, id, cached: true });
if (host == undefined) {
if (!authResult.valid) { const error = 'Host not found.';
return next(new Error('Invalid authentication token')); logger.warn(error, 'Host:', id);
return { valid: false, error: error };
} }
if (host.active == false) {
// Attach user information to socket const error = 'Host not active.';
socket.user = authResult.user; logger.warn(error, 'Host:', id);
next(); return { valid: false, error: error };
} catch (err) { }
logger.error('Authentication error:', err); if (host.authCode == undefined || host.authCode == '') {
next(new Error('Authentication failed')); const error = 'No authCode on database.';
logger.warn(error, 'Host:', id);
return { valid: false, error: error };
}
if (host.authCode != authCode) {
const error = 'authCode does not match.';
logger.warn(error, 'Host:', id);
return { valid: false, error: error };
}
return { valid: true, host: host };
} catch (error) {
logger.error('Code verification error:', error.message);
return { valid: false };
} }
}
async verifyOtp(otp) {
try {
const host = await getObjectByFilter({
model: hostModel,
filter: { otp: otp },
cached: false
});
if (host == undefined) {
const error = 'No host found with OTP.';
logger.warn(error);
return { valid: false, error: error };
}
const id = host._id.toString();
if (host.active == false) {
const error = 'Host is not active.';
logger.warn(error, 'Host:', id);
return { valid: false, error: error };
}
if (host.otp == undefined) {
const error = 'No OTP on database.';
logger.warn(error, 'Host:', id);
return { valid: false, error: error };
}
if (host.otpExpiresAt == undefined) {
const error = 'No OTP expiry.';
logger.warn(error, 'Host:', id);
return { valid: false, error: error };
}
if (host.otpExpiresAt < Date.now()) {
const error = 'OTP expired.';
logger.warn(error, 'Host:', id);
return { valid: false, error: error };
}
const authCodeHost = await editObject({
model: hostModel,
id: id,
updateData: { authCode: generateAuthCode() }
});
return { valid: true, host: authCodeHost };
} catch (error) {
logger.error('Code verification error:', error.message);
return { valid: false, error: error.message };
}
}
}
// 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
logger.trace('Event:', event);
if (event === 'authenticate') {
next();
return;
}
if (socketUser.authenticated) {
next();
return;
}
return next(new Error('Authentication is required.'));
}; };
} }