From aa4e552a03657a63922f5cd085431257c183f458 Mon Sep 17 00:00:00 2001 From: Jannik Schönartz Date: Mon, 2 Jul 2018 19:52:25 +0000 Subject: [server] Initial commit to add the node server stuff. --- server/lib/authentication.js | 203 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 server/lib/authentication.js (limited to 'server/lib/authentication.js') diff --git a/server/lib/authentication.js b/server/lib/authentication.js new file mode 100644 index 0000000..3a4dccd --- /dev/null +++ b/server/lib/authentication.js @@ -0,0 +1,203 @@ +/* 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', 'database')).connectionPool; +var db = require(path.join(__appdir, 'lib', 'sequelize')); +var securePassword = require('secure-password'); +var pwd = securePassword(); + +module.exports = { + // Authentifivation method for the frontend using secure httpOnly cookies. (POST) + login: function(req, res) { + var params = req.body; + + verifyUser(res, params.username, params.password, function(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) + auth: function(req, res) { + var query = req.query; + + verifyUser(res, query.username, query.password, function(token) { + return res.status(200).send({ auth: true, token }); + }); + }, + + signup: function(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(500).send({ auth: false, status: 'USER_MISSING', error_message: 'This service requires an username.' }); + if (!params.password) return res.status(500).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. + //SEQ//db.query('SELECT * FROM users WHERE username = ?', [params.username], function(err, rows) { + db.user.findOne({ where: { username: params.username } }).then(user_db => { + //SEQ//if (err) return res.status(500).send({ auth: false, status: 'DATABASE_ERROR', error_message: 'SQL query failed.' }); + + // User exists validation. + //SEQ//if (rows.length) return res.status(500).send({ auth: false, status: 'USER_ALREADY_EXISTS', error_message: 'The provided username already exists.' }); + if (user_db) 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(500).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 + pwd.hash(userPassword, function(err, hash) { + 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. + //SEQ//var att = [params.username, hash, params.email, params.name]; + //SEQ//db.query('INSERT INTO users (username, password, email, name) VALUES (?)', [att], function(err, result) { + db.user.create({ username: params.username, password: hash, email: params.email, name: params.name }).then((user_db) => { + //SEQ//if (err) return res.status(500).send({ auth: false, status: 'DATABASE_INSERT_ERROR', error_message: 'Inserting the user in the database failed.' }); + // TODO: Username could also be used because those are unique as well. + //SEQ//var userId = result.insertId; + var userId = user_db.id; + + // Verify & improving the hash. + verifyHash(res, userPassword, hash, userId, function() { + return res.status(200).send({ auth: true, status: 'VALID'}); + }); + }); + }); + }); + }, + + // Logout method for the frontend. Deleting the cookies by overwriting them. + logout: function(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. + }, + + changePassword: function(req, res) { + // TODO: IMPLEMENT + }, + + verifyToken: function(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 { + return res.status(403).send({ auth: false, status: 'TOKEN_MISSING', error_message: 'This service requires a token.' }); + } + // Verify the token with the secret. + jwt.verify(token, config.secret, function(err) { + if (err) return res.status(500).send({ auth: false, status: 'TOKEN_INVALID', error_message: 'The provided token is invalid.' }); + req.token = token; + next(); + }); + } +}; + +// 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(500).send({ auth: false, status: 'USER_MISSING', error_message: 'This service requires an username.' }); + if (!password) return res.status(500).send({ auth: false, status: 'PASSWORD_MISSING', error_message: 'This services requires a password.' }); + + //SEQ//db.query('SELECT * FROM users WHERE username = ?', [username], function(err, rows) { + db.user.findOne({ where: { username: username } }).then(user_db => { + //SEQ//if (err) return res.status(500).send({ auth: false, status: 'DATABASE_ERROR', error_message: 'Database connection failed.' }); + //SEQ//if (rows.length != 1) { + //SEQ// return res.status(404).send({ auth: false, status: 'USER_NOTFOUND', error_message: 'User does not exist.' }); + //SEQ//} + + if (!user_db) { + return res.status(404).send({ auth: false, status: 'USER_NOTFOUND', error_message: 'User does not exist.' }); + } + var user = {}; + //SEQ//user.id = rows[0].id; + user.id = user_db.id; + //user.username = rows[0].username; + //user.email = rows[0].email; + var userPassword = Buffer.from(password); + //SEQ//var hash = Buffer.from(rows[0].password); + var hash = Buffer.from(user_db.password); + + // Verify & improving the hash. + verifyHash(res, userPassword, hash, user.id, function() { + jwt.sign({user}, config.secret, { expiresIn: '12h' }, (err, token) => { + if (err) return res.status(500).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(500).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(500).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(500).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(500).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(500).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. + //SEQ//db.query('UPDATE users SET password=? WHERE id=?', [improvedHash, userId], function(err, result) { + db.user.findOne({ where: { id: userId } }).then(user_db => { + //SEQ//if (err) throw err; + user_db.updateAttributes({ + password: improvedHash + }); + return callback(); + }); + }); + } + }); +} + +// Function for validating the e-mail. +function validateEmail(email) { + 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()); +} + +// Function for validating the password. Password requirements are implemented here. +function validatePassword(password) { + // TODO: implement pw requirements like in the frontend. + return true; +} \ No newline at end of file -- cgit v1.2.3-55-g7522