69 lines
2.4 KiB
JavaScript
69 lines
2.4 KiB
JavaScript
const express = require('express');
|
|
const router = express.Router();
|
|
const pool = require('../../db');
|
|
const crypto = require('crypto');
|
|
const { promisify } = require('util');
|
|
const { sanitizeLoginInput } = require('../../middleware/sanitize_person_input');
|
|
const { strictInput } = require('../../middleware/strict_input');
|
|
const { signToken } = require('../../middleware/authenticate_token');
|
|
|
|
const scrypt = promisify(crypto.scrypt);
|
|
|
|
// Fixed salt for unknown emails: the scrypt work is still performed so the
|
|
// response time does not reveal whether the email address exists.
|
|
const DUMMY_SALT = '00000000000000000000000000000000';
|
|
|
|
async function login(req, res) {
|
|
try {
|
|
const { email, password } = req.loginInput;
|
|
const { rows } = await pool.query(
|
|
'SELECT id, password, token_version FROM people WHERE email = $1',
|
|
[email]
|
|
);
|
|
|
|
if (rows.length === 0) {
|
|
await scrypt(password, DUMMY_SALT, 64);
|
|
res.status(401).send('Invalid email or password');
|
|
return;
|
|
}
|
|
|
|
const [salt, storedKeyHex] = rows[0].password.split(':');
|
|
const storedKey = Buffer.from(storedKeyHex || '', 'hex');
|
|
const derivedKey = await scrypt(password, salt, 64);
|
|
|
|
if (
|
|
storedKey.length !== derivedKey.length ||
|
|
!crypto.timingSafeEqual(storedKey, derivedKey)
|
|
) {
|
|
res.status(401).send('Invalid email or password');
|
|
return;
|
|
}
|
|
|
|
const sessionId = crypto.randomUUID();
|
|
const deviceName = (req.headers['user-agent'] || 'unknown').slice(0, 255);
|
|
await pool.query(
|
|
'INSERT INTO sessions (id, user_id, device_name) VALUES ($1, $2, $3)',
|
|
[sessionId, rows[0].id, deviceName],
|
|
);
|
|
|
|
const { token, expiresAt } = signToken(rows[0].id, {
|
|
jti: sessionId,
|
|
tokenVersion: rows[0].token_version,
|
|
});
|
|
res.cookie('fc_session_token', token, {
|
|
httpOnly: true,
|
|
secure: true,
|
|
sameSite: 'Strict',
|
|
path: '/',
|
|
});
|
|
res.status(200).json({ id: rows[0].id, token, expires_at: expiresAt });
|
|
}
|
|
catch (err) {
|
|
console.error('login failed', { message: err.message, code: err.code });
|
|
res.status(500).send('Login failed, request could not be completed');
|
|
}
|
|
};
|
|
|
|
router.post('/', strictInput({ body: ['email', 'password'] }), sanitizeLoginInput, login);
|
|
module.exports = router;
|