fix: cookie refresh stuff

This commit is contained in:
DeathKaioken 2026-01-18 21:57:22 +01:00
parent e86986d40c
commit 33c584e68e
5 changed files with 90 additions and 38 deletions

View File

@ -5,6 +5,62 @@ const { logger } = require('../../middleware/logger');
const UnitOfWork = require('../../database/UnitOfWork'); const UnitOfWork = require('../../database/UnitOfWork');
const db = require('../../database/database'); const db = require('../../database/database');
// ---- Refresh token cookie helpers (keep aligned with BFF/frontend expectations) ----
function normalizeSameSite(v) {
const raw = String(v || '').toLowerCase();
if (raw === 'none') return 'none';
if (raw === 'strict') return 'strict';
return 'lax'; // default
}
function getRefreshCookieDomain() {
// In dev/localhost, setting Domain breaks cookies; only set Domain in prod (or if explicitly provided).
const envDomain = process.env.COOKIE_DOMAIN || process.env.REFRESH_COOKIE_DOMAIN;
if (envDomain) return envDomain;
if (process.env.NODE_ENV === 'production') return '.profit-planet.partners';
return undefined;
}
function shouldUseSecureCookie(req) {
// Works behind proxies if x-forwarded-proto is present; still prefer explicit env override.
const override = process.env.COOKIE_SECURE;
if (override != null) return String(override).toLowerCase() === 'true';
const xfProto = String(req.headers['x-forwarded-proto'] || '').toLowerCase();
const isHttps = req.secure || xfProto === 'https';
return process.env.NODE_ENV === 'production' || isHttps;
}
function buildRefreshCookieOptions(req, refreshTokenExpires) {
const sameSite = normalizeSameSite(process.env.COOKIE_SAMESITE || 'lax');
const secure = shouldUseSecureCookie(req);
// SameSite=None requires Secure; force it if configured that way.
const finalSecure = sameSite === 'none' ? true : secure;
const expires = refreshTokenExpires ? new Date(refreshTokenExpires) : undefined;
const maxAge = expires ? Math.max(0, expires.getTime() - Date.now()) : undefined;
return {
domain: getRefreshCookieDomain(), // e.g. .profit-planet.partners (shared across subdomains)
path: '/', // important for consistent clearing
httpOnly: true,
secure: finalSecure,
sameSite,
...(expires ? { expires } : {}),
...(typeof maxAge === 'number' ? { maxAge } : {})
};
}
function buildRefreshCookieClearOptions(req) {
// Must match Domain+Path+Secure+SameSite used when setting.
const opts = buildRefreshCookieOptions(req, null);
return {
...opts,
expires: new Date(0),
maxAge: 0
};
}
class LoginController { class LoginController {
static async login(req, res) { static async login(req, res) {
try { try {
@ -56,13 +112,8 @@ class LoginController {
} }
// Step 5: If credentials valid, allow login (do NOT increment rate limit) // Step 5: If credentials valid, allow login (do NOT increment rate limit)
// Set refresh token as HTTP-only cookie // Set refresh token as HTTP-only cookie (shared across .profit-planet.partners)
res.cookie('refreshToken', result.refreshToken, { res.cookie('refreshToken', result.refreshToken, buildRefreshCookieOptions(req, result.refreshTokenExpires));
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
expires: result.refreshTokenExpires
});
// --- Send login notification email --- // --- Send login notification email ---
try { try {
@ -101,15 +152,11 @@ class LoginController {
logger.warn('No refresh token provided'); logger.warn('No refresh token provided');
return res.status(401).json({ success: false, message: 'No refresh token provided' }); return res.status(401).json({ success: false, message: 'No refresh token provided' });
} }
const result = await LoginService.refresh(refreshToken); const result = await LoginService.refresh(refreshToken);
// FIX: Set new refresh token as HTTP-only cookie after rotation // Always set the rotated refresh token cookie with consistent attributes.
res.cookie('refreshToken', result.refreshToken, { res.cookie('refreshToken', result.refreshToken, buildRefreshCookieOptions(req, result.refreshTokenExpires));
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
expires: result.refreshTokenExpires
});
logger.info('Refresh token rotated', { userId: result.user.id }); logger.info('Refresh token rotated', { userId: result.user.id });
return res.status(200).json({ return res.status(200).json({
@ -191,30 +238,15 @@ class LoginController {
// continue to attempt to clear cookie even if service logout fails // continue to attempt to clear cookie even if service logout fails
} }
// Explicitly expire the refresh token cookie // Clear cookie using SAME Domain+Path+SameSite+Secure
try { try {
res.cookie('refreshToken', '', { res.cookie('refreshToken', '', buildRefreshCookieClearOptions(req));
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
path: '/',
expires: new Date(0)
});
const setCookieHeader = res.getHeader && res.getHeader('Set-Cookie');
logger.info('[LOGOUT] Set-Cookie header after expiring cookie', { setCookieHeader });
} catch (cookieErr) { } catch (cookieErr) {
logger.error('[LOGOUT] Error while setting expired cookie', { error: cookieErr }); logger.error('[LOGOUT] Error while setting expired cookie', { error: cookieErr });
} }
// Clear the cookie as well
try { try {
res.clearCookie('refreshToken', { res.clearCookie('refreshToken', buildRefreshCookieClearOptions(req));
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
path: '/'
});
logger.info('[LOGOUT] res.clearCookie called', { headersSent: !!res.headersSent });
} catch (clearErr) { } catch (clearErr) {
logger.error('[LOGOUT] Error while calling res.clearCookie', { error: clearErr }); logger.error('[LOGOUT] Error while calling res.clearCookie', { error: clearErr });
} }

View File

@ -304,6 +304,17 @@ const createDatabase = async () => {
`); `);
console.log('✅ Refresh tokens table created/verified'); console.log('✅ Refresh tokens table created/verified');
// Ensure refresh token column length supports longer tokens (JWT/base64/etc.)
try {
await connection.query(`
ALTER TABLE refresh_tokens
MODIFY COLUMN token VARCHAR(512) NOT NULL;
`);
console.log('🔧 Ensured refresh_tokens.token is VARCHAR(512)');
} catch (e) {
console.log(' refresh_tokens.token length ALTER skipped:', e.message);
}
// 6. email_verifications table: For email verification codes // 6. email_verifications table: For email verification codes
await connection.query(` await connection.query(`
CREATE TABLE IF NOT EXISTS email_verifications ( CREATE TABLE IF NOT EXISTS email_verifications (

View File

@ -2,6 +2,9 @@ const express = require('express');
const path = require('path'); const path = require('path');
const router = express.Router(); const router = express.Router();
// NOTE: If the frontend ever calls backend routes directly with cookies,
// CORS must allow credentials and a non-* origin; otherwise refreshToken cookies won't be sent.
const authMiddleware = require('../middleware/authMiddleware'); const authMiddleware = require('../middleware/authMiddleware');
const UserSettingsController = require('../controller/auth/UserSettingsController'); const UserSettingsController = require('../controller/auth/UserSettingsController');
const ReferralTokenController = require('../controller/referral/ReferralTokenController'); const ReferralTokenController = require('../controller/referral/ReferralTokenController');

View File

@ -35,7 +35,10 @@ const upload = multer({ storage: multer.memoryStorage() });
console.log('🛣️ Setting up POST routes'); console.log('🛣️ Setting up POST routes');
// auth POSTs (moved from routes/auth.js) // auth POSTs (BFF/frontend contract notes):
// - /login MUST set refreshToken cookie (Domain=.profit-planet.partners, Path=/, HttpOnly, Secure in prod, SameSite=Lax by default)
// - /refresh MUST rotate + also Set-Cookie refreshToken=<new> with SAME attributes
// - /logout MUST revoke server-side + clear cookie using SAME Domain+Path+SameSite+Secure (Max-Age=0)
router.post('/login', LoginController.login); router.post('/login', LoginController.login);
router.post('/refresh', LoginController.refresh); router.post('/refresh', LoginController.refresh);
router.post('/logout', LoginController.logout); router.post('/logout', LoginController.logout);

View File

@ -3,11 +3,14 @@ const UnitOfWork = require('../database/UnitOfWork');
const argon2 = require('argon2'); const argon2 = require('argon2');
async function createAdminUser() { async function createAdminUser() {
// const adminEmail = process.env.ADMIN_EMAIL || 'office@profit-planet.com'; // Avoid hardcoding credentials in repo; require env vars.
// const adminEmail = process.env.ADMIN_EMAIL || 'alexander.ibrahim.ai@gmail.com'; const adminEmail = process.env.ADMIN_EMAIL;
const adminEmail = process.env.ADMIN_EMAIL || 'loki.aahi@gmail.com'; const adminPassword = process.env.ADMIN_PASSWORD;
const adminPassword = process.env.ADMIN_PASSWORD || 'Chalanger75$%';
// const adminPassword = process.env.ADMIN_PASSWORD || 'W.profit-planet.com.2025'; if (!adminEmail || !adminPassword) {
throw new Error('ADMIN_EMAIL and ADMIN_PASSWORD must be set (refusing to use hardcoded defaults).');
}
const firstName = process.env.ADMIN_FIRST_NAME || 'Admin'; const firstName = process.env.ADMIN_FIRST_NAME || 'Admin';
const lastName = process.env.ADMIN_LAST_NAME || 'User'; const lastName = process.env.ADMIN_LAST_NAME || 'User';