This repository was archived by the owner on Apr 3, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.js
More file actions
175 lines (159 loc) · 6.73 KB
/
index.js
File metadata and controls
175 lines (159 loc) · 6.73 KB
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
var _ = require('lodash');
var moment = require('moment');
var uuid = require('uuid');
var bcrypt = require('bcryptjs');
var debug = require('debug')('gr8:security');
var verbose = require('debug')('gr8:security:verbose');
/**
Authentication functions supported are:
1. login - performs authentication
2. generateResetPasswordToken - generates a token that can be used to obtain a new password
3. resetPassword - saves a new password
@param {object} daoFactory data access object factory (see ./db/database)
@param {object} options object for controlling logins, encryption strength
@example {
max_bad_logins: 10,
salt_rounds: 15,
security_logger: <winston logger>
}
@version 2.0.2
*/
class AuthService {
constructor(daoFactory, opts) {
this.userDao = daoFactory.User();
this.userRoleDao = daoFactory.UserRole();
this.max_bad_logins = opts ? opts.max_bad_logins || 10 : 10;
this.salt_rounds = opts ? opts.salt_rounds || 5 : 5;
}
/**
* Logs in a user and returns the user if successful.
* The password is compared against the hash stored in the database.
* An error message is thrown carrying a message that is generally suitable for
* display to the end-user (does not divulge PII).
*
* @param {string} username
* @param {string} plainTextPassword such as that provided on a web form
* @returns {Promise<object>} the user object
*/
async login(username, plainTextPassword) {
if (_.isNil(username) || _.isNil(plainTextPassword)) {
debug('Failed login. Both a username and a password are required.');
throw new Error('Invalid credentials.');
}
verbose('Beginning login process...');
let theUser = await this.userDao.one({ username: username });
if (_.isEmpty(theUser)) {
throw new UserNotFoundError('Invalid credentials.');
}
verbose('Attempting password comparison...');
let isMatch = await bcrypt.compare(plainTextPassword, theUser.password);
if (isMatch) {
verbose('Comparison succeeded...');
if (theUser.must_reset_password) {
verbose('User must reset password.');
throw new PasswordExpiredError('Your password has expired and must be reset.');
}
//Comparison success, check whether user is valid
if (theUser.status === 'active') {
verbose('User resolved successfully.');
theUser.bad_login_attempts = 0;
theUser.last_login = moment().utc().format('YYYY-MM-DDTHH:mm:ss');
theUser.login_count += 1;
//OK, update the user information, load roles and resolve.
theUser = await this.userDao.update(theUser);
let roles = await this.userRoleDao.find({ user_id: theUser.id });
//Set the roles on the user.
theUser.roles = roles.map(r => { return r.role; });
//
// Success.
//
return theUser;
} else {
verbose('User has an inactive status.');
//Any status other than active is considered suspended.
throw new UserSuspendedError('Your account is no longer active. Please contact your administrator.');
}
} else {
verbose('Password does not match.');
//Increment bad login attempts and/or flag password reset.
theUser.bad_login_attempts++;
if (theUser.bad_login_attempts >= this.max_bad_logins) {
//suspend the user.
theUser.status = 'suspended';
}
verbose('Updating invalid login statistics.');
await this.userDao.update(theUser);
throw new InvalidPasswordError('Invalid credentials.');
}
}
/**
* Validates a user exists and then generates a reset password token for that user.
* The user (with the reset_password_token) is then returned via Promise.
* @param {string} uidString either username or email address.
*/
async generateResetPasswordToken(uidString) {
let theUser = await this.userDao.one({ email: uidString });
if (_.isEmpty(theUser)) {
theUser = await this.userDao.one({ username: uidString });
}
if (_.isEmpty(theUser)) {
verbose('Requested user was not found.');
throw new UserNotFoundError('Unable to reset password.');
}
//Generate a token.
theUser.reset_password_token = uuid();
theUser.reset_password_token_expires = moment().add(1, 'day').utc().format('YYYY-MM-DDTHH:mm:ss');
verbose('Updating the user with the password reset token which expires at: ' + theUser.reset_password_token_expires);
theUser = await this.userDao.update(theUser);
return theUser; //will contain the reset_password_token for use.
}
/**
Completes the password-reset process. The new password is hashed and stored, and all
reset password-related login counters and flags are set to their nominal values.
@param {string} token the reset-password token issued when the user initially
requested to reset their password. (Typically this is sent in a validation email as part
of the reset-password link).
@param {string} newPlainTextPassword the new user-provided plain-text password
@returns {Promise<object>} the user object for whom the password was reset. If an
error occurs, the message returned is NOT suitable for display to the end-user.
*/
async resetPassword(token, newPlainTextPassword) {
verbose('Resetting password...')
//Fetch the user record for the token
let user = await this.userDao.one({ reset_password_token: token });
if (_.isEmpty(user)) {
throw new InvalidResetTokenError();
}
//Validate the token.
var tokenExpiration = moment(user.reset_password_token_expires);
if (tokenExpiration.isBefore(moment())) {
throw new ExpiredResetTokenError();
} else {
//Salt and hash the password
verbose(' Hashing password.');
let hash = await bcrypt.hash(newPlainTextPassword, this.salt_rounds);
verbose(' Updating user.')
//Reset counters, clear the token
user.password = hash;
user.must_reset_password = false;
user.reset_password_token = null;
user.reset_password_token_expires = null;
user.bad_login_attempts = 0;
user = await this.userDao.update(user);
verbose(' ...password reset complete.')
}
}
}//AuthService class
exports.AuthService = AuthService;
class UserNotFoundError extends Error { }
class UserSuspendedError extends Error { }
class PasswordExpiredError extends Error { }
class InvalidPasswordError extends Error { }
class InvalidResetTokenError extends Error { }
class ExpiredResetTokenError extends Error { }
exports.UserNotFoundError = UserNotFoundError;
exports.UserSuspendedError = UserSuspendedError;
exports.PasswordExpiredError = PasswordExpiredError;
exports.InvalidPasswordError = InvalidPasswordError;
exports.InvalidResetTokenError = InvalidResetTokenError;
exports.ExpiredResetTokenError = ExpiredResetTokenError;