/* global __appdir */ var jwt = require('jsonwebtoken') var path = require('path') var config = require(path.join(__appdir, 'config', 'authentication')) var db = require(path.join(__appdir, 'lib', 'sequelize')) var securePassword = require('secure-password') var pwd = securePassword() module.exports = { loginCookie, loginToken, logout, verifyToken, signup, changePassword, validateEmail } // Authentifivation method for the frontend using secure httpOnly cookies. (POST) function loginCookie (req, res) { var params = req.body verifyUser(res, params.username, params.password, token => { // The token has the form header.payload.signature // We split the cookie in header.payload and signature in two seperate cookies. // The signature cookie is httpOnly so JavaScript never has access to the full cookie. // Read more at: https://medium.com/lightrail/getting-token-authentication-right-in-a-stateless-single-page-application-57d0c6474e3 const split = token.split('.') const headerPayload = split[0] + '.' + split[1] const signature = split[2] res.cookie('jwt_hp', headerPayload, { secure: true, httpOnly: false, sameSite: 'strict' }) res.cookie('jwt_s', signature, { secure: true, httpOnly: true, sameSite: 'strict' }) return res.status(200).send({ auth: true, status: 'VALID' }) }) } // Authentification method for the API using the authorization header. (GET) function loginToken (req, res) { var body = req.body verifyUser(res, body.username, body.password, function (token) { return res.status(200).send({ auth: true, token }) }) } // Method for creating a new user. async function signup (req, res) { // TODO: Implement some security stuff. Not every user who call this request should be able to sign up. var params = req.body if (!params.username) return res.status(400).send({ auth: false, status: 'USER_MISSING', error_message: 'This service requires an username.' }) if (!validateUsername(params.username)) return res.status(400).send({ auth: false, status: 'INVALID_USERNAME', error_message: 'Username does not fullfill the requirements. (No whitespaces)' }) if (!params.password) return res.status(400).send({ auth: false, status: 'PASSWORD_MISSING', error_message: 'This services requires a password.' }) // if (!params.email) return res.status(500).send({ auth: false, status: 'EMAIL_MISSING', error_message: 'This services requires an email.' }) // Database and user validation. let userDb = await db.user.findOne({ where: { username: params.username } }) // User exists validation. if (userDb) return res.status(500).send({ auth: false, status: 'USER_ALREADY_EXISTS', error_message: 'The provided username already exists.' }) // Password requirements validation. if (!validatePassword(params.password)) return res.status(400).send({ auth: false, status: 'PASSWORD_REQUIREMENTS', error_message: 'The password requirements are not fullfilled.' }) // Email validation. // if (!validateEmail(params.email)) return res.status(500).send({ auth: false, status: 'EMAIL_INVALID', error_message: 'The provided email is invalid.' }) var userPassword = Buffer.from(params.password) // Register user const hash = await pwd.hash(userPassword) // if (err) return res.status(500).send({ auth: false, status: 'PASSWORD_HASH_ERROR', error_message: 'Hashing the password failed.' }) // Saving the non improved hash and creating the user in the db. const newUser = await db.user.create({ username: params.username, password: hash, email: params.email, name: params.name }) // TODO: Username could also be used because those are unique as well. var userId = newUser.id // Verify & improving the hash. await verifyHash(res, userPassword, hash, userId, () => {}) return res.status(200).send({ auth: true, status: 'VALID' }) } // Logout method for the frontend. Deleting the cookies by overwriting them. function logout (req, res) { // End session properly. res.clearCookie('jwt_hp') res.clearCookie('jwt_s') return res.status(200).send() // TODO: Implement.. blacklisting for jwt's and destroy the cookies.. // Maybe use express-jwt and use the rewoke function. } function changePassword (req, res) { // Check if the new password is different. if (req.body.passwordCurrent === req.body.passwordNew) return res.status(500).send({ auth: false, status: 'PASSWORD_ERROR', error_message: 'The provided password must be different than the old password.' }) // 1. Get the user and verify it's existence. db.user.findOne({ where: { id: req.params.id } }).then(user => { if (user) { const pwCurrent = Buffer.from(req.body.passwordCurrent) const pwNew = Buffer.from(req.body.passwordNew) // 2. Verify the current hast with the provided current password. verifyHash(res, pwCurrent, Buffer.from(user.password), user.id, () => { // 3. Check if the new provided password fullfills the requirements if (validatePassword(req.body.passwordNew)) { // 4. Calculate the new password hash. pwd.hash(pwNew, (err, hash) => { if (err) return res.status(500).send({ auth: false, status: 'PASSWORD_HASH_ERROR', error_message: 'Hashing the password failed.' }) // 5. Write the hash in the db user.update({ password: hash }).then(() => { // 6. Verify & improving the hash. verifyHash(res, pwNew, hash, user.id, () => { res.status(200).send({ auth: true, status: 'VALID' }) }) }) }) } else res.send({ status: 'PASSWORD_REQUIREMENTS', error_message: 'The provided password doesn\'t fullfill the requirements' }) }) } else res.send({ status: 'INVALID_USER', error_message: 'There is no user with the provided id.' }) }) } // Middleware function. // Verifies the token given in the request either by the authorization header or jwt cookies. function verifyToken (req, res, next) { var token = '' // Check for the token in the authorization header or in the cookies. Else return with auth: false. if (req.headers['authorization']) { var authorization = req.headers['authorization'] // Authorization: Bearer // Split the bearer token. const bearer = authorization.split(' ') token = bearer[1] } else if (req.cookies.jwt_hp && req.cookies.jwt_s) { token = req.cookies.jwt_hp + '.' + req.cookies.jwt_s } else { if (res) return res.status(401).send({ auth: false, status: 'TOKEN_MISSING', error_message: 'This service requires a token.' }) else return next(new Error('TOKEN_MISSING')) } // Verify the token with the secret. jwt.verify(token, config.secret, err => { if (err) { if (res) return res.status(401).send({ auth: false, status: 'TOKEN_INVALID', error_message: 'The provided token is invalid.' }) else return next(new Error('TOKEN_INVALID')) } req.token = token const decodedToken = jwt.decode(token, { complete: true }) req.user = { id: decodedToken.payload.user.id } // Check weather the user exists. db.user.findOne({ where: { id: req.user.id } }).then(user => { if (user) next() else { if (res) return res.status(401).send({ auth: false, status: 'TOKEN_INVALID', error_message: 'The token is from an invalid userid.' }) else return next(new Error('TOKEN_INVALID')) } }) }) } // Function for validating the e-mail. function validateEmail (email) { // TODO: Remove if email is not optional if (email === '') return true // Removed escape before [ because eslint told me so. var re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ return re.test(String(email).toLowerCase()) } // ################################################ // ############## Helper function ################# // ################################################ // The function for verifying a user. Callback only gets called if the user gets verified. function verifyUser (res, username, password, callback) { if (!username) return res.status(401).send({ auth: false, status: 'USER_MISSING', error_message: 'This service requires an username.' }) if (!password) return res.status(401).send({ auth: false, status: 'PASSWORD_MISSING', error_message: 'This services requires a password.' }) db.user.findOne({ where: { username: username } }).then(userDb => { if (!userDb) { return res.status(401).send({ auth: false, status: 'USER_NOTFOUND', error_message: 'User does not exist.' }) } var user = {} user.id = userDb.id var userPassword = Buffer.from(password) var hash = Buffer.from(userDb.password) // Verify & improving the hash. verifyHash(res, userPassword, hash, user.id, () => { jwt.sign({ user }, config.secret, { expiresIn: '12h' }, (err, token) => { if (err) return res.status(401).send({ auth: false, status: 'JWT_ERROR', error_message: 'Jwt sign failed.' }) return callback(token) }) }) }) } // The verify hash function from the secure-passwords with error handling. function verifyHash (res, password, hash, userId, callback) { // Check if the hash in the database fullfills the requirements needed for pwd.verify. // Hash will be a Buffer of length SecurePassword.HASH_BYTES. if (hash.length !== securePassword.HASH_BYTES) return res.status(401).send({ auth: false, status: 'DATABASE_HASH_INVALID', error_message: 'The hash in the database is corrupted.' }) // Password must be a Buffer of length SecurePassword.PASSWORD_BYTES_MIN - SecurePassword.PASSWORD_BYTES_MAX. if (password.length < securePassword.PASSWORD_BYTES_MIN || password.length > securePassword.PASSWORD_BYTES_MAX) return res.status(401).send({ auth: false, status: 'PASSWORD_INVALID', error_message: 'The provided password has an invalid length.' }) // Verification of the password. Rehash if needed. pwd.verify(password, hash, function (err, result) { if (err) return res.status(401).send({ auth: false, status: 'PASSWORD_VERIFY_ERROR', error_message: 'Verifying the password failed.' }) // Check the state of the verification. if (result === securePassword.INVALID_UNRECOGNIZED_HASH) return res.status(401).send({ auth: false, status: 'INVALID_UNRECOGNIZED_HASH', error_message: 'This hash was not made with secure-password. Attempt legacy algorithm.' }) if (result === securePassword.INVALID) return res.status(401).send({ auth: false, status: 'PASSWORD_INVALID', error_message: 'The provided password is invalid.' }) if (result === securePassword.VALID) callback() if (result === securePassword.VALID_NEEDS_REHASH) { pwd.hash(password, function (err, improvedHash) { if (err) throw err // Update the improved hash in the db. db.user.findOne({ where: { id: userId } }).then(user => { user.updateAttributes({ password: improvedHash }) return callback() }) }) } }) } // Function for validating the password. Password requirements are implemented here. function validatePassword (password) { // TODO: implement pw requirements like in the frontend. (SetupPage) if (password.length < 8) return false return true } // Function for validating the username. Username requirements are implemented here. function validateUsername (username) { // Disallow whitespaces return !/\s/.test(username) }