Harden web session handling and production config
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ const loginSchema = Joi.object({
|
||||
});
|
||||
|
||||
const refreshSchema = Joi.object({
|
||||
refreshToken: Joi.string().required(),
|
||||
refreshToken: Joi.string().optional(),
|
||||
});
|
||||
|
||||
const changePasswordSchema = Joi.object({
|
||||
|
||||
Reference in New Issue
Block a user