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.
9
10
* @param {String } [options.algorithm="SHA1"] - The algorithm used for TOTP generation. Defaults to `"SHA1"`.
10
11
* @param {Function } [options.sendSMS] - A callback function for sending SMS OTPs. Required if `"SMS"` is included in `options`.
11
12
*
@@ -87,8 +88,9 @@ class MFAAdapter extends AuthAdapter {
87
88
}
88
89
this . sms = validOptions . includes ( 'SMS' ) ;
89
90
this . totp = validOptions . includes ( 'TOTP' ) ;
90
- if ( ! this . sms && ! this . totp ) {
91
- throw 'mfa.options must include SMS or TOTP' ;
91
+ this . email = validOptions . includes ( 'EMAIL' ) ;
92
+ if ( ! this . sms && ! this . totp && ! this . email ) {
93
+ throw 'mfa.options must include SMS or TOTP or EMAIL' ;
92
94
}
93
95
const digits = opts . digits || 6 ;
94
96
const period = opts . period || 30 ;
@@ -104,11 +106,21 @@ class MFAAdapter extends AuthAdapter {
104
106
if ( period < 10 ) {
105
107
throw 'mfa.period must be greater than 10' ;
106
108
}
109
+ if ( this . email ) {
110
+ if ( this . emailOTPExpiry < 60 ) {
111
+ throw 'mfa.emailExpiry must be greater than 60 seconds' ;
112
+ }
113
+ }
107
114
const sendSMS = opts . sendSMS ;
115
+ const sendEmail = opts . sendEmail ;
116
+ if ( this . email && typeof sendEmail !== 'function' ) {
117
+ throw 'mfa.sendEmail callback must be defined when using EMAIL OTPs' ;
118
+ }
108
119
if ( this . sms && typeof sendSMS !== 'function' ) {
109
120
throw 'mfa.sendSMS callback must be defined when using SMS OTPs' ;
110
121
}
111
122
this . smsCallback = sendSMS ;
123
+ this . emailCallback = sendEmail ;
112
124
this . digits = digits ;
113
125
this . period = period ;
114
126
this . algorithm = opts . algorithm || 'SHA1' ;
@@ -120,6 +132,9 @@ class MFAAdapter extends AuthAdapter {
120
132
if ( this . totp ) {
121
133
return this . setupTOTP ( mfaData ) ;
122
134
}
135
+ if ( mfaData . email && this . email ) {
136
+ return this . setupEmailOTP ( mfaData . email ) ;
137
+ }
123
138
throw 'Invalid MFA data' ;
124
139
}
125
140
async validateLogin ( loginData , _ , req ) {
@@ -128,7 +143,28 @@ class MFAAdapter extends AuthAdapter {
128
143
} ;
129
144
const token = loginData . token ;
130
145
const auth = req . original . get ( 'authData' ) || { } ;
131
- const { secret, recovery, mobile, token : saved , expiry } = auth . mfa || { } ;
146
+ const { secret, recovery, mobile, email, token : saved , expiry } = auth . mfa || { } ;
147
+ if ( this . email && email ) {
148
+ if ( token === 'request' ) {
149
+ const { token : sendToken , expiry } = await this . sendEmail ( email ) ;
150
+ auth . mfa . token = sendToken ;
151
+ auth . mfa . expiry = expiry ;
152
+ req . object . set ( 'authData' , auth ) ;
153
+ await req . object . save ( null , { useMasterKey : true } ) ;
154
+ throw 'Please enter the token' ;
155
+ }
156
+ if ( ! saved || token !== saved ) {
157
+ throw 'Invalid MFA token 1' ;
158
+ }
159
+ if ( new Date ( ) > expiry ) {
160
+ throw 'Invalid MFA token 2' ;
161
+ }
162
+ delete auth . mfa . token ;
163
+ delete auth . mfa . expiry ;
164
+ return {
165
+ save : auth . mfa ,
166
+ } ;
167
+ }
132
168
if ( this . sms && mobile ) {
133
169
if ( token === 'request' ) {
134
170
const { token : sendToken , expiry } = await this . sendSMS ( mobile ) ;
@@ -185,6 +221,13 @@ class MFAAdapter extends AuthAdapter {
185
221
}
186
222
return this . confirmSMSOTP ( authData , req . original . get ( 'authData' ) ?. mfa || { } ) ;
187
223
}
224
+
225
+ if ( authData . email && this . email ) {
226
+ if ( ! authData . token ) {
227
+ throw 'MFA is already set up on this account' ;
228
+ }
229
+ return this . confirmEmailOTP ( authData , req . original . get ( 'authData' ) ?. mfa || { } ) ;
230
+ }
188
231
if ( this . totp ) {
189
232
await this . validateLogin ( { token : authData . old } , null , req ) ;
190
233
return this . validateSetUp ( authData ) ;
@@ -205,6 +248,11 @@ class MFAAdapter extends AuthAdapter {
205
248
status : 'enabled' ,
206
249
} ;
207
250
}
251
+ if ( this . email && authData . email ) {
252
+ return {
253
+ status : 'enabled' ,
254
+ } ;
255
+ }
208
256
return {
209
257
status : 'disabled' ,
210
258
} ;
@@ -231,6 +279,20 @@ class MFAAdapter extends AuthAdapter {
231
279
} ;
232
280
}
233
281
282
+ async setupEmailOTP ( email ) {
283
+ const { token, expiry } = await this . sendEmail ( email ) ;
284
+ return {
285
+ save : {
286
+ pending : {
287
+ [ email ] : {
288
+ token,
289
+ expiry,
290
+ } ,
291
+ } ,
292
+ } ,
293
+ } ;
294
+ }
295
+
234
296
async sendSMS ( mobile ) {
235
297
if ( ! / ^ [ + ] * [ ( ] { 0 , 1 } [ 0 - 9 ] { 1 , 3 } [ ) ] { 0 , 1 } [ - \s \. / 0 - 9 ] * $ / g. test ( mobile ) ) {
236
298
throw 'Invalid mobile number.' ;
@@ -245,7 +307,20 @@ class MFAAdapter extends AuthAdapter {
245
307
return { token, expiry } ;
246
308
}
247
309
248
- async confirmSMSOTP ( inputData , authData ) {
310
+ async sendEmail ( email ) {
311
+ if ( ! / ^ [ ^ \s @ ] + @ [ ^ \s @ ] + \. [ ^ \s @ ] + $ / . test ( email ) ) {
312
+ throw 'Invalid email address.' ;
313
+ }
314
+ let token = '' ;
315
+ while ( token . length < this . digits ) {
316
+ token += randomString ( 10 ) . replace ( / \D / g, '' ) ;
317
+ }
318
+ token = token . substring ( 0 , this . digits ) ;
319
+ await Promise . resolve ( this . emailCallback ( token , email ) ) ;
320
+ const expiry = new Date ( new Date ( ) . getTime ( ) + this . emailOTPExpiry * 1000 ) ;
321
+ return { token, expiry } ;
322
+ }
323
+ async confirmSMSOTP ( inputData , authData ) {
249
324
const { mobile, token } = inputData ;
250
325
if ( ! authData . pending ?. [ mobile ] ) {
251
326
throw 'This number is not pending' ;
@@ -264,6 +339,25 @@ class MFAAdapter extends AuthAdapter {
264
339
} ;
265
340
}
266
341
342
+ async confirmEmailOTP ( inputData , authData ) {
343
+ const { email, token } = inputData ;
344
+ if ( ! authData . pending ?. [ email ] ) {
345
+ throw 'This email is not pending' ;
346
+ }
347
+ const pendingData = authData . pending [ email ] ;
348
+ if ( token !== pendingData . token ) {
349
+ throw 'Invalid MFA token' ;
350
+ }
351
+ if ( new Date ( ) > pendingData . expiry ) {
352
+ throw 'Invalid MFA token' ;
353
+ }
354
+ delete authData . pending [ email ] ;
355
+ authData . email = email ;
356
+ return {
357
+ save : authData ,
358
+ } ;
359
+ }
360
+
267
361
setupTOTP ( mfaData ) {
268
362
const { secret, token } = mfaData ;
269
363
if ( ! secret || ! token || secret . length < 20 ) {
0 commit comments