mbkauthe
Version:
MBKTech's reusable authentication system for Node.js applications.
150 lines (139 loc) • 6.11 kB
JavaScript
import session from "express-session";
import pgSession from "connect-pg-simple";
const PgSession = pgSession(session);
import { dblogin, runWithRequestContext } from "#pool.js";
import { mbkautheVar } from "#config.js";
import { cachedCookieOptions, decryptSessionId, encryptSessionId } from "#cookies.js";
// Session configuration
export const sessionConfig = {
store: new PgSession({
pool: dblogin,
tableName: "session",
createTableIfMissing: true,
// Prevent connect-pg-simple from touching the session row on every request.
// This avoids an UPDATE per request, which can significantly reduce DB load under burst traffic.
// The session will still expire based on the cookie maxAge and the TTL stored when the session is saved.
disableTouch: true
}),
secret: mbkautheVar.SESSION_SECRET_KEY,
resave: false,
saveUninitialized: false,
proxy: true,
cookie: {
maxAge: mbkautheVar.COOKIE_EXPIRE_TIME * 24 * 60 * 60 * 1000,
// Don't set domain in development/localhost to avoid cookie issues
domain: (mbkautheVar.IS_DEPLOYED === 'true' && process.env.test !== 'dev') ? `.${mbkautheVar.DOMAIN}` : undefined,
httpOnly: true,
// Only use secure cookies in production with HTTPS
secure: mbkautheVar.IS_DEPLOYED === 'true' && process.env.test !== 'dev',
sameSite: 'lax',
path: '/'
},
name: 'mbkauthe.sid'
};
// CORS middleware
export function corsMiddleware(req, res, next) {
const origin = req.headers.origin;
if (origin) {
try {
const originUrl = new URL(origin);
const allowedDomain = `.${mbkautheVar.DOMAIN}`;
// Exact match or subdomain match
if (originUrl.hostname === mbkautheVar.DOMAIN ||
(originUrl.hostname.endsWith(allowedDomain) && originUrl.hostname.charAt(originUrl.hostname.length - allowedDomain.length - 1) !== '.')) {
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');
}
} catch (err) {
// Invalid origin URL, skip CORS headers
}
}
next();
}
// Session restoration middleware
export async function sessionRestorationMiddleware(req, res, next) {
// Only restore session if not already present and sessionId cookie exists
if (!req.session.user && req.cookies.sessionId) {
// Decrypt the sessionId from cookie
const sessionId = decryptSessionId(req.cookies.sessionId);
// Early validation to avoid unnecessary processing (expect DB UUID id)
if (!sessionId || typeof sessionId !== 'string' || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(sessionId)) {
// Clear invalid cookie to prevent repeated attempts
res.clearCookie('sessionId', {
domain: mbkautheVar.IS_DEPLOYED === 'true' ? `.${mbkautheVar.DOMAIN}` : undefined,
path: '/',
httpOnly: true,
secure: mbkautheVar.IS_DEPLOYED === 'true',
sameSite: 'lax'
});
return next();
}
try {
// Validate session by DB primary key id and join to user
const query = `SELECT u.id, u."UserName", u."Active", u."Role", u."AllowedApps", s.expires_at
FROM "Sessions" s
JOIN "Users" u ON s."UserName" = u."UserName"
WHERE s.id = $1 LIMIT 1`;
const result = await dblogin.query({ name: 'restore-user-session', text: query, values: [sessionId] });
if (result.rows.length > 0) {
const row = result.rows[0];
// Reject expired sessions or inactive users
if ((row.expires_at && new Date(row.expires_at) <= new Date()) || !row.Active) {
// leave cookies cleared and don't restore session
} else {
const normalizedSessionId = String(sessionId);
req.session.user = {
id: row.id,
username: row.UserName,
role: row.Role,
sessionId: normalizedSessionId,
allowedApps: row.AllowedApps,
};
// Use cached FullName from client cookie when available to avoid extra DB queries
if (req.cookies && req.cookies.fullName && typeof req.cookies.fullName === 'string') {
req.session.user.fullname = req.cookies.fullName;
} else {
// Fallback: attempt to fetch FullName from Users to populate session
try {
const profileRes = await dblogin.query({
name: 'restore-get-fullname',
text: 'SELECT "FullName" FROM "Users" WHERE "UserName" = $1 LIMIT 1',
values: [row.UserName]
});
if (profileRes.rows.length > 0 && profileRes.rows[0].FullName) {
req.session.user.fullname = profileRes.rows[0].FullName;
}
} catch (profileErr) {
console.error(`[mbkauthe] Error fetching FullName during session restore:`, profileErr);
}
}
}
}
} catch (err) {
console.error(`[mbkauthe] Session restoration error:`, err);
}
}
next();
}
// Session cookie sync middleware
export function sessionCookieSyncMiddleware(req, res, next) {
if (req.session && req.session.user) {
// Decrypt existing cookie to compare with session
const currentDecryptedId = decryptSessionId(req.cookies.sessionId);
// Only set cookies if they're missing or different
if (currentDecryptedId !== req.session.user.sessionId) {
res.cookie("fullName", req.session.user.fullname || req.session.user.username, { ...cachedCookieOptions, httpOnly: false });
const encryptedSessionId = encryptSessionId(req.session.user.sessionId);
if (encryptedSessionId) {
res.cookie("sessionId", encryptedSessionId, cachedCookieOptions);
}
}
}
next();
}
// Request context middleware (used for DB query logging)
export function requestContextMiddleware(req, res, next) {
return runWithRequestContext(req, () => next());
}