const base64 = require('base-64'); const sha512 = require('js-sha512'); const shortid = require('shortid'); const owaspPw = require('owasp-password-strength-test'); const db = require('./db'); const { consoleBackend } = require('./email'); const buildSessionCookie = ({ username, sessionToken, isLogout }) => { if (!isLogout) { const tokenData = { username, sessionToken, }; const tokenDataStr = JSON.stringify(tokenData); const encodedToken = base64.encode(tokenDataStr); return `sessionCookie=${encodedToken}; Domain=localhost; HttpOnly; SameSite=strict;`; } else { return `sessionCookie=deleted; Domain=localhost; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; SameSite=strict;`; } }; const authMiddleware = async (req, res, next) => { const cookies = req.cookies; const encodedCookie = cookies.sessionCookie; if (!encodedCookie) { res.status(403); return res.json({ message: 'Log in first!', }); } const sessionCookieStr = base64.decode(encodedCookie); const sessionCookie = JSON.parse(sessionCookieStr); const { username, sessionToken } = sessionCookie; const user = await db('users').where({ username }).first(); if (!user) { res.status(400); res.set('Set-Cookie', buildSessionCookie({ isLogout: true })); return res.json({ message: 'Invalid session token.', sessionCookie, }); } const salt = user.salt; const sessionTokenHash = sha512(salt + sessionToken); const token = await db('tokens').where({ type: 'session', value: sessionTokenHash }); if (!token) { res.status(400); res.set('Set-Cookie', buildSessionCookie({ isLogout: true })); return res.json({ message: 'Invalid session token.', sessionCookie, }); } req.sessionTokenHash = sessionTokenHash; req.user = user; next(); }; const registerRoute = async (req, res) => { const { username, password, email } = req.body; if (!username || !password) { res.status(400); return res.json({ error: 'Username or password missing.', }); } const existingUser = await db('users').where({ username }).first(); if (existingUser) { res.status(400); return res.json({ error: 'Username already taken.', }); } let validationErrors = []; const usernameMinLength = 5; const usernameMaxLength = 20; if (username.length < usernameMinLength) { validationErrors.push(`Username must be at least ${usernameMinLength} characters long.`); } if (username.length > usernameMaxLength) { validationErrors.push(`Username must be at most ${usernameMaxLength} characters long.`); } const owaspPwTestResults = owaspPw.test(password); if (!owaspPwTestResults.strong) { validationErrors = validationErrors.concat(owaspPwTestResults.errors); } if (validationErrors.length > 0) { res.status(400); return res.json({ errors: validationErrors, }); } const salt = shortid.generate(); const pwHash = sha512(salt + password); const insertedUsers = await db('users').insert({ username, password: pwHash, email, salt }).returning('*'); const user = insertedUsers[0]; const activationToken = shortid.generate(); await db('tokens').insert({ user_id: user.id, type: 'activation', value: activationToken }); await consoleBackend.send({ recipients: [ username ], subject: `Welcome to the service, ${username}!`, body: `Welcome.\n\nActivate your account at http://localhost:3000/activate/${activationToken}.\n\nBR,\nTeam`, }); res.set('Set-Cookie', buildSessionCookie({ isLogout: true })); return res.json({ username, }); }; const loginRoute = async (req, res) => { const { username, password, remember } = req.body; if (!username || !password) { res.status(400); return res.json({ error: 'Username or password missing.', }); } const user = await db('users').where({ username }).first(); if (!user) { res.status(400); return res.json({ error: 'Wrong username or password', }); } const salt = user.salt; const pwHash = sha512(salt + password); if (pwHash !== user.password) { res.status(400); return res.json({ error: 'Wrong username or password', }); } const sessionToken = shortid.generate(); const sessionTokenHash = sha512(user.salt + sessionToken); await db('tokens').insert({ user_id: user.id, type: 'session', value: sessionTokenHash }); res.set('Set-Cookie', buildSessionCookie({ username, sessionToken, isLogout: false })); return res.json({ username, }); }; const logoutRoute = async (req, res) => { await db('tokens').where({ type: 'session', user_id: req.user.id, value: req.sessionTokenHash }).delete(); res.status(204); res.set('Set-Cookie', buildSessionCookie({ isLogout: true })); res.send(); }; const activateRoute = async (req, res) => { const { activationToken } = req.body; if (!activationToken) { res.status(400); return res.json({ error: 'Missing or invalid activation token.', }); } const tokenObj = await db('tokens').where({ type: 'activation', value: activationToken }).first(); if (!tokenObj) { res.status(400); return res.json({ error: 'Missing or invalid activation token.', }); } const updatedUsers = await db('users').where({ id: tokenObj.user_id }).update({ activated: true }).returning('*'); const user = updatedUsers[0]; await db('tokens').where({ type: 'activation', value: activationToken }).delete(); const { username } = user; const sessionToken = shortid.generate(); const sessionTokenHash = sha512(user.salt + sessionToken); await db('tokens').insert({ user_id: user.id, type: 'session', value: sessionTokenHash }); res.set('Set-Cookie', buildSessionCookie({ username, sessionToken, isLogout: false })); return res.json({ username, }); }; module.exports = { buildSessionCookie, authMiddleware, registerRoute, loginRoute, logoutRoute, activateRoute, };