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';
// Load configuration
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();
@ -11,7 +19,7 @@ const logger = log4js.getLogger('Auth');
logger.level = config.server.logLevel;
export class KeycloakAuth {
constructor(config) {
constructor() {
this.config = config.auth;
this.tokenCache = new Map(); // Cache for verified tokens
}
@ -66,7 +74,7 @@ export class KeycloakAuth {
// Parse token to extract user info
const decodedToken = jwt.decode(token);
const user = {
const decodedUser = {
id: decodedToken.sub,
username: decodedToken.preferred_username,
email: decodedToken.email,
@ -74,6 +82,11 @@ export class KeycloakAuth {
roles: this.extractRoles(decodedToken)
};
const user = await getObjectByFilter({
model: userModel,
filter: { username: decodedUser.username }
});
// Cache the verified token
const expiresAt = introspection.exp * 1000; // Convert to milliseconds
this.tokenCache.set(token, { expiresAt, user });
@ -120,28 +133,101 @@ export class KeycloakAuth {
}
}
// Socket.IO middleware for authentication
export function createAuthMiddleware(auth) {
return async (socket, next) => {
const { token } = socket.handshake.auth;
if (!token) {
return next(new Error('Authentication token is required'));
}
export class CodeAuth {
// Verify a code with the database
async verifyCode(id, authCode) {
try {
const authResult = await auth.verifyToken(token);
if (!authResult.valid) {
return next(new Error('Invalid authentication token'));
const host = await getObject({ model: hostModel, id, cached: true });
if (host == undefined) {
const error = 'Host not found.';
logger.warn(error, 'Host:', id);
return { valid: false, error: error };
}
if (host.active == false) {
const error = 'Host not active.';
logger.warn(error, 'Host:', id);
return { valid: false, error: error };
}
if (host.authCode == undefined || host.authCode == '') {
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 };
}
}
// Attach user information to socket
socket.user = authResult.user;
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();
} catch (err) {
logger.error('Authentication error:', err);
next(new Error('Authentication failed'));
return;
}
if (socketUser.authenticated) {
next();
return;
}
return next(new Error('Authentication is required.'));
};
}