summaryrefslogblamecommitdiffstats
path: root/server/lib/authentication.js
blob: 3a4dccdc4eeeea1f9bdbeb56820460e755ad1c32 (plain) (tree)










































































































































































































                                                                                                                                                                                                                                                            
/* 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 <token>
            // 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;
}