summaryrefslogtreecommitdiffstats
path: root/server/lib/authentication.js
blob: b8ee506a065c4da2b2de77d60445faca65af5854 (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
/* 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', 'database')).connectionPool;
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.
    // SEQ//db.query('SELECT * FROM users WHERE username = ?', [params.username], function (err, rows) {
    db.user.findOne({ where: { username: params.username } }).then(userDb => {
      // SEQ//if (err) return res.status(500).send({ auth: false, status: 'DATABASE_ERROR', error_message: 'SQL query failed.' });
      // User exists validation.
      // SEQ//if (rows.length) return res.status(500).send({ auth: false, status: 'USER_ALREADY_EXISTS', error_message: 'The provided username already exists.' });
      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.
        // SEQ//var att =  [params.username, hash, params.email, params.name];
        // SEQ//db.query('INSERT INTO users (username, password, email, name) VALUES (?)', [att], function (err, result) {
        db.user.create({ username: params.username, password: hash, email: params.email, name: params.name }).then((userDb) => {
          // SEQ//if (err) return res.status(500).send({ auth: false, status: 'DATABASE_INSERT_ERROR', error_message: 'Inserting the user in the database failed.' });
          // TODO: Username could also be used because those are unique as well.
          // SEQ//var userId = result.insertId;
          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 {
      return res.status(403).send({ auth: false, status: 'TOKEN_MISSING', error_message: 'This service requires a token.' })
    }
    // Verify the token with the secret.
    jwt.verify(token, config.secret, function (err) {
      if (err) return res.status(500).send({ auth: false, status: 'TOKEN_INVALID', error_message: 'The provided token is invalid.' })
      req.token = token
      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.' })

  // SEQ//db.query('SELECT * FROM users WHERE username = ?', [username], function (err, rows) {
  db.user.findOne({ where: { username: username } }).then(userDb => {
    // SEQ//if (err) return res.status(500).send({ auth: false, status: 'DATABASE_ERROR', error_message: 'Database connection failed.' });
    // SEQ//if (rows.length != 1) {
    // SEQ//    return res.status(404).send({ auth: false, status: 'USER_NOTFOUND', error_message: 'User does not exist.' });
    // SEQ//}
    if (!userDb) {
      return res.status(404).send({ auth: false, status: 'USER_NOTFOUND', error_message: 'User does not exist.' })
    }
    var user = {}
    // SEQ//user.id = rows[0].id;
    user.id = userDb.id
    // user.username = rows[0].username;
    // user.email = rows[0].email;
    var userPassword = Buffer.from(password)
    // SEQ//var hash = Buffer.from(rows[0].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.
        // SEQ//db.query('UPDATE users SET password=? WHERE id=?', [improvedHash, userId], function (err, result) {
        db.user.findOne({ where: { id: userId } }).then(user => {
          // SEQ//if (err) throw err;
          user.updateAttributes({
            password: improvedHash
          })
          return callback()
        })
      })
    }
  })
}

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