Files
LogiFlow/server/localAuth.production.ts
T
michaelschal 269e050009 Improve password handling to support multiple formats for compatibility
Enhance `localAuth.production.ts` to support backward compatibility for password hashing by implementing checks for `hash.salt`, `bcrypt`, plain text, and SHA256 formats, and migrate existing admin user passwords to the new `hash.salt` format.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: d396e5bd-e32d-4a20-9e7d-71e7102ddc6c
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/d396e5bd-e32d-4a20-9e7d-71e7102ddc6c/CrwFq15
2025-08-11 15:16:21 +00:00

242 lines
7.4 KiB
TypeScript

import passport from "passport";
import { Strategy as LocalStrategy } from "passport-local";
import type { Express } from "express";
import session from "express-session";
import { storage } from "./storage.js";
import connectPg from "connect-pg-simple";
import { scrypt, randomBytes, timingSafeEqual } from "crypto";
import { promisify } from "util";
const scryptAsync = promisify(scrypt);
console.log('🐳 PRODUCTION: Local auth configured with PostgreSQL sessions');
async function hashPassword(password: string) {
const salt = randomBytes(16).toString("hex");
const buf = (await scryptAsync(password, salt, 64)) as Buffer;
return `${buf.toString("hex")}.${salt}`;
}
async function comparePasswords(supplied: string, stored: string) {
console.log('🔐 Production password comparison', { suppliedLength: supplied.length, storedFormat: stored?.substring(0, 10) + '...' });
// Try different password formats for backward compatibility
// 1. Try new format: hash.salt
if (stored && stored.includes('.')) {
const [hashed, salt] = stored.split(".");
if (hashed && salt) {
try {
const hashedBuf = Buffer.from(hashed, "hex");
const suppliedBuf = (await scryptAsync(supplied, salt, 64)) as Buffer;
const result = timingSafeEqual(hashedBuf, suppliedBuf);
if (result) {
console.log('✅ Password matched with hash.salt format');
return true;
}
} catch (error) {
console.log('⚠️ hash.salt format failed, trying other formats');
}
}
}
// 2. Try bcrypt format (common in existing systems)
if (stored && stored.startsWith('$2')) {
try {
const bcrypt = await import('bcrypt');
const result = await bcrypt.compare(supplied, stored);
if (result) {
console.log('✅ Password matched with bcrypt format');
return true;
}
} catch (error) {
console.log('⚠️ bcrypt comparison failed');
}
}
// 3. Try plain text (for development/migration)
if (stored === supplied) {
console.log('✅ Password matched with plain text format');
return true;
}
// 4. Try simple hash format (legacy)
try {
const crypto = await import('crypto');
const hash = crypto.createHash('sha256').update(supplied).digest('hex');
if (stored === hash) {
console.log('✅ Password matched with SHA256 format');
return true;
}
} catch (error) {
console.log('⚠️ SHA256 comparison failed');
}
console.log('❌ No password format matched');
return false;
}
async function createDefaultAdminUser() {
try {
const existingAdmin = await storage.getUserByUsername('admin');
if (!existingAdmin) {
const hashedPassword = await hashPassword('admin');
await storage.createUser({
id: 'admin_prod',
username: 'admin',
email: 'admin@logiflow.com',
firstName: 'Administrateur',
lastName: 'Production',
password: hashedPassword,
role: 'admin',
passwordChanged: false,
});
console.log('✅ Production admin user created: admin/admin');
} else {
console.log('✅ Production admin user found:', {
id: existingAdmin.id,
username: existingAdmin.username,
passwordFormat: existingAdmin.password ? 'present' : 'missing'
});
// Check if password needs migration to new format
if (existingAdmin.password && !existingAdmin.password.includes('.')) {
console.log('🔄 Migrating admin password to new format...');
try {
const newHashedPassword = await hashPassword('admin');
await storage.updateUser(existingAdmin.id, { password: newHashedPassword });
console.log('✅ Admin password migrated to new format');
} catch (error) {
console.log('⚠️ Could not migrate password, will try multiple formats:', error.message);
}
}
}
} catch (error) {
console.error('Error managing admin user:', error);
}
}
export function setupLocalAuth(app: Express) {
// Create admin user on startup
createDefaultAdminUser();
const PostgresSessionStore = connectPg(session);
const sessionStore = new PostgresSessionStore({
conString: process.env.DATABASE_URL,
createTableIfMissing: true,
tableName: 'session',
});
const sessionSettings: session.SessionOptions = {
secret: process.env.SESSION_SECRET || 'production-fallback-secret-key',
resave: false,
saveUninitialized: false,
store: sessionStore,
cookie: {
httpOnly: true,
secure: false, // Set to true with HTTPS proxy
maxAge: 24 * 60 * 60 * 1000, // 24 hours
},
};
app.set("trust proxy", 1);
app.use(session(sessionSettings));
app.use(passport.initialize());
app.use(passport.session());
passport.use(
new LocalStrategy(
{
usernameField: 'username',
passwordField: 'password',
},
async (username, password, done) => {
try {
const user = await storage.getUserByUsername(username);
if (!user || !user.password) {
return done(null, false, { message: 'Invalid credentials' });
}
const isValidPassword = await comparePasswords(password, user.password);
if (!isValidPassword) {
return done(null, false, { message: 'Invalid credentials' });
}
return done(null, user);
} catch (error) {
return done(error);
}
}
)
);
passport.serializeUser((user, done) => done(null, user.id));
passport.deserializeUser(async (id: string, done) => {
try {
const user = await storage.getUserWithGroups(id);
done(null, user);
} catch (error) {
done(error);
}
});
// Login route
app.post("/api/login", (req, res, next) => {
passport.authenticate("local", (err: any, user: any, info: any) => {
if (err) return next(err);
if (!user) {
return res.status(400).json({ message: info?.message || "Invalid credentials" });
}
req.login(user, (err) => {
if (err) return next(err);
res.json({
id: user.id,
username: user.username,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
role: user.role,
passwordChanged: user.passwordChanged
});
});
})(req, res, next);
});
// Logout route
app.post("/api/logout", (req: any, res: any, next: any) => {
req.logout((err: any) => {
if (err) return next(err);
res.json({ message: "Logout successful" });
});
});
// Get current user
app.get("/api/user", (req: any, res) => {
if (req.isAuthenticated && req.isAuthenticated()) {
res.json({
id: req.user.id,
username: req.user.username,
email: req.user.email,
firstName: req.user.firstName,
lastName: req.user.lastName,
role: req.user.role,
passwordChanged: req.user.passwordChanged
});
} else {
res.status(401).json({ message: "Not authenticated" });
}
});
// Check default credentials endpoint
app.get("/api/default-credentials-check", (req, res) => {
res.json({ hasDefaultCredentials: true });
});
}
export function requireAuth(req: any, res: any, next: any) {
if (req.isAuthenticated && req.isAuthenticated()) {
return next();
}
res.status(401).json({ message: "Authentication required" });
}