summaryrefslogtreecommitdiffstats
path: root/server/lib/authentication.js
blob: 87fb02e8238fcd0c950a9662cfa1263f049e9168 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
/* 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)
}