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


                                                                     


                                                         

                  

































                                                                                                                                                       
                                                                              
                                










                                                                                                                                                                                           
                                                                                                                                
                                                                                



































                                                                                                       

                                                                                                                                     
     

                                                     



                                                                                                                                       
                       


                                                                



            

                                                                                          



                                                                                                                                              
                                                                     



                                                                                                                  
                       
                                            



                                                              
                                                                               




                                                                                                                     


                                                                          



















                                                                                                                                                                                                                                                         
                                                                 







                                  


                                      
                                


                                                                                                                                                                  


                                                                                    



                                                          
/* 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 = {
  // 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.
    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.
  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 {
      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 }

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

  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.
  return true
}