@@ -37,6 +37,8 @@ import {
3737 getMultiFactorResolver ,
3838 multiFactor ,
3939 onAuthStateChanged ,
40+ PhoneAuthProvider ,
41+ PhoneMultiFactorGenerator ,
4042 reload ,
4143 sendEmailVerification ,
4244 signInWithEmailAndPassword ,
@@ -61,7 +63,7 @@ const Button = (props: {
6163 ) ;
6264} ;
6365
64- export function AuthTOTPDemonstrator ( ) {
66+ export function AuthMFADemonstrator ( ) {
6567 const [ authReady , setAuthReady ] = useState ( false ) ;
6668 const [ user , setUser ] = useState < FirebaseAuthTypes . User | null > ( null ) ;
6769
@@ -187,7 +189,7 @@ const Login = () => {
187189 } ;
188190
189191 if ( mfaError ) {
190- return < MfaLogin error = { mfaError } /> ;
192+ return < MfaLogin error = { mfaError } clearError = { ( ) => setMfaError ( undefined ) } /> ;
191193 }
192194
193195 return (
@@ -225,37 +227,73 @@ const Login = () => {
225227 </ View >
226228 ) ;
227229} ;
228-
229- const MfaLogin = ( { error } : { error : FirebaseAuthTypes . MultiFactorError } ) => {
230+ const MfaLogin = ( {
231+ error,
232+ clearError,
233+ } : {
234+ error : FirebaseAuthTypes . MultiFactorError ;
235+ clearError : ( ) => void ;
236+ } ) => {
230237 const [ resolver , setResolver ] = useState < FirebaseAuthTypes . MultiFactorResolver > ( ) ;
231238 const [ activeFactor , setActiveFactor ] = useState < FirebaseAuthTypes . MultiFactorInfo > ( ) ;
232-
239+ const [ verificationId , setVerificationId ] = useState < string > ( '' ) ;
233240 const [ code , setCode ] = useState < string > ( '' ) ;
234241 const [ isLoading , setLoading ] = useState ( false ) ;
235242
236243 useEffect ( ( ) => {
237244 const resolver = getMultiFactorResolver ( getAuth ( ) , error ) ;
238245 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 ] ) ) ;
240248 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 ) ) ;
243252 }
244253 } , [ error ] ) ;
245254
246- const handleConfirm = async ( ) => {
255+ const requestCode = async ( ) => {
247256 if ( ! resolver ) return ;
248257
249258 try {
250259 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+ } ) ,
255265 ) ;
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+
256294 return await resolver . resolveSignIn ( multiFactorAssertion ) ;
257295 } catch ( error ) {
258- console . error ( 'Error during MFA sign in:' , error ) ;
296+ console . error ( 'Error during MFA TOTP sign in:' , error ) ;
259297 } finally {
260298 setLoading ( false ) ;
261299 }
@@ -265,15 +303,57 @@ const MfaLogin = ({ error }: { error: FirebaseAuthTypes.MultiFactorError }) => {
265303 return null ;
266304 }
267305
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+
269332 return (
270333 < View style = { styles . container } >
271334 < 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 >
276349
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+ ) }
277357 < View style = { styles . inputContainer } >
278358 < TextInput
279359 style = { styles . input }
@@ -290,6 +370,17 @@ const MfaLogin = ({ error }: { error: FirebaseAuthTypes.MultiFactorError }) => {
290370 < Button onPress = { handleConfirm } isLoading = { isLoading } >
291371 Verify
292372 </ 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 >
293384 </ View >
294385 </ View >
295386 ) ;
@@ -299,6 +390,7 @@ const Home = () => {
299390 const [ factors , setFactors ] = useState ( getAuth ( ) . currentUser ?. multiFactor ?. enrolledFactors ) ;
300391 const [ addingFactor , setAddingFactor ] = useState ( false ) ;
301392 const [ removingFactor , setRemovingFactor ] = useState ( false ) ;
393+ const [ addingPhoneFactor , setAddingPhoneFactor ] = useState ( false ) ;
302394
303395 const [ totpSecret , setTotpSecret ] = useState < TotpSecret | null > ( null ) ;
304396
@@ -336,11 +428,21 @@ const Home = () => {
336428 }
337429 } ;
338430
431+ if ( addingPhoneFactor ) {
432+ return (
433+ < EnrollPhone
434+ onComplete = { ( ) => {
435+ setFactors ( getAuth ( ) . currentUser ?. multiFactor ?. enrolledFactors ) ;
436+ setAddingPhoneFactor ( false ) ;
437+ } }
438+ />
439+ ) ;
440+ }
441+
339442 if ( totpSecret ) {
340443 return (
341444 < EnrollTotp
342445 totpSecret = { totpSecret }
343- // totpUriQRBase64={totpUriQRBase64}
344446 onComplete = { ( ) => {
345447 setFactors ( getAuth ( ) . currentUser ?. multiFactor ?. enrolledFactors ) ;
346448 setTotpSecret ( null ) ;
@@ -361,6 +463,7 @@ const Home = () => {
361463
362464 { factors ?. map ( factor => (
363465 < Button
466+ style = { { marginTop : 20 } }
364467 key = { factor . uid }
365468 onPress = { ( ) => handleRemoveFactor ( factor ) }
366469 isLoading = { removingFactor }
@@ -369,8 +472,105 @@ const Home = () => {
369472 </ Button >
370473 ) ) }
371474
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
374574 </ Button >
375575
376576 < Pressable style = { styles . secondaryButton } onPress = { ( ) => signOut ( getAuth ( ) ) } >
0 commit comments