summaryrefslogblamecommitdiffstats
path: root/server/lib/authentication.js
blob: 76e8b60056f4e4a8b62ac16760e85d12dbcaaef1 (plain) (tree)
1
2
3
4
5
6
7
8
                     


                                                                     


                                                         
 

















                                                                                                                                       
 



































                                                                                                                                                                                         


          







































                                                                                                                                     
     







                                                                                                                                       
      
    
 
 



                                                   
                                                                                          



                                                                                                                                              
                                                                     



                                                                                                                  
                       
                                            



                                                              
                                                                               




                                                                                                                     


                                                                          



















                                                                                                                                                                                                                                                         
                                                                 







                                  


                                      
                                


                                                                                                                                                                  


                                                                                    
                                      

                                                                      

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

// Authentifivation method for the frontend using secure httpOnly cookies. (POST)
function loginCookie (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)
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.
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(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.
  db.user.findOne({ where: { username: params.username } }).then(userDb => {
    // 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(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.
      db.user.create({ username: params.username, password: hash, email: params.email, name: params.name }).then((userDb) => {
        // TODO: Username could also be used because those are unique as well.
        var userId = userDb.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.
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) {
  // TODO: IMPLEMENT
}

// 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 <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 {
    if (res) return res.status(403).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, function (err) {
    if (err) {
      if (res) return res.status(500).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 return res.status(500).send({ auth: false, status: 'TOKEN_INVALID', error_message: 'The token is from an invalid userid.' })
    })
  })
}

// ################################################
// ############## 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(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.' })

  db.user.findOne({ where: { username: username } }).then(userDb => {
    if (!userDb) {
      return res.status(404).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, 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.
        db.user.findOne({ where: { id: userId } }).then(user => {
          user.updateAttributes({
            password: improvedHash
          })
          return callback()
        })
      })
    }
  })
}

// Function for validating the e-mail.
function validateEmail (email) {
// 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())
}

// 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
}