@@ -9,6 +9,8 @@ const Account = db.Account;
9
9
10
10
module . exports = {
11
11
authenticate,
12
+ refreshToken,
13
+ revokeToken,
12
14
register,
13
15
verifyEmail,
14
16
forgotPassword,
@@ -21,13 +23,58 @@ module.exports = {
21
23
delete : _delete
22
24
} ;
23
25
24
- async function authenticate ( { email, password } ) {
25
- const account = await Account . findOne ( { email, isVerified : true } ) ;
26
- if ( account && bcrypt . compareSync ( password , account . passwordHash ) ) {
27
- // return basic details and auth token
28
- const token = jwt . sign ( { sub : account . id , id : account . id } , config . secret ) ;
29
- return { ...basicDetails ( account ) , token } ;
26
+ async function authenticate ( { email, password, ipAddress } ) {
27
+ const account = await Account . findOne ( { email } ) ;
28
+
29
+ if ( ! account || ! account . isVerified || ! bcrypt . compareSync ( password , account . passwordHash ) ) {
30
+ throw 'Email or password is incorrect' ;
30
31
}
32
+
33
+ // authentication successful so generate jwt and refresh tokens
34
+ const jwtToken = generateJwtToken ( account ) ;
35
+ const refreshToken = generateRefreshToken ( ipAddress ) ;
36
+
37
+ // save refresh token
38
+ account . refreshTokens . push ( refreshToken ) ;
39
+ account . save ( ) ;
40
+
41
+ // return basic details and tokens
42
+ return {
43
+ ...basicDetails ( account ) ,
44
+ jwtToken,
45
+ refreshToken : refreshToken . token
46
+ } ;
47
+ }
48
+
49
+ async function refreshToken ( { token, ipAddress } ) {
50
+ const { account, refreshToken } = await getRefreshToken ( token ) ;
51
+
52
+ // replace old refresh token with a new one and save
53
+ const newRefreshToken = generateRefreshToken ( ipAddress ) ;
54
+ refreshToken . revoked = Date . now ( ) ;
55
+ refreshToken . revokedByIp = ipAddress ;
56
+ refreshToken . replacedByToken = newRefreshToken . token ;
57
+ account . refreshTokens . push ( newRefreshToken ) ;
58
+ account . save ( ) ;
59
+
60
+ // generate new jwt
61
+ const jwtToken = generateJwtToken ( account ) ;
62
+
63
+ // return basic details and tokens
64
+ return {
65
+ ...basicDetails ( account ) ,
66
+ jwtToken,
67
+ refreshToken : newRefreshToken . token
68
+ } ;
69
+ }
70
+
71
+ async function revokeToken ( { token, ipAddress } ) {
72
+ const { account, refreshToken } = await getRefreshToken ( token ) ;
73
+
74
+ // revoke token and save
75
+ refreshToken . revoked = Date . now ( ) ;
76
+ refreshToken . revokedByIp = ipAddress ;
77
+ account . save ( )
31
78
}
32
79
33
80
async function register ( params , origin ) {
@@ -43,8 +90,7 @@ async function register(params, origin) {
43
90
// first registered account is an admin
44
91
const isFirstAccount = ( await Account . countDocuments ( { } ) ) === 0 ;
45
92
account . role = isFirstAccount ? Role . Admin : Role . User ;
46
- account . verificationToken = generateToken ( ) ;
47
- account . isVerified = false ;
93
+ account . verificationToken = randomTokenString ( ) ;
48
94
49
95
// hash password
50
96
if ( params . password ) {
@@ -63,7 +109,8 @@ async function verifyEmail({ token }) {
63
109
64
110
if ( ! account ) throw 'Verification failed' ;
65
111
66
- account . isVerified = true ;
112
+ account . verified = Date . now ( ) ;
113
+ account . verificationToken = undefined ;
67
114
await account . save ( ) ;
68
115
}
69
116
@@ -74,36 +121,37 @@ async function forgotPassword({ email }, origin) {
74
121
if ( ! account ) return ;
75
122
76
123
// create reset token that expires after 24 hours
77
- account . resetToken = generateToken ( ) ;
78
- account . resetTokenExpiry = new Date ( Date . now ( ) + 24 * 60 * 60 * 1000 ) . toISOString ( ) ;
124
+ account . resetToken = {
125
+ token : randomTokenString ( ) ,
126
+ expires : new Date ( Date . now ( ) + 24 * 60 * 60 * 1000 )
127
+ } ;
79
128
account . save ( ) ;
80
129
81
130
// send email
82
131
sendPasswordResetEmail ( account , origin ) ;
83
132
}
84
133
85
134
async function validateResetToken ( { token } ) {
86
- const account = await Account . findOne ( {
87
- resetToken : token ,
88
- resetTokenExpiry : { $gt : new Date ( ) }
135
+ const account = await Account . findOne ( {
136
+ ' resetToken.token' : token ,
137
+ 'resetToken.expires' : { $gt : Date . now ( ) }
89
138
} ) ;
90
139
91
140
if ( ! account ) throw 'Invalid token' ;
92
141
}
93
142
94
143
async function resetPassword ( { token, password } ) {
95
144
const account = await Account . findOne ( {
96
- resetToken : token ,
97
- resetTokenExpiry : { $gt : new Date ( ) }
145
+ ' resetToken.token' : token ,
146
+ 'resetToken.expires' : { $gt : Date . now ( ) }
98
147
} ) ;
99
148
100
149
if ( ! account ) throw 'Invalid token' ;
101
150
102
151
// update password and remove reset token
103
152
account . passwordHash = hash ( password ) ;
104
- account . isVerified = true ;
153
+ account . passwordReset = Date . now ( ) ;
105
154
account . resetToken = undefined ;
106
- account . resetTokenExpiry = undefined ;
107
155
await account . save ( ) ;
108
156
}
109
157
@@ -124,7 +172,7 @@ async function create(params) {
124
172
}
125
173
126
174
const account = new Account ( params ) ;
127
- account . isVerified = true ;
175
+ account . verified = Date . now ( ) ;
128
176
129
177
// hash password
130
178
if ( params . password ) {
@@ -152,7 +200,7 @@ async function update(id, params) {
152
200
153
201
// copy params to account and save
154
202
Object . assign ( account , params ) ;
155
- account . dateUpdated = Date . now ( ) ;
203
+ account . updated = Date . now ( ) ;
156
204
await account . save ( ) ;
157
205
158
206
return basicDetails ( account ) ;
@@ -172,17 +220,39 @@ async function getAccount(id) {
172
220
return account ;
173
221
}
174
222
223
+ async function getRefreshToken ( token ) {
224
+ const account = await Account . findOne ( ) . elemMatch ( 'refreshTokens' , { token } ) ;
225
+ if ( ! account ) throw 'Invalid token' ;
226
+ const refreshToken = account . refreshTokens . find ( x => x . token === token ) ;
227
+ if ( ! refreshToken . isActive ) throw 'Invalid token' ;
228
+ return { account, refreshToken } ;
229
+ }
230
+
175
231
function hash ( password ) {
176
232
return bcrypt . hashSync ( password , 10 ) ;
177
233
}
178
234
179
- function generateToken ( ) {
235
+ function generateJwtToken ( account ) {
236
+ // create a jwt token containing the account id that expires in 15 minutes
237
+ return jwt . sign ( { sub : account . id , id : account . id } , config . secret , { expiresIn : '15m' } ) ;
238
+ }
239
+
240
+ function generateRefreshToken ( ipAddress ) {
241
+ // create a refresh token that expires in 7 days
242
+ return {
243
+ token : randomTokenString ( ) ,
244
+ expires : new Date ( Date . now ( ) + 7 * 24 * 60 * 60 * 1000 ) ,
245
+ createdByIp : ipAddress
246
+ } ;
247
+ }
248
+
249
+ function randomTokenString ( ) {
180
250
return crypto . randomBytes ( 40 ) . toString ( 'hex' ) ;
181
251
}
182
252
183
253
function basicDetails ( account ) {
184
- const { id, title, firstName, lastName, email, role, dateCreated , dateUpdated } = account ;
185
- return { id, title, firstName, lastName, email, role, dateCreated , dateUpdated } ;
254
+ const { id, title, firstName, lastName, email, role, created , updated , isVerified } = account ;
255
+ return { id, title, firstName, lastName, email, role, created , updated , isVerified } ;
186
256
}
187
257
188
258
function sendVerificationEmail ( account , origin ) {
@@ -225,12 +295,12 @@ function sendAlreadyRegisteredEmail(email, origin) {
225
295
function sendPasswordResetEmail ( account , origin ) {
226
296
let message ;
227
297
if ( origin ) {
228
- const resetUrl = `${ origin } /account/reset-password?token=${ account . resetToken } ` ;
298
+ const resetUrl = `${ origin } /account/reset-password?token=${ account . resetToken . token } ` ;
229
299
message = `<p>Please click the below link to reset your password, the link will be valid for 1 day:</p>
230
300
<p><a href="${ resetUrl } ">${ resetUrl } </a></p>` ;
231
301
} else {
232
302
message = `<p>Please use the below token to reset your password with the <code>/account/reset-password</code> api route:</p>
233
- <p><code>${ account . resetToken } </code></p>` ;
303
+ <p><code>${ account . resetToken . token } </code></p>` ;
234
304
}
235
305
236
306
sendEmail ( {
0 commit comments