6
6
* @param {Array<String> } options.options - Supported MFA methods. Must include `"SMS"` or `"TOTP"`.
7
7
* @param {Number } [options.digits=6] - The number of digits for the one-time password (OTP). Must be between 4 and 10.
8
8
* @param {Number } [options.period=30] - The validity period of the OTP in seconds. Must be greater than 10.
9
- * @param {Number } [options.emailExpiry=5*60] - The validity period of the email OTP in seconds. Must be greater than 10.
10
9
* @param {String } [options.algorithm="SHA1"] - The algorithm used for TOTP generation. Defaults to `"SHA1"`.
11
10
* @param {Function } [options.sendSMS] - A callback function for sending SMS OTPs. Required if `"SMS"` is included in `options`.
12
11
*
78
77
*/
79
78
80
79
import { TOTP , Secret } from 'otpauth' ;
81
- import crypto from 'crypto' ;
82
- import { randomString } from '../../cryptoUtils' ;
80
+ import { randomString , sha256Hash } from '../../cryptoUtils' ;
83
81
import AuthAdapter from './AuthAdapter' ;
84
82
class MFAAdapter extends AuthAdapter {
85
83
validateOptions ( opts ) {
@@ -95,7 +93,31 @@ class MFAAdapter extends AuthAdapter {
95
93
}
96
94
const digits = opts . digits || 6 ;
97
95
const period = opts . period || 30 ;
98
- const emailOTPExpiry = opts . emailOTPExpiry || 5 * 60 ; // Default to 5 minutes
96
+
97
+ // Define default periods for each method
98
+ const defaultPeriods = {
99
+ SMS : 30 , // 5 minutes for SMS
100
+ EMAIL : 30 , // 5 minutes for Email
101
+ TOTP : 30 , // 30 seconds for TOTP
102
+ } ;
103
+
104
+ if ( typeof opts . period === 'number' ) {
105
+ validOptions . forEach ( method => {
106
+ this . period [ method ] = this . period ;
107
+ } ) ;
108
+ } else if ( opts . period && typeof opts . period === 'object' ) {
109
+ Object . keys ( this . period ) . forEach ( method => {
110
+ if ( this . periods . hasOwnProperty ( method ) && typeof this . period [ method ] === 'number' ) {
111
+ this . period [ method ] = this . period [ method ] ?? defaultPeriods [ method ] ?? 30 ;
112
+ }
113
+ } ) ;
114
+ this . period = { ...opts . period } ;
115
+ } else {
116
+ validOptions . forEach ( method => {
117
+ this . period [ method ] = defaultPeriods [ method ] ?? 30 ;
118
+ } ) ;
119
+ }
120
+
99
121
if ( typeof digits !== 'number' ) {
100
122
throw 'mfa.digits must be a number' ;
101
123
}
@@ -108,11 +130,7 @@ class MFAAdapter extends AuthAdapter {
108
130
if ( period < 10 ) {
109
131
throw 'mfa.period must be greater than 10' ;
110
132
}
111
- if ( this . email ) {
112
- if ( this . emailOTPExpiry < 5 * 60 ) {
113
- throw 'mfa.emailExpiry must be greater than 5 minutes' ;
114
- }
115
- }
133
+
116
134
const sendSMS = opts . sendSMS ;
117
135
const sendEmail = opts . sendEmail ;
118
136
if ( this . email && typeof sendEmail !== 'function' ) {
@@ -125,14 +143,13 @@ class MFAAdapter extends AuthAdapter {
125
143
this . emailCallback = sendEmail ;
126
144
this . digits = digits ;
127
145
this . period = period ;
128
- this . emailOTPExpiry = emailOTPExpiry ;
129
146
this . algorithm = opts . algorithm || 'SHA1' ;
130
147
}
131
148
validateSetUp ( mfaData ) {
132
149
if ( mfaData . mobile && this . sms ) {
133
150
return this . setupMobileOTP ( mfaData . mobile ) ;
134
151
}
135
- if ( mfaData . email && this . email ) {
152
+ if ( mfaData . email && this . email ) {
136
153
return this . setupEmailOTP ( mfaData . email ) ;
137
154
}
138
155
if ( this . totp ) {
@@ -150,13 +167,11 @@ class MFAAdapter extends AuthAdapter {
150
167
if ( this . email && email ) {
151
168
if ( token === 'request' ) {
152
169
const { token : sendToken , expiry } = await this . sendEmail ( email ) ;
153
- const auth = req . original . get ( 'authData' ) || { } ;
154
170
auth . mfa = {
155
171
token : sendToken ,
156
172
email : email ,
157
- expiry : expiry
173
+ expiry : expiry ,
158
174
} ;
159
-
160
175
// Use direct database access to avoid validation
161
176
const query = new Parse . Query ( Parse . User ) ;
162
177
const user = await query . get ( req . object . id , { useMasterKey : true } ) ;
@@ -166,16 +181,16 @@ class MFAAdapter extends AuthAdapter {
166
181
await user . save ( null , {
167
182
useMasterKey : true ,
168
183
context : { skipValidation : true } ,
169
- validateSave : false
184
+ validateSave : false ,
170
185
} ) ;
171
186
172
- throw new Parse . Error ( 209 , 'Please enter the token' ) ;
187
+ throw new Parse . Error ( 209 , 'Please enter the token' ) ;
173
188
}
174
189
if ( ! saved || token !== saved ) {
175
190
throw 'Invalid MFA token 1' ;
176
191
}
177
192
if ( new Date ( ) > expiry ) {
178
- throw 'Expired MFA token' ;
193
+ throw 'Invalid MFA token 2 ' ;
179
194
}
180
195
delete auth . mfa . token ;
181
196
delete auth . mfa . expiry ;
@@ -235,14 +250,14 @@ class MFAAdapter extends AuthAdapter {
235
250
}
236
251
if ( authData . mobile && this . sms ) {
237
252
if ( ! authData . token ) {
238
- throw 'MFA is already set up on this account ' ;
253
+ throw 'Token required to confirm MFA changes. ' ;
239
254
}
240
255
return this . confirmSMSOTP ( authData , req . original . get ( 'authData' ) ?. mfa || { } ) ;
241
256
}
242
257
243
258
if ( authData . email && this . email ) {
244
259
if ( ! authData . token ) {
245
- throw 'MFA is already set up on this account ' ;
260
+ throw 'Token required to confirm MFA changes. ' ;
246
261
}
247
262
return this . confirmEmailOTP ( authData , req . original . get ( 'authData' ) ?. mfa || { } ) ;
248
263
}
@@ -299,11 +314,10 @@ class MFAAdapter extends AuthAdapter {
299
314
300
315
async setupEmailOTP ( email ) {
301
316
const { token, expiry } = await this . sendEmail ( email ) ;
302
- const emailHash = this . md5Hash ( email ) ;
317
+ const emailHash = sha256Hash ( email ) ;
303
318
return {
304
319
save : {
305
320
pending : {
306
- // encode the email md5 has
307
321
[ emailHash ] : {
308
322
token,
309
323
expiry,
@@ -328,20 +342,19 @@ class MFAAdapter extends AuthAdapter {
328
342
}
329
343
330
344
async sendEmail ( email ) {
331
- const decodedEmail = email . replace ( / _ _ _ D O T _ _ _ / g, '.' )
332
- if ( ! / ^ [ ^ \s @ ] + @ [ ^ \s @ ] + \. [ ^ \s @ ] + $ / . test ( decodedEmail ) ) {
345
+ if ( ! / ^ [ ^ \s @ ] + @ [ ^ \s @ ] + \. [ ^ \s @ ] + $ / . test ( email ) ) {
333
346
throw 'Invalid email address.' ;
334
347
}
335
348
let token = '' ;
336
349
while ( token . length < this . digits ) {
337
- token += ( 0 , _cryptoUtils . randomString ) ( 10 ) . replace ( / \D / g, '' ) ;
350
+ token += randomString ( 10 ) . replace ( / \D / g, '' ) ;
338
351
}
339
352
token = token . substring ( 0 , this . digits ) ;
340
- await Promise . resolve ( this . emailCallback ( token , decodedEmail ) ) ;
341
- const expiry = new Date ( new Date ( ) . getTime ( ) + this . emailOTPExpiry * 1000 ) ;
353
+ await Promise . resolve ( this . emailCallback ( token , email ) ) ;
354
+ const expiry = new Date ( new Date ( ) . getTime ( ) + this . period * 1000 ) ;
342
355
return { token, expiry } ;
343
356
}
344
- async confirmSMSOTP ( inputData , authData ) {
357
+ async confirmSMSOTP ( inputData , authData ) {
345
358
const { mobile, token } = inputData ;
346
359
if ( ! authData . pending ?. [ mobile ] ) {
347
360
throw 'This number is not pending' ;
@@ -362,7 +375,7 @@ class MFAAdapter extends AuthAdapter {
362
375
363
376
async confirmEmailOTP ( inputData , authData ) {
364
377
const { email, token } = inputData ;
365
- const emailHash = this . md5Hash ( email ) ;
378
+ const emailHash = sha256Hash ( email ) ;
366
379
if ( ! authData . pending ?. [ emailHash ] ) {
367
380
throw 'This email is not pending' ;
368
381
}
@@ -403,8 +416,5 @@ class MFAAdapter extends AuthAdapter {
403
416
save : { secret, recovery } ,
404
417
} ;
405
418
}
406
- md5Hash ( str ) {
407
- return crypto . createHash ( 'md5' ) . update ( str ) . digest ( 'hex' ) ;
408
- }
409
419
}
410
420
export default new MFAAdapter ( ) ;
0 commit comments