summaryrefslogblamecommitdiffstats
path: root/server/lib/authentication.js
blob: dcbe88005e076ef666810e830e00ca4f4d729c80 (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 = { loginCookie, loginToken, logout, verifyToken, signup, changePassword, validateEmail, validateUsername }

// Authentifivation method for the frontend using secure httpOnly cookies. (POST)
async function loginCookie (req, res) {
  var params = req.body
  var result = await verifyUser(res, params.username, params.password)
  if (result.status !== 'SUCCESS') return res.status(401).send(result)

  // 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 = result.data.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)
async function loginToken (req, res) {
  var body = req.body

  var result = await verifyUser(res, body.username, body.password)
  if (result.status !== 'SUCCESS') return res.status(401).send(result)
  const token = result.data.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
  try {
    var hash = await pwd.hash(userPassword)
  } catch (error) {
    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.
  var result = await verifyHash(res, userPassword, hash, userId)
  if (result.status !== 'SUCCESS') return res.status(401).send(result)

  return newUser
}

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

async function changePassword (req, res) {
  // 1. Get the user and verify it's existence.
  let user = await db.user.findOne({ where: { id: req.params.id } })
  if (!user) return res.send({ status: 'INVALID_USER', error_message: 'There is no user with the provided id.' })

  const pwNew = Buffer.from(req.body.password)

  // 2. Only if the current password is set we have to check if it's valid.
  // This is because root can set passwords witout having the old ones.
  // But the authentication if you can call this function without the currentPasswords needs to be in the API.
  if (req.body.passwordCurrent) {
    // Verify the current hast with the provided current password.
    const pwCurrent = Buffer.from(req.body.passwordCurrent)
    var result = await verifyHash(res, pwCurrent, Buffer.from(user.password), user.id)
    if (result.status !== 'SUCCESS') return res.status(400).send(result)
  }

  // 3. Check if the new provided password fullfills the requirements
  if (!validatePassword(req.body.password)) return res.status(400).send({ status: 'PASSWORD_REQUIREMENTS', error_message: 'The provided password doesn\'t fullfill the requirements' })

  // 4. Calculate the new password hash.
  try {
    var hash = await pwd.hash(pwNew)
  } catch (error) {
    return res.status(500).send({ auth: false, status: 'PASSWORD_HASH_ERROR', error_message: 'Hashing the password failed.' })
  }

  // 5. Write the hash in the dbW
  await user.update({ password: hash })

  // 6. Verify & improving the hash.
  await verifyHash(res, pwNew, hash, user.id)
  res.status(200).send({ auth: true, status: 'VALID' })
}

// 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(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.
async function verifyUser (res, username, password) {
  if (!username) return { auth: false, status: 'USER_MISSING', error_message: 'This service requires an username.' }
  if (!password) return { auth: false, status: 'PASSWORD_MISSING', error_message: 'This services requires a password.' }

  const userDb = await db.user.findOne({ where: { username: username } })
  if (!userDb) {
    return { 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.
  var result = await verifyHash(res, userPassword, hash, user.id)
  if (result.status !== 'SUCCESS') return result

  try {
    var token = await jwt.sign({ user }, config.secret, { expiresIn: '12h' })
  } catch (error) {
    return { auth: false, status: 'JWT_ERROR', error_message: 'Jwt sign failed.' }
  }

  return { auth: true, status: 'SUCCESS', data: { token: token } }
}

// The verify hash function from the secure-passwords with error handling.
async function verifyHash (res, password, hash, userId) {
  // 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 { 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 { auth: false, status: 'PASSWORD_INVALID', error_message: 'The provided password has an invalid length.' }

  // Verification of the password. Rehash if needed.
  try {
    var result = await pwd.verify(password, hash)
  } catch (error) {
    return { 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 { auth: false, status: 'INVALID_UNRECOGNIZED_HASH', error_message: 'This hash was not made with secure-password. Attempt legacy algorithm.' }
  if (result === securePassword.INVALID) return { auth: false, status: 'PASSWORD_INVALID', error_message: 'The provided password is invalid.' }
  if (result === securePassword.VALID) return { status: 'SUCCESS' }
  if (result === securePassword.VALID_NEEDS_REHASH) {
    try {
      var improvedHash = await pwd.hash(password)
    } catch (error) {
      return { auth: false, status: 'PASSWORD_REHASH_ERROR', error_message: 'Rehashing the password failed.' }
    }
    // Update the improved hash in the db.
    const user = await db.user.findOne({ where: { id: userId } })
    await user.updateAttributes({
      password: improvedHash
    })
    return { status: 'SUCCESS' }
  }
}

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