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

// Method for creating a new user.
async function signup (user) {
  // TODO: Implement some security stuff. Not every user who call this request should be able to sign up.
  if (!user.username) return { code: 400, error: 'USER_MISSING', message: 'This service requires an username.' }
  if (!validateUsername(user.username)) return { code: 400, error: 'INVALID_USERNAME', message: 'Username does not fullfill the requirements. (No whitespaces)' }
  if (!user.password) return { code: 400, error: 'PASSWORD_MISSING', 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: user.username } })

  // User exists validation.
  if (userDb) return { code: 500, error: 'USER_ALREADY_EXISTS', message: 'The provided username already exists.' }
  // Password requirements validation.
  if (!validatePassword(user.password)) return { code: 400, error: 'PASSWORD_REQUIREMENTS', message: 'The password requirements are not fullfilled.' }
  // Email validation.
  // if (!validateEmail(params.email)) return { code: 400, error: 'EMAIL_INVALID', message: 'The provided email is invalid.' }
  var userPassword = Buffer.from(user.password)

  // Register user
  try {
    var hash = await pwd.hash(userPassword)
  } catch (error) {
    return { code: 500, error: 'PASSWORD_HASH_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: user.username, password: hash, email: user.email, name: user.name })
  if (!newUser) return { code: 500, error: 'USER_CREATE_ERROR', message: 'User could not be created.' }

  // TODO: Username could also be used because those are unique as well.
  var userId = newUser.id

  // Verify & improving the hash.
  var result = await verifyHash(userPassword, hash, userId)
  if (result.error) return result
  result.id = userId
  return result
}

async function changePassword (id, password, passwordCurrent = '') {
  // 1. Get the user and verify it's existence.
  let user = await db.user.findOne({ where: { id: id } })
  if (!user) return { code: 404, error: 'USER_NOTFOUND', message: 'There is no user with the provided id.' }

  const pwNew = Buffer.from(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 (passwordCurrent) {
    // Verify the current hast with the provided current password.
    const pwCurrent = Buffer.from(passwordCurrent)
    var result = await verifyHash(pwCurrent, Buffer.from(user.password), user.id)
    if (result.error) return result
  }

  // 3. Check if the new provided password fullfills the requirements
  if (!validatePassword(password)) return { code: 400, error: 'PASSWORD_REQUIREMENTS', 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 { code: 500, error: 'PASSWORD_HASH_ERROR', message: 'Hashing the password failed.' }
  }

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

  // 6. Verify & improving the hash.
  const res = await verifyHash(pwNew, hash, user.id)
  return res
}

// 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({ error: 'TOKEN_MISSING', 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({ error: 'TOKEN_INVALID', 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({ error: 'TOKEN_INVALID', message: 'The token is from an invalid userid.' })
        else return next(new Error('TOKEN_INVALID'))
      }
    })
  })
}

// The function for verifying a user.
async function verifyUser (username, password) {
  if (!username) return { code: 400, error: 'USER_MISSING', message: 'This service requires an username.' }
  if (!password) return { code: 400, error: 'PASSWORD_MISSING', message: 'This services requires a password.' }

  const userDb = await db.user.findOne({ where: { username: username } })
  if (!userDb) return { code: 404, error: 'USER_NOTFOUND', 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(userPassword, hash, user.id)
  if (result.error) return result

  try {
    var token = await jwt.sign({ user }, config.secret, { expiresIn: '12h' })
  } catch (error) {
    return { code: 500, error: 'JWT_ERROR', message: 'Jwt sign failed.' }
  }

  result.token = token
  return result
}

// 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 verify hash function from the secure-passwords with error handling.
async function verifyHash (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 { code: 500, error: 'DATABASE_HASH_INVALID', 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 { code: 401, error: 'PASSWORD_INVALID', message: 'The provided password is invalid.' }

  // Verification of the password. Rehash if needed.
  try {
    var result = await pwd.verify(password, hash)
  } catch (error) {
    return { code: 500, error: 'PASSWORD_VERIFY_ERROR', message: 'Verifying the password failed.' }
  }

  // Check the state of the verification.
  if (result === securePassword.INVALID_UNRECOGNIZED_HASH) return { code: 500, error: 'INVALID_UNRECOGNIZED_HASH', message: 'This hash was not made with secure-password. Attempt legacy algorithm.' }
  if (result === securePassword.INVALID) return { code: 401, error: 'PASSWORD_INVALID', message: 'The provided password is invalid.' }
  if (result === securePassword.VALID) return { code: 200 }
  if (result === securePassword.VALID_NEEDS_REHASH) {
    try {
      var improvedHash = await pwd.hash(password)
    } catch (error) {
      return { code: 500, error: 'PASSWORD_REHASH_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 { code: 200 }
  }
}

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