Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
258 changes: 258 additions & 0 deletions controllers/security.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');
const webauthnConfig = require('../config/webauthn');
const User = require('../models/User');

exports.getTwoFactorSetup = async (req, res) => {
try {
const user = await User.findById(req.user.id);

if (user.twoFactorEnabled) {
req.flash('info', { msg: 'Two-factor authentication is already enabled.' });
return res.redirect('/account');
}

// Generate a new secret
const secret = speakeasy.generateSecret({
name: `${process.env.APP_NAME || 'Express App'} (${user.email})`,
issuer: process.env.APP_NAME || 'Express App'
});

// Generate QR code
const qrCodeUrl = await QRCode.toDataURL(secret.otpauth_url);

// Store temporary secret in session
req.session.tempTwoFactorSecret = secret.base32;

res.render('account/twofactor-setup', {
title: 'Setup Two-Factor Authentication',
secret: secret.base32,
qrCodeUrl
});
} catch (error) {
console.error('Two-factor setup error:', error);
req.flash('errors', { msg: 'Error setting up two-factor authentication.' });
res.redirect('/account');
}
};

exports.postTwoFactorVerify = async (req, res) => {
try {
const { token } = req.body;
const user = await User.findById(req.user.id);

if (user.twoFactorEnabled) {
req.flash('errors', { msg: 'Two-factor authentication is already enabled.' });
return res.redirect('/account');
}

const verified = speakeasy.totp.verify({
secret: req.session.tempTwoFactorSecret,
encoding: 'base32',
token,
window: 1
});

if (verified) {
user.twoFactorEnabled = true;
user.twoFactorSecret = req.session.tempTwoFactorSecret;
user.generateBackupCodes();
await user.save();

req.session.tempTwoFactorSecret = null;

req.flash('success', {
msg: 'Two-factor authentication enabled successfully. Please save your backup codes.'
});

res.render('account/backup-codes', {
title: 'Backup Codes',
backupCodes: user.twoFactorBackupCodes
});
} else {
req.flash('errors', { msg: 'Invalid verification code. Please try again.' });
res.redirect('/security/2fa/setup');
}
} catch (error) {
console.error('Two-factor verification error:', error);
req.flash('errors', { msg: 'Error verifying two-factor authentication.' });
res.redirect('/security/2fa/setup');
}
};

exports.postTwoFactorDisable = async (req, res) => {
try {
const { token } = req.body;
const user = await User.findById(req.user.id);

if (!user.twoFactorEnabled) {
req.flash('errors', { msg: 'Two-factor authentication is not enabled.' });
return res.redirect('/account');
}

const verified = speakeasy.totp.verify({
secret: user.twoFactorSecret,
encoding: 'base32',
token,
window: 1
});

if (verified) {
user.twoFactorEnabled = false;
user.twoFactorSecret = undefined;
user.twoFactorBackupCodes = [];
await user.save();

req.flash('success', { msg: 'Two-factor authentication disabled successfully.' });
} else {
req.flash('errors', { msg: 'Invalid verification code. Please try again.' });
}

res.redirect('/account');
} catch (error) {
console.error('Two-factor disable error:', error);
req.flash('errors', { msg: 'Error disabling two-factor authentication.' });
res.redirect('/account');
}
};

exports.getWebAuthnRegistration = async (req, res) => {
try {
const user = await User.findById(req.user.id);
const options = await webauthnConfig.generateRegistrationOptions(user);

req.session.webauthnChallenge = options.challenge;

res.json(options);
} catch (error) {
console.error('WebAuthn registration options error:', error);
res.status(500).json({ error: 'Failed to generate registration options' });
}
};

exports.postWebAuthnRegistration = async (req, res) => {
try {
const { attestationResponse, deviceName } = req.body;
const user = await User.findById(req.user.id);

const verification = await webauthnConfig.verifyRegistrationResponse(
attestationResponse,
req.session.webauthnChallenge
);

if (verification.verified) {
const { credential } = verification.registrationInfo;

user.addWebAuthnCredential({
credentialID: credential.id,
publicKey: Buffer.from(credential.publicKey).toString('hex'),
counter: credential.counter,
transports: attestationResponse.response.transports || [],
deviceType: this.getDeviceType(attestationResponse),
name: deviceName || 'Security Key'
});

await user.save();
req.session.webauthnChallenge = null;

res.json({ success: true, message: 'Biometric credential registered successfully' });
} else {
res.status(400).json({ error: 'Registration verification failed' });
}
} catch (error) {
console.error('WebAuthn registration error:', error);
res.status(500).json({ error: 'Registration failed' });
}
};

exports.getWebAuthnAuthentication = async (req, res) => {
try {
const options = await webauthnConfig.generateAuthenticationOptions();
req.session.webauthnChallenge = options.challenge;
res.json(options);
} catch (error) {
console.error('WebAuthn authentication options error:', error);
res.status(500).json({ error: 'Failed to generate authentication options' });
}
};

exports.postWebAuthnLogin = async (req, res, next) => {
try {
const { assertionResponse, email } = req.body;

const user = await User.findOne({ email: email.toLowerCase() });
if (!user) {
return res.status(400).json({ error: 'Invalid credentials' });
}

// Find the credential used for authentication
const credential = user.webauthnCredentials.find(
cred => cred.credentialID === assertionResponse.id
);

if (!credential) {
return res.status(400).json({ error: 'Unknown credential' });
}

const verification = await webauthnConfig.verifyAuthenticationResponse(
assertionResponse,
req.session.webauthnChallenge,
credential
);

if (verification.verified) {
// Update credential counter
credential.counter = verification.authenticationInfo.newCounter;
await user.save();

req.session.webauthnChallenge = null;

// Log the user in
req.logIn(user, (err) => {
if (err) { return next(err); }
res.json({ success: true, redirectTo: req.session.returnTo || '/' });
});
} else {
res.status(400).json({ error: 'Authentication verification failed' });
}
} catch (error) {
console.error('WebAuthn login error:', error);
res.status(500).json({ error: 'Authentication failed' });
}
};

exports.getDeviceType = (attestationResponse) => {
// Simple device type detection based on user agent and response
const ua = attestationResponse.clientExtensionResults?.userAgent;
if (ua?.includes('Windows')) return 'Windows Hello';
if (ua?.includes('Mac')) return 'Touch ID';
if (ua?.includes('Android')) return 'Android Biometric';
if (ua?.includes('iPhone')) return 'Face ID/Touch ID';
return 'Security Key';
};

exports.getSecuritySettings = async (req, res) => {
const user = await User.findById(req.user.id);
res.render('account/security', {
title: 'Security Settings',
twoFactorEnabled: user.twoFactorEnabled,
webauthnCredentials: user.webauthnCredentials
});
};

exports.postRemoveWebAuthnCredential = async (req, res) => {
try {
const { credentialId } = req.body;
const user = await User.findById(req.user.id);

user.removeWebAuthnCredential(credentialId);
await user.save();

req.flash('success', { msg: 'Biometric credential removed successfully.' });
res.redirect('/account/security');
} catch (error) {
console.error('Remove WebAuthn credential error:', error);
req.flash('errors', { msg: 'Error removing biometric credential.' });
res.redirect('/account/security');
}
};
101 changes: 101 additions & 0 deletions controllers/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -706,3 +706,104 @@ exports.postLogoutEverywhere = async (req, res, next) => {
return next(err);
}
};

const speakeasy = require('speakeasy');

// Update the postLogin method to handle 2FA
exports.postLogin = async (req, res, next) => {
try {
const { email, password, token } = req.body;

// Find user and validate password (your existing logic)
const user = await User.findOne({ email: email.toLowerCase() });
if (!user || !(await user.comparePassword(password))) {
req.flash('errors', { msg: 'Invalid email or password.' });
return res.redirect('/login');
}

// Check if 2FA is enabled
if (user.twoFactorEnabled) {
if (!token) {
// Store user ID in session and show 2FA prompt
req.session.twoFactorUserId = user._id.toString();
return res.render('account/twofactor-prompt', {
title: 'Two-Factor Authentication',
email: user.email
});
}

// Verify 2FA token
const verified = speakeasy.totp.verify({
secret: user.twoFactorSecret,
encoding: 'base32',
token,
window: 1
});

// Check backup codes if TOTP fails
if (!verified && !user.verifyBackupCode(token)) {
req.flash('errors', { msg: 'Invalid two-factor authentication code.' });
return res.render('account/twofactor-prompt', {
title: 'Two-Factor Authentication',
email: user.email
});
}

await user.save(); // Save if backup code was used
}

// Continue with normal login
req.logIn(user, (err) => {
if (err) { return next(err); }
req.session.twoFactorUserId = null;
req.flash('success', { msg: 'Success! You are logged in.' });
res.redirect(req.session.returnTo || '/');
});
} catch (error) {
next(error);
}
};

// Add 2FA verification endpoint
exports.postTwoFactorVerify = async (req, res, next) => {
try {
const { token } = req.body;

if (!req.session.twoFactorUserId) {
req.flash('errors', { msg: 'Two-factor authentication session expired.' });
return res.redirect('/login');
}

const user = await User.findById(req.session.twoFactorUserId);
if (!user) {
req.flash('errors', { msg: 'User not found.' });
return res.redirect('/login');
}

const verified = speakeasy.totp.verify({
secret: user.twoFactorSecret,
encoding: 'base32',
token,
window: 1
});

if (!verified && !user.verifyBackupCode(token)) {
req.flash('errors', { msg: 'Invalid two-factor authentication code.' });
return res.render('account/twofactor-prompt', {
title: 'Two-Factor Authentication',
email: user.email
});
}

await user.save(); // Save if backup code was used

req.logIn(user, (err) => {
if (err) { return next(err); }
req.session.twoFactorUserId = null;
req.flash('success', { msg: 'Success! You are logged in.' });
res.redirect(req.session.returnTo || '/');
});
} catch (error) {
next(error);
}
};
Loading