234 lines
6.9 KiB
JavaScript
234 lines
6.9 KiB
JavaScript
// auth.js - Keycloak authentication handler
|
|
import axios from 'axios';
|
|
import jwt from 'jsonwebtoken';
|
|
import log4js from 'log4js';
|
|
// Load configuration
|
|
import { loadConfig } from '../config.js';
|
|
import { editObject, getObject, listObjects } 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 logger = log4js.getLogger('Auth');
|
|
logger.level = config.server.logLevel;
|
|
|
|
export class KeycloakAuth {
|
|
constructor() {
|
|
this.config = config.auth;
|
|
this.tokenCache = new Map(); // Cache for verified tokens
|
|
}
|
|
|
|
// Verify a token with Keycloak server
|
|
async verifyToken(token) {
|
|
// Check cache first
|
|
if (this.tokenCache.has(token)) {
|
|
const cachedInfo = this.tokenCache.get(token);
|
|
if (cachedInfo.expiresAt > Date.now()) {
|
|
return { valid: true, user: cachedInfo.user };
|
|
} else {
|
|
// Token expired, remove from cache
|
|
this.tokenCache.delete(token);
|
|
}
|
|
}
|
|
|
|
try {
|
|
// Verify token with Keycloak introspection endpoint
|
|
const response = await axios.post(
|
|
`${this.config.keycloak.url}/realms/${this.config.keycloak.realm}/protocol/openid-connect/token/introspect`,
|
|
new URLSearchParams({
|
|
token,
|
|
client_id: this.config.keycloak.clientId,
|
|
client_secret: this.config.keycloak.clientSecret
|
|
}),
|
|
{
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded'
|
|
}
|
|
}
|
|
);
|
|
|
|
const introspection = response.data;
|
|
|
|
if (!introspection.active) {
|
|
logger.info('Token is not active');
|
|
return { valid: false };
|
|
}
|
|
|
|
// Verify required roles if configured
|
|
if (this.config.requiredRoles && this.config.requiredRoles.length > 0) {
|
|
const hasRequiredRole = this.checkRoles(
|
|
introspection,
|
|
this.config.requiredRoles
|
|
);
|
|
if (!hasRequiredRole) {
|
|
logger.info("User doesn't have required roles");
|
|
return { valid: false };
|
|
}
|
|
}
|
|
|
|
// Parse token to extract user info
|
|
const decodedToken = jwt.decode(token);
|
|
const decodedUser = {
|
|
id: decodedToken.sub,
|
|
username: decodedToken.preferred_username,
|
|
email: decodedToken.email,
|
|
name: decodedToken.name,
|
|
roles: this.extractRoles(decodedToken)
|
|
};
|
|
|
|
const user = await listObjects({
|
|
model: userModel,
|
|
filter: { username: decodedUser.username }
|
|
});
|
|
|
|
// Cache the verified token
|
|
const expiresAt = introspection.exp * 1000; // Convert to milliseconds
|
|
this.tokenCache.set(token, { expiresAt, user: user[0] });
|
|
|
|
return { valid: true, user: user[0] };
|
|
} catch (error) {
|
|
logger.error('Token verification error:', error.message);
|
|
return { valid: false };
|
|
}
|
|
}
|
|
|
|
// Extract roles from token
|
|
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;
|
|
}
|
|
|
|
// Check if user has required roles
|
|
checkRoles(tokenInfo, requiredRoles) {
|
|
// Extract roles from token
|
|
const userRoles = this.extractRoles(tokenInfo);
|
|
|
|
// Check if user has any of the required roles
|
|
return requiredRoles.some(role => userRoles.includes(role));
|
|
}
|
|
}
|
|
|
|
export class CodeAuth {
|
|
// Verify a code with the database
|
|
async verifyCode(id, authCode) {
|
|
try {
|
|
logger.trace('Verifying code:', { id, authCode });
|
|
const host = await getObject({ model: hostModel, id, cached: true });
|
|
logger.trace('Host retrieved:', host);
|
|
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 };
|
|
}
|
|
logger.trace('Code verification successful:', host);
|
|
return { valid: true, host: host };
|
|
} catch (error) {
|
|
logger.error('Code verification error:', error.message);
|
|
return { valid: false };
|
|
}
|
|
}
|
|
|
|
async verifyOtp(otp) {
|
|
try {
|
|
const hosts = await listObjects({
|
|
model: hostModel,
|
|
filter: { otp: otp },
|
|
cached: false
|
|
});
|
|
const host = hosts[0];
|
|
if (host == undefined) {
|
|
const error = `No host found with OTP: ${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() }
|
|
});
|
|
logger.info('Host found with OTP:', otp);
|
|
return { valid: true, host: authCodeHost };
|
|
} catch (error) {
|
|
logger.error('OTP 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
|
|
|
|
if (event === 'authenticate') {
|
|
next();
|
|
return;
|
|
}
|
|
|
|
if (socketUser.authenticated) {
|
|
next();
|
|
return;
|
|
}
|
|
return next(new Error('Authentication is required.'));
|
|
};
|
|
}
|