UNPKG

mbkauthe

Version:

MBKTechStudio's reusable authentication system for Node.js applications.

609 lines (522 loc) 21.1 kB
import express from "express"; import csurf from "csurf"; import crypto from "crypto"; import session from "express-session"; import pgSession from "connect-pg-simple"; const PgSession = pgSession(session); import { dblogin } from "./pool.js"; import { authenticate } from "./validateSessionAndRole.js"; import fetch from 'node-fetch'; import cookieParser from "cookie-parser"; import bcrypt from 'bcrypt'; import rateLimit from 'express-rate-limit'; import speakeasy from "speakeasy"; //import passport from 'passport'; //import GitHubStrategy from 'passport-github2'; import { createRequire } from "module"; import fs from "fs"; import path from "path"; import dotenv from "dotenv"; dotenv.config(); const mbkautheVar = JSON.parse(process.env.mbkautheVar); const router = express.Router(); const require = createRequire(import.meta.url); const packageJson = require("../package.json"); router.use(express.json()); router.use(express.urlencoded({ extended: true })); router.use(cookieParser()); // CSRF protection middleware const csrfProtection = csurf({ cookie: false }); // CORS and security headers router.use((req, res, next) => { const origin = req.headers.origin; if (origin && origin.endsWith(`.${mbkautheVar.DOMAIN}`)) { res.header('Access-Control-Allow-Origin', origin); res.header('Access-Control-Allow-Credentials', 'true'); res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE'); res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization'); } next(); }); const LoginLimit = rateLimit({ windowMs: 1 * 60 * 1000, max: 8, message: { success: false, message: "Too many attempts, please try again later" }, skip: (req) => { return !!req.session.user; } }); const sessionConfig = { store: new PgSession({ pool: dblogin, tableName: "session", createTableIfMissing: true }), secret: mbkautheVar.SESSION_SECRET_KEY, resave: false, saveUninitialized: false, proxy: true, cookie: { maxAge: mbkautheVar.COOKIE_EXPIRE_TIME * 24 * 60 * 60 * 1000, domain: mbkautheVar.IS_DEPLOYED === 'true' ? `.${mbkautheVar.DOMAIN}` : undefined, httpOnly: true, secure: mbkautheVar.IS_DEPLOYED === 'true' ? 'auto' : false, sameSite: 'lax', path: '/' }, name: 'mbkauthe.sid' }; router.use(session(sessionConfig)); router.use(async (req, res, next) => { if (!req.session.user && req.cookies.sessionId) { try { const sessionId = req.cookies.sessionId; const query = `SELECT * FROM "Users" WHERE "SessionId" = $1`; const result = await dblogin.query(query, [sessionId]); if (result.rows.length > 0) { const user = result.rows[0]; req.session.user = { id: user.id, username: user.UserName, sessionId, }; } } catch (err) { console.error("[mbkauthe] Session restoration error:", err); } } next(); }); const getCookieOptions = () => ({ maxAge: mbkautheVar.COOKIE_EXPIRE_TIME * 24 * 60 * 60 * 1000, domain: mbkautheVar.IS_DEPLOYED === 'true' ? `.${mbkautheVar.DOMAIN}` : undefined, secure: mbkautheVar.IS_DEPLOYED === 'true' ? 'auto' : false, sameSite: 'lax', path: '/', httpOnly: true }); async function completeLoginProcess(req, res, user, redirectUrl = null) { try { const sessionId = crypto.randomBytes(256).toString("hex"); console.log(`[mbkauthe] Generated session ID for username: ${user.username}`); // Delete old session record for this user await dblogin.query('DELETE FROM "session" WHERE username = $1', [user.username]); await dblogin.query(`UPDATE "Users" SET "SessionId" = $1 WHERE "id" = $2`, [ sessionId, user.id, ]); req.session.user = { id: user.id, username: user.username, role: user.role, sessionId, }; if (req.session.preAuthUser) { delete req.session.preAuthUser; } req.session.save(async (err) => { if (err) { console.log("[mbkauthe] Session save error:", err); return res.status(500).json({ success: false, message: "Internal Server Error" }); } try { await dblogin.query( 'UPDATE "session" SET username = $1 WHERE sid = $2', [user.username, req.sessionID] ); } catch (e) { console.log("[mbkauthe] Failed to update username in session table:", e); } const cookieOptions = getCookieOptions(); res.cookie("sessionId", sessionId, cookieOptions); console.log(`[mbkauthe] User "${user.username}" logged in successfully`); const responsePayload = { success: true, message: "Login successful", sessionId, }; if (redirectUrl) { responsePayload.redirectUrl = redirectUrl; } res.status(200).json(responsePayload); }); } catch (err) { console.log("[mbkauthe] Error during login completion:", err); res.status(500).json({ success: false, message: "Internal Server Error" }); } } router.use(async (req, res, next) => { if (req.session && req.session.user) { const cookieOptions = getCookieOptions(); res.cookie("username", req.session.user.username, { ...cookieOptions, httpOnly: false }); res.cookie("sessionId", req.session.user.sessionId, cookieOptions); } next(); }); router.post("/mbkauthe/api/terminateAllSessions", authenticate(mbkautheVar.Main_SECRET_TOKEN), async (req, res) => { try { await dblogin.query(`UPDATE "Users" SET "SessionId" = NULL`); await dblogin.query('DELETE FROM "session"'); req.session.destroy((err) => { if (err) { console.log("[mbkauthe] Error destroying session:", err); return res.status(500).json({ success: false, message: "Failed to terminate sessions" }); } const cookieOptions = getCookieOptions(); res.clearCookie("mbkauthe.sid", cookieOptions); res.clearCookie("sessionId", cookieOptions); res.clearCookie("username", cookieOptions); console.log("[mbkauthe] All sessions terminated successfully"); res.status(200).json({ success: true, message: "All sessions terminated successfully", }); }); } catch (err) { console.log("[mbkauthe] Database query error during session termination:", err); res.status(500).json({ success: false, message: "Internal Server Error" }); } }); router.post("/mbkauthe/api/login", LoginLimit, async (req, res) => { console.log("[mbkauthe] Login request received"); const { username, password } = req.body; console.log(`[mbkauthe] Login attempt for username: ${username}`); if (!username || !password) { console.log("[mbkauthe] Missing username or password"); return res.status(400).json({ success: false, message: "Username and password are required", }); } try { const userQuery = `SELECT * FROM "Users" WHERE "UserName" = $1`; const userResult = await dblogin.query(userQuery, [username]); if (userResult.rows.length === 0) { console.log(`[mbkauthe] Username does not exist: ${username}`); return res.status(404).json({ success: false, message: "Incorrect Username Or Password" }); } const user = userResult.rows[0]; if (mbkautheVar.EncryptedPassword === "true") { try { const result = await bcrypt.compare(password, user.Password); if (!result) { console.log("[mbkauthe] Incorrect password."); return res.status(401).json({ success: false, errorCode: 603, message: "Incorrect Username Or Password." }); } console.log("[mbkauthe] Password matches!"); } catch (err) { console.error("[mbkauthe] Error comparing password:", err); return res.status(500).json({ success: false, errorCode: 605, message: `Internal Server Error` }); } } else { if (user.Password !== password) { console.log(`[mbkauthe] Incorrect password for username: ${username}`); return res.status(401).json({ success: false, errorCode: 603, message: "Incorrect Username Or Password" }); } } if (!user.Active) { console.log(`[mbkauthe] Inactive account for username: ${username}`); return res.status(403).json({ success: false, message: "Account is inactive" }); } if (user.Role !== "SuperAdmin") { const allowedApps = user.AllowedApps; if (!allowedApps || !allowedApps.some(app => app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) { console.warn(`[mbkauthe] User \"${user.UserName}\" is not authorized to use the application \"${mbkautheVar.APP_NAME}\"`); return res.status(403).json({ success: false, message: `You Are Not Authorized To Use The Application \"${mbkautheVar.APP_NAME}\"` }); } } if ((mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLocaleLowerCase() === "true") { const query = `SELECT "TwoFAStatus" FROM "TwoFA" WHERE "UserName" = $1`; const twoFAResult = await dblogin.query(query, [username]); if (twoFAResult.rows.length > 0 && twoFAResult.rows[0].TwoFAStatus) { // 2FA is enabled, prompt for token on a separate page req.session.preAuthUser = { id: user.id, username: user.UserName, role: user.Role, }; console.log(`[mbkauthe] 2FA required for user: ${username}`); return res.json({ success: true, twoFactorRequired: true }); } } // If 2FA is not enabled, proceed with login const userForSession = { id: user.id, username: user.UserName, role: user.Role, }; await completeLoginProcess(req, res, userForSession); } catch (err) { console.log("[mbkauthe] Error during login process:", err); res.status(500).json({ success: false, message: "Internal Server Error" }); } }); router.get("/mbkauthe/2fa", csrfProtection, (req, res) => { if (!req.session.preAuthUser) { return res.redirect("/mbkauthe/login"); } res.render("2fa.handlebars", { layout: false, customURL: mbkautheVar.loginRedirectURL || '/home', csrfToken: req.csrfToken(), }); }); router.post("/mbkauthe/api/verify-2fa", async (req, res) => { if (!req.session.preAuthUser) { return res.status(401).json({ success: false, message: "Not authorized. Please login first." }); } const { token } = req.body; const { username, id, role } = req.session.preAuthUser; if (!token) { return res.status(400).json({ success: false, message: "2FA token is required" }); } try { const query = `SELECT "TwoFASecret" FROM "TwoFA" WHERE "UserName" = $1`; const twoFAResult = await dblogin.query(query, [username]); if (twoFAResult.rows.length === 0 || !twoFAResult.rows[0].TwoFASecret) { return res.status(500).json({ success: false, message: "2FA is not configured correctly." }); } const sharedSecret = twoFAResult.rows[0].TwoFASecret; const tokenValidates = speakeasy.totp.verify({ secret: sharedSecret, encoding: "base32", token: token, window: 1, }); if (!tokenValidates) { console.log(`[mbkauthe] Invalid 2FA code for username: ${username}`); return res.status(401).json({ success: false, message: "Invalid 2FA code" }); } // 2FA successful, complete login const userForSession = { id, username, role }; const redirectUrl = mbkautheVar.loginRedirectURL || '/home'; await completeLoginProcess(req, res, userForSession, redirectUrl); } catch (err) { console.log("[mbkauthe] Error during 2FA verification:", err); res.status(500).json({ success: false, message: "Internal Server Error" }); } }); router.post("/mbkauthe/api/logout", async (req, res) => { if (req.session.user) { try { const { id, username } = req.session.user; await dblogin.query(`UPDATE "Users" SET "SessionId" = NULL WHERE "id" = $1`, [id]); if (req.sessionID) { await dblogin.query('DELETE FROM "session" WHERE sid = $1', [req.sessionID]); } req.session.destroy((err) => { if (err) { console.log("[mbkauthe] Error destroying session:", err); return res.status(500).json({ success: false, message: "Logout failed" }); } const cookieOptions = getCookieOptions(); res.clearCookie("mbkauthe.sid", cookieOptions); res.clearCookie("sessionId", cookieOptions); res.clearCookie("username", cookieOptions); console.log(`[mbkauthe] User "${username}" logged out successfully`); res.status(200).json({ success: true, message: "Logout successful" }); }); } catch (err) { console.log("[mbkauthe] Database query error during logout:", err); res.status(500).json({ success: false, message: "Internal Server Error" }); } } else { res.status(400).json({ success: false, message: "Not logged in" }); } }); router.get("/mbkauthe/login", LoginLimit, csrfProtection, (req, res) => { return res.render("loginmbkauthe.handlebars", { layout: false, githubLoginEnabled: mbkautheVar.GITHUB_LOGIN_ENABLED, customURL: mbkautheVar.loginRedirectURL || '/home', userLoggedIn: !!req.session?.user, username: req.session?.user?.username || '', version: packageJson.version, appName: mbkautheVar.APP_NAME.toUpperCase(), csrfToken: req.csrfToken(), }); }); async function getLatestVersion() { try { const response = await fetch('https://raw.githubusercontent.com/MIbnEKhalid/mbkauthe/main/package.json'); if (!response.ok) { console.Error(`GitHub API responded with status ${response.status}`); return "0.0.0"; } const latestPackageJson = await response.json(); return latestPackageJson.version; } catch (error) { console.error('[mbkauthe] Error fetching latest version from GitHub:', error); return null; } } router.get(["/mbkauthe/info", "/mbkauthe/i"], LoginLimit, async (_, res) => { let latestVersion; try { latestVersion = await getLatestVersion(); //latestVersion = "Under Development"; // Placeholder for the latest version } catch (err) { console.error("[mbkauthe] Error fetching package-lock.json:", err); } try { res.render("info.handlebars", { layout: false, mbkautheVar: mbkautheVar, version: packageJson.version, latestVersion, }); } catch (err) { console.error("[mbkauthe] Error fetching version information:", err); res.status(500).send(` <html> <head> <title>Error</title> </head> <body> <h1>Error</h1> <p>Failed to fetch version information. Please try again later.</p> </body> </html> `); } }); /* // Configure GitHub Strategy for login passport.use('github-login', new GitHubStrategy({ clientID: process.env.GITHUB_CLIENT_ID, clientSecret: process.env.GITHUB_CLIENT_SECRET, callbackURL: '/mbkauthe/api/github/login/callback', scope: ['user:email'] }, async (accessToken, refreshToken, profile, done) => { try { // Check if this GitHub account is linked to any user const githubUser = await dblogin.query( 'SELECT ug.*, u."UserName", u."Role", u."Active", u."AllowedApps" FROM user_github ug JOIN "Users" u ON ug.user_name = u."UserName" WHERE ug.github_id = $1', [profile.id] ); if (githubUser.rows.length === 0) { // GitHub account is not linked to any user return done(new Error('GitHub account not linked to any user')); } const user = githubUser.rows[0]; // Check if the user account is active if (!user.Active) { return done(new Error('Account is inactive')); } // Check if user is authorized for this app (same logic as regular login) if (user.Role !== "SuperAdmin") { const allowedApps = user.AllowedApps; if (!allowedApps || !allowedApps.some(app => app.toLowerCase() === mbkautheVar.APP_NAME.toLowerCase())) { return done(new Error(`Not authorized to use ${mbkautheVar.APP_NAME}`)); } } // Return user data for login return done(null, { id: user.id, // This should be the user ID from the Users table username: user.UserName, role: user.Role, githubId: user.github_id, githubUsername: user.github_username }); } catch (err) { console.error('[mbkauthe] GitHub login error:', err); return done(err); } } )); // Serialize/Deserialize user for GitHub login passport.serializeUser((user, done) => { done(null, user); }); passport.deserializeUser((user, done) => { done(null, user); }); // Initialize passport router.use(passport.initialize()); router.use(passport.session()); // GitHub login initiation router.get('/mbkauthe/api/github/login', passport.authenticate('github-login')); // GitHub login callback router.get('/mbkauthe/api/github/login/callback', passport.authenticate('github-login', { failureRedirect: '/mbkauthe/login?error=github_auth_failed', session: false // We'll handle session manually }), async (req, res) => { try { const githubUser = req.user; // Find the actual user record const userQuery = `SELECT * FROM "Users" WHERE "UserName" = $1`; const userResult = await dblogin.query(userQuery, [githubUser.username]); if (userResult.rows.length === 0) { console.log(`[mbkauthe] GitHub login: User not found: ${githubUser.username}`); return res.redirect('/mbkauthe/login?error=user_not_found'); } const user = userResult.rows[0]; // Check 2FA if enabled if ((mbkautheVar.MBKAUTH_TWO_FA_ENABLE || "").toLowerCase() === "true") { const twoFAQuery = `SELECT "TwoFAStatus" FROM "TwoFA" WHERE "UserName" = $1`; const twoFAResult = await dblogin.query(twoFAQuery, [githubUser.username]); if (twoFAResult.rows.length > 0 && twoFAResult.rows[0].TwoFAStatus) { // 2FA is enabled, store pre-auth user and redirect to 2FA req.session.preAuthUser = { id: user.id, username: user.UserName, role: user.Role, loginMethod: 'github' }; console.log(`[mbkauthe] GitHub login: 2FA required for user: ${githubUser.username}`); return res.redirect('/mbkauthe/2fa'); } } // Complete login process const userForSession = { id: user.id, username: user.UserName, role: user.Role, }; // Generate session and complete login const sessionId = crypto.randomBytes(256).toString("hex"); console.log(`[mbkauthe] GitHub login: Generated session ID for username: ${user.UserName}`); // Delete old session record for this user await dblogin.query('DELETE FROM "session" WHERE username = $1', [user.UserName]); await dblogin.query(`UPDATE "Users" SET "SessionId" = $1 WHERE "id" = $2`, [ sessionId, user.id, ]); req.session.user = { id: user.id, username: user.UserName, role: user.Role, sessionId, }; req.session.save(async (err) => { if (err) { console.log("[mbkauthe] GitHub login session save error:", err); return res.redirect('/mbkauthe/login?error=session_error'); } try { await dblogin.query( 'UPDATE "session" SET username = $1 WHERE sid = $2', [user.UserName, req.sessionID] ); } catch (e) { console.log("[mbkauthe] GitHub login: Failed to update username in session table:", e); } const cookieOptions = getCookieOptions(); res.cookie("sessionId", sessionId, cookieOptions); console.log(`[mbkauthe] GitHub login: User "${user.UserName}" logged in successfully`); // Redirect to the configured URL or home const redirectUrl = mbkautheVar.loginRedirectURL || '/home'; res.redirect(redirectUrl); }); } catch (err) { console.error('[mbkauthe] GitHub login callback error:', err); res.redirect('/mbkauthe/login?error=internal_error'); } } ); */ export { getLatestVersion }; export default router;