@@ -37,6 +37,8 @@ import {
37
37
getMultiFactorResolver ,
38
38
multiFactor ,
39
39
onAuthStateChanged ,
40
+ PhoneAuthProvider ,
41
+ PhoneMultiFactorGenerator ,
40
42
reload ,
41
43
sendEmailVerification ,
42
44
signInWithEmailAndPassword ,
@@ -61,7 +63,7 @@ const Button = (props: {
61
63
) ;
62
64
} ;
63
65
64
- export function AuthTOTPDemonstrator ( ) {
66
+ export function AuthMFADemonstrator ( ) {
65
67
const [ authReady , setAuthReady ] = useState ( false ) ;
66
68
const [ user , setUser ] = useState < FirebaseAuthTypes . User | null > ( null ) ;
67
69
@@ -187,7 +189,7 @@ const Login = () => {
187
189
} ;
188
190
189
191
if ( mfaError ) {
190
- return < MfaLogin error = { mfaError } /> ;
192
+ return < MfaLogin error = { mfaError } clearError = { ( ) => setMfaError ( undefined ) } /> ;
191
193
}
192
194
193
195
return (
@@ -225,37 +227,73 @@ const Login = () => {
225
227
</ View >
226
228
) ;
227
229
} ;
228
-
229
- const MfaLogin = ( { error } : { error : FirebaseAuthTypes . MultiFactorError } ) => {
230
+ const MfaLogin = ( {
231
+ error,
232
+ clearError,
233
+ } : {
234
+ error : FirebaseAuthTypes . MultiFactorError ;
235
+ clearError : ( ) => void ;
236
+ } ) => {
230
237
const [ resolver , setResolver ] = useState < FirebaseAuthTypes . MultiFactorResolver > ( ) ;
231
238
const [ activeFactor , setActiveFactor ] = useState < FirebaseAuthTypes . MultiFactorInfo > ( ) ;
232
-
239
+ const [ verificationId , setVerificationId ] = useState < string > ( '' ) ;
233
240
const [ code , setCode ] = useState < string > ( '' ) ;
234
241
const [ isLoading , setLoading ] = useState ( false ) ;
235
242
236
243
useEffect ( ( ) => {
237
244
const resolver = getMultiFactorResolver ( getAuth ( ) , error ) ;
238
245
setResolver ( resolver ) ;
239
- setActiveFactor ( resolver . hints [ 0 ] ) ;
246
+ console . log ( 'Active factors: ' + JSON . stringify ( resolver . hints ) ) ;
247
+ console . log ( 'resolver.hints[0] is ' + JSON . stringify ( resolver . hints [ 0 ] ) ) ;
240
248
if ( resolver . hints . length === 1 ) {
241
- const hint = resolver . hints [ 0 ] ;
242
- setActiveFactor ( hint ) ;
249
+ setActiveFactor ( resolver . hints [ 0 ] ) ;
250
+ console . log ( 'activeFactor is ' + JSON . stringify ( activeFactor ) ) ;
251
+ console . log ( 'activeFactor.factorId is ' + JSON . stringify ( activeFactor ?. factorId ) ) ;
243
252
}
244
253
} , [ error ] ) ;
245
254
246
- const handleConfirm = async ( ) => {
255
+ const requestCode = async ( ) => {
247
256
if ( ! resolver ) return ;
248
257
249
258
try {
250
259
setLoading ( true ) ;
251
- // For demo, assume only 1 hint and it's totp
252
- const multiFactorAssertion = TotpMultiFactorGenerator . assertionForSignIn (
253
- activeFactor ! . uid ,
254
- code ,
260
+ setVerificationId (
261
+ await new PhoneAuthProvider ( getAuth ( ) ) . verifyPhoneNumber ( {
262
+ multiFactorHint : activeFactor ,
263
+ session : resolver . session ,
264
+ } ) ,
255
265
) ;
266
+ } catch ( error ) {
267
+ console . error ( 'Error during MFA Phone code send:' , error ) ;
268
+ } finally {
269
+ setLoading ( false ) ;
270
+ }
271
+ } ;
272
+
273
+ const handleConfirm = async ( ) => {
274
+ if ( ! resolver || ! activeFactor ) return ;
275
+
276
+ try {
277
+ setLoading ( true ) ;
278
+ let multiFactorAssertion : FirebaseAuthTypes . MultiFactorAssertion ;
279
+ switch ( activeFactor . factorId ) {
280
+ case 'totp' :
281
+ multiFactorAssertion = TotpMultiFactorGenerator . assertionForSignIn (
282
+ activeFactor ! . uid ,
283
+ code ,
284
+ ) ;
285
+ break ;
286
+ case 'phone' :
287
+ const phoneAuthCredential = new PhoneAuthProvider . credential ( verificationId , code ) ;
288
+ multiFactorAssertion = PhoneMultiFactorGenerator . assertion ( phoneAuthCredential ) ;
289
+ break ;
290
+ default :
291
+ throw new Error ( 'Unknown MFA factor type: ' + activeFactor . factorId ) ;
292
+ }
293
+
256
294
return await resolver . resolveSignIn ( multiFactorAssertion ) ;
257
295
} catch ( error ) {
258
- console . error ( 'Error during MFA sign in:' , error ) ;
296
+ console . error ( 'Error during MFA TOTP sign in:' , error ) ;
259
297
} finally {
260
298
setLoading ( false ) ;
261
299
}
@@ -265,15 +303,57 @@ const MfaLogin = ({ error }: { error: FirebaseAuthTypes.MultiFactorError }) => {
265
303
return null ;
266
304
}
267
305
268
- // For demo, assume only 1 hint and it's totp
306
+ if ( ! activeFactor ) {
307
+ return (
308
+ < View style = { styles . container } >
309
+ < View style = { styles . card } >
310
+ < Text style = { styles . title } > MFA Factor Selection</ Text >
311
+ < Text style = { styles . subtitle } >
312
+ You have multiple second factors enrolled. Please select one.
313
+ </ Text >
314
+ { resolver . hints ?. map ( factor => (
315
+ < Button
316
+ style = { { marginTop : 20 } }
317
+ key = { factor . uid }
318
+ onPress = { ( ) => setActiveFactor ( factor ) }
319
+ >
320
+ { `${ factor . displayName || factor . factorId } (${ factor . factorId } )` }
321
+ </ Button >
322
+ ) ) }
323
+
324
+ < Pressable style = { styles . secondaryButton } onPress = { clearError } >
325
+ < Text style = { styles . secondaryButtonText } > Sign Out</ Text >
326
+ </ Pressable >
327
+ </ View >
328
+ </ View >
329
+ ) ;
330
+ }
331
+
269
332
return (
270
333
< View style = { styles . container } >
271
334
< View style = { styles . card } >
272
- < Text style = { styles . title } > Two-Factor Authentication</ Text >
273
- < Text style = { styles . subtitle } >
274
- Please enter the verification code from your authenticator app
275
- </ Text >
335
+ { /* Show the TOTP code entry if that factor is selected */ }
336
+ { activeFactor !== undefined && activeFactor . factorId === 'totp' && (
337
+ < >
338
+ < Text style = { styles . title } > TOTP Two-Factor Authentication</ Text >
339
+ < Text style = { styles . subtitle } >
340
+ Please enter the verification code from your authenticator app
341
+ </ Text >
342
+ </ >
343
+ ) }
344
+ { /* Show the Phone verify && code entry if that factor is selected */ }
345
+ { activeFactor !== undefined && activeFactor . factorId === 'phone' && (
346
+ < >
347
+ < Text style = { styles . title } > Phone Two-Factor Authentication</ Text >
348
+ < Text style = { styles . subtitle } > 1) Request SMS code</ Text >
276
349
350
+ < Button onPress = { requestCode } isLoading = { isLoading } >
351
+ Request SMS Code
352
+ </ Button >
353
+
354
+ < Text style = { styles . subtitle } > 2) enter the code, then Verify</ Text >
355
+ </ >
356
+ ) }
277
357
< View style = { styles . inputContainer } >
278
358
< TextInput
279
359
style = { styles . input }
@@ -290,6 +370,17 @@ const MfaLogin = ({ error }: { error: FirebaseAuthTypes.MultiFactorError }) => {
290
370
< Button onPress = { handleConfirm } isLoading = { isLoading } >
291
371
Verify
292
372
</ Button >
373
+
374
+ { /* Allow user to change factor if more than one */ }
375
+ { activeFactor && resolver . hints . length > 1 && (
376
+ < Pressable style = { styles . secondaryButton } onPress = { ( ) => setActiveFactor ( undefined ) } >
377
+ < Text style = { styles . secondaryButtonText } > Switch Factor</ Text >
378
+ </ Pressable >
379
+ ) }
380
+
381
+ < Pressable style = { styles . secondaryButton } onPress = { clearError } >
382
+ < Text style = { styles . secondaryButtonText } > Sign Out</ Text >
383
+ </ Pressable >
293
384
</ View >
294
385
</ View >
295
386
) ;
@@ -299,6 +390,7 @@ const Home = () => {
299
390
const [ factors , setFactors ] = useState ( getAuth ( ) . currentUser ?. multiFactor ?. enrolledFactors ) ;
300
391
const [ addingFactor , setAddingFactor ] = useState ( false ) ;
301
392
const [ removingFactor , setRemovingFactor ] = useState ( false ) ;
393
+ const [ addingPhoneFactor , setAddingPhoneFactor ] = useState ( false ) ;
302
394
303
395
const [ totpSecret , setTotpSecret ] = useState < TotpSecret | null > ( null ) ;
304
396
@@ -336,11 +428,21 @@ const Home = () => {
336
428
}
337
429
} ;
338
430
431
+ if ( addingPhoneFactor ) {
432
+ return (
433
+ < EnrollPhone
434
+ onComplete = { ( ) => {
435
+ setFactors ( getAuth ( ) . currentUser ?. multiFactor ?. enrolledFactors ) ;
436
+ setAddingPhoneFactor ( false ) ;
437
+ } }
438
+ />
439
+ ) ;
440
+ }
441
+
339
442
if ( totpSecret ) {
340
443
return (
341
444
< EnrollTotp
342
445
totpSecret = { totpSecret }
343
- // totpUriQRBase64={totpUriQRBase64}
344
446
onComplete = { ( ) => {
345
447
setFactors ( getAuth ( ) . currentUser ?. multiFactor ?. enrolledFactors ) ;
346
448
setTotpSecret ( null ) ;
@@ -361,6 +463,7 @@ const Home = () => {
361
463
362
464
{ factors ?. map ( factor => (
363
465
< Button
466
+ style = { { marginTop : 20 } }
364
467
key = { factor . uid }
365
468
onPress = { ( ) => handleRemoveFactor ( factor ) }
366
469
isLoading = { removingFactor }
@@ -369,8 +472,105 @@ const Home = () => {
369
472
</ Button >
370
473
) ) }
371
474
372
- < Button style = { { marginTop : 20 } } isLoading = { addingFactor } onPress = { generateTotpSecret } >
373
- Add TOTP Factor
475
+ { factors ?. find ( factor => factor . factorId === 'totp' ) === undefined && (
476
+ < Button style = { { marginTop : 20 } } isLoading = { addingFactor } onPress = { generateTotpSecret } >
477
+ Add TOTP Factor
478
+ </ Button >
479
+ ) }
480
+
481
+ { factors ?. find ( factor => factor . factorId === 'phone' ) === undefined && (
482
+ < Button
483
+ style = { { marginTop : 20 } }
484
+ isLoading = { addingFactor }
485
+ onPress = { ( ) => setAddingPhoneFactor ( true ) }
486
+ >
487
+ Add SMS Factor
488
+ </ Button >
489
+ ) }
490
+
491
+ < Pressable style = { styles . secondaryButton } onPress = { ( ) => signOut ( getAuth ( ) ) } >
492
+ < Text style = { styles . secondaryButtonText } > Sign Out</ Text >
493
+ </ Pressable >
494
+ </ View >
495
+ </ View >
496
+ ) ;
497
+ } ;
498
+
499
+ const EnrollPhone = ( { onComplete } : { onComplete : ( ) => void } ) => {
500
+ const [ waitingForPhoneVerification , setWaitingForPhoneVerification ] = useState ( false ) ;
501
+ const [ verificationCode , setVerificationCode ] = useState ( '' ) ;
502
+ const [ verificationId , setVerificationId ] = useState ( '' ) ;
503
+ const [ phoneNumber , setPhoneNumber ] = useState ( '' ) ;
504
+ const [ isLoading , setLoading ] = useState ( false ) ;
505
+
506
+ const handleVerifyPhone = async ( ) => {
507
+ setLoading ( true ) ;
508
+ setWaitingForPhoneVerification ( true ) ;
509
+ try {
510
+ const user = getAuth ( ) . currentUser ;
511
+ if ( ! user ) return ;
512
+
513
+ const session = await multiFactor ( user ) . getSession ( ) ;
514
+ setVerificationId (
515
+ await new PhoneAuthProvider ( getAuth ( ) ) . verifyPhoneNumber ( {
516
+ phoneNumber,
517
+ session,
518
+ } ) ,
519
+ ) ;
520
+ } catch ( error ) {
521
+ console . error ( 'Error sending phone verification:' , error ) ;
522
+ } finally {
523
+ setLoading ( false ) ;
524
+ }
525
+ } ;
526
+
527
+ const handleEnrollPhone = async ( ) => {
528
+ setLoading ( true ) ;
529
+ try {
530
+ const user = getAuth ( ) . currentUser ;
531
+ if ( ! user ) return ;
532
+ const cred = PhoneAuthProvider . credential ( verificationId , verificationCode ) ;
533
+ const multiFactorAssertion = PhoneMultiFactorGenerator . assertion ( cred ) ;
534
+ await multiFactor ( user ) . enroll ( multiFactorAssertion , 'Phone' ) ;
535
+ onComplete ( ) ;
536
+ } catch ( error ) {
537
+ console . error ( 'Error enrolling Phone:' , error ) ;
538
+ } finally {
539
+ setLoading ( false ) ;
540
+ setWaitingForPhoneVerification ( false ) ;
541
+ }
542
+ } ;
543
+
544
+ return (
545
+ < View style = { styles . container } >
546
+ < View style = { styles . card } >
547
+ < Text style = { styles . title } > Enroll Phone</ Text >
548
+
549
+ < Text style = { styles . subtitle } > 1) Enter phone # and press send code</ Text >
550
+
551
+ < TextInput
552
+ style = { styles . input }
553
+ value = { phoneNumber }
554
+ placeholder = "+593985787666"
555
+ placeholderTextColor = "#9CA3AF"
556
+ onChangeText = { setPhoneNumber }
557
+ />
558
+ < Text style = { styles . subtitle } > 2) Enter the verification code received.</ Text >
559
+ < TextInput
560
+ style = { styles . input }
561
+ placeholder = "Verification Code"
562
+ placeholderTextColor = "#9CA3AF"
563
+ keyboardType = "number-pad"
564
+ textAlign = "center"
565
+ maxLength = { 6 }
566
+ onChangeText = { setVerificationCode }
567
+ value = { verificationCode }
568
+ />
569
+ < Button onPress = { handleVerifyPhone } isLoading = { isLoading || waitingForPhoneVerification } >
570
+ Send Code
571
+ </ Button >
572
+ < Button style = { { marginTop : 20 } } onPress = { handleEnrollPhone } isLoading = { isLoading } >
573
+ Confirm
374
574
</ Button >
375
575
376
576
< Pressable style = { styles . secondaryButton } onPress = { ( ) => signOut ( getAuth ( ) ) } >
0 commit comments