Harden web session handling and production config

This commit is contained in:
2026-04-07 19:48:48 -05:00
parent 890f3fff2b
commit 0292b2e85a
9 changed files with 96 additions and 20 deletions
+34 -2
View File
@@ -1,12 +1,41 @@
const authService = require('./auth.service');
const usersService = require('../users/users.service');
const { sendSuccess } = require('../../utils/apiResponse');
const env = require('../../config/env');
const getContext = (req) => ({
ip: req.ip?.replace('::ffff:', '') || null,
userAgent: req.headers['user-agent'] || null,
});
const getRefreshCookieOptions = () => ({
httpOnly: true,
secure: env.nodeEnv === 'production',
sameSite: 'lax',
path: '/',
maxAge: 7 * 24 * 60 * 60 * 1000,
});
const readRefreshToken = (req) => {
const cookieHeader = req.headers.cookie || '';
const cookies = Object.fromEntries(
cookieHeader
.split(';')
.map((entry) => entry.trim())
.filter(Boolean)
.map((entry) => {
const separatorIndex = entry.indexOf('=');
if (separatorIndex === -1) return [entry, ''];
return [
decodeURIComponent(entry.slice(0, separatorIndex)),
decodeURIComponent(entry.slice(separatorIndex + 1)),
];
})
);
return cookies[env.refreshCookieName] || req.body?.refreshToken || null;
};
const register = async (req, res, next) => {
try {
if (!req.file) {
@@ -33,6 +62,7 @@ const register = async (req, res, next) => {
const login = async (req, res, next) => {
try {
const result = await authService.login(req.body, getContext(req));
res.cookie(env.refreshCookieName, result.refreshToken, getRefreshCookieOptions());
return sendSuccess(res, result, 200);
} catch (err) {
return next(err);
@@ -41,7 +71,8 @@ const login = async (req, res, next) => {
const refresh = async (req, res, next) => {
try {
const result = await authService.refresh(req.body.refreshToken);
const result = await authService.refresh(readRefreshToken(req));
res.cookie(env.refreshCookieName, result.refreshToken, getRefreshCookieOptions());
return sendSuccess(res, result, 200);
} catch (err) {
return next(err);
@@ -50,7 +81,8 @@ const refresh = async (req, res, next) => {
const logout = async (req, res, next) => {
try {
await authService.logout(req.body.refreshToken);
await authService.logout(readRefreshToken(req));
res.clearCookie(env.refreshCookieName, getRefreshCookieOptions());
return sendSuccess(res, { message: 'Logged out.' }, 200);
} catch (err) {
return next(err);
+10 -4
View File
@@ -1,4 +1,7 @@
const db = require('../../config/db');
const crypto = require('crypto');
const hashRefreshToken = (token) => crypto.createHash('sha256').update(String(token)).digest('hex');
const ensureSchema = async () => {
const columns = [
@@ -144,22 +147,25 @@ const updateUnlockToken = async (userId, unlockToken, unlockTokenExpires) => {
};
const saveRefreshToken = async (userId, token, expiresAt) => {
const hashedToken = hashRefreshToken(token);
await db.execute(
'INSERT INTO refresh_tokens (user_id, token, expires_at) VALUES (?, ?, ?)',
[userId, token, expiresAt]
[userId, hashedToken, expiresAt]
);
};
const findRefreshToken = async (token) => {
const hashedToken = hashRefreshToken(token);
const [rows] = await db.execute(
'SELECT rt.*, u.id as uid, u.email, u.name, u.role, u.is_verified, u.is_active FROM refresh_tokens rt JOIN users u ON rt.user_id = u.id WHERE rt.token = ? AND rt.expires_at > NOW()',
[token]
'SELECT rt.*, u.id as uid, u.email, u.name, u.role, u.is_verified, u.is_active FROM refresh_tokens rt JOIN users u ON rt.user_id = u.id WHERE rt.token IN (?, ?) AND rt.expires_at > NOW()',
[hashedToken, token]
);
return rows[0] || null;
};
const deleteRefreshToken = async (token) => {
await db.execute('DELETE FROM refresh_tokens WHERE token = ?', [token]);
const hashedToken = hashRefreshToken(token);
await db.execute('DELETE FROM refresh_tokens WHERE token IN (?, ?)', [hashedToken, token]);
};
const deleteUserRefreshTokens = async (userId) => {
+7
View File
@@ -267,6 +267,12 @@ const login = async ({ email, password }, context = {}) => {
};
const refresh = async (token) => {
if (!token) {
const err = new Error('Invalid or expired refresh token.');
err.statusCode = 401;
throw err;
}
const record = await authRepository.findRefreshToken(token);
if (!record) {
const err = new Error('Invalid or expired refresh token.');
@@ -293,6 +299,7 @@ const refresh = async (token) => {
};
const logout = async (token) => {
if (!token) return;
await authRepository.deleteRefreshToken(token);
};
+1 -1
View File
@@ -35,7 +35,7 @@ const loginSchema = Joi.object({
});
const refreshSchema = Joi.object({
refreshToken: Joi.string().required(),
refreshToken: Joi.string().optional(),
});
const changePasswordSchema = Joi.object({