diff --git a/docs/auth/multi-factor-auth.md b/docs/auth/multi-factor-auth.md index ccfc4d8749..a6c6face8d 100644 --- a/docs/auth/multi-factor-auth.md +++ b/docs/auth/multi-factor-auth.md @@ -14,6 +14,8 @@ The [official guide for Firebase web TOTP authentication](https://firebase.googl The API details and usage examples may be combined with the full Phone auth example below to give you an MFA solution that fully supports TOTP or SMS MFA. +You may also find it useful to investigate [the local / manual test screens](https://github.com/invertase/react-native-firebase/blob/main/tests/local-tests/auth/auth-mfa-demonstrator.tsx) that we use to verify this functionality. + # Phone MFA ## iOS Setup diff --git a/packages/auth/__tests__/auth.test.ts b/packages/auth/__tests__/auth.test.ts index fd47ecd49e..2ef7e2bd3f 100644 --- a/packages/auth/__tests__/auth.test.ts +++ b/packages/auth/__tests__/auth.test.ts @@ -72,6 +72,8 @@ import auth, { OIDCAuthProvider, PhoneAuthProvider, PhoneMultiFactorGenerator, + TotpSecret, + TotpMultiFactorGenerator, TwitterAuthProvider, PhoneAuthState, } from '../lib'; @@ -508,6 +510,14 @@ describe('Auth', function () { expect(PhoneMultiFactorGenerator).toBeDefined(); }); + it('`TotpSecret` class is properly exposed to end user', function () { + expect(TotpSecret).toBeDefined(); + }); + + it('`TotpMultiFactorGenerator` class is properly exposed to end user', function () { + expect(TotpMultiFactorGenerator).toBeDefined(); + }); + it('`TwitterAuthProvider` class is properly exposed to end user', function () { expect(TwitterAuthProvider).toBeDefined(); }); diff --git a/packages/auth/e2e/multiFactor.e2e.js b/packages/auth/e2e/multiFactor.e2e.js index fe40945d4d..5da0b9e0a7 100644 --- a/packages/auth/e2e/multiFactor.e2e.js +++ b/packages/auth/e2e/multiFactor.e2e.js @@ -556,9 +556,11 @@ describe('multi-factor modular', function () { describe('sign-in', function () { it('requires multi-factor auth when enrolled', async function () { - if (Platform.ios) { - this.skip(); - } + // iOS receives: + // NativeFirebaseError: [auth/unknown] MFA_ENROLLMENT_NOT_FOUND + // if (device.getPlatform() === 'ios') { + // this.skip(); + // } const { phoneNumber, email, password } = await createUserWithMultiFactor(); const maskedNumber = '+********' + phoneNumber.substring(phoneNumber.length - 4); diff --git a/packages/auth/lib/index.js b/packages/auth/lib/index.js index f099f29cdc..21d3e8db1d 100644 --- a/packages/auth/lib/index.js +++ b/packages/auth/lib/index.js @@ -48,6 +48,7 @@ import OAuthProvider from './providers/OAuthProvider'; import OIDCAuthProvider from './providers/OIDCAuthProvider'; import PhoneAuthProvider from './providers/PhoneAuthProvider'; import TwitterAuthProvider from './providers/TwitterAuthProvider'; +import { TotpSecret } from './TotpSecret'; import version from './version'; import fallBackModule from './web/RNFBAuthModule'; @@ -68,6 +69,7 @@ export { FacebookAuthProvider, PhoneMultiFactorGenerator, TotpMultiFactorGenerator, + TotpSecret, OAuthProvider, OIDCAuthProvider, PhoneAuthState, diff --git a/packages/auth/lib/modular/index.d.ts b/packages/auth/lib/modular/index.d.ts index a2b769e4dc..6c2e0b8487 100644 --- a/packages/auth/lib/modular/index.d.ts +++ b/packages/auth/lib/modular/index.d.ts @@ -795,6 +795,8 @@ export { OIDCAuthProvider, PhoneAuthProvider, PhoneMultiFactorGenerator, + TotpMultiFactorGenerator, + TotpSecret, TwitterAuthProvider, PhoneAuthState, } from '../index'; diff --git a/packages/auth/lib/modular/index.js b/packages/auth/lib/modular/index.js index 8c38cff83e..b9385d9c46 100644 --- a/packages/auth/lib/modular/index.js +++ b/packages/auth/lib/modular/index.js @@ -18,7 +18,6 @@ import { getApp } from '@react-native-firebase/app'; import { fetchPasswordPolicy } from '../password-policy/passwordPolicyApi'; import { PasswordPolicyImpl } from '../password-policy/PasswordPolicyImpl'; -import FacebookAuthProvider from '../providers/FacebookAuthProvider'; import { MultiFactorUser } from '../multiFactor'; import { MODULAR_DEPRECATION_ARG } from '@react-native-firebase/app/lib/common'; diff --git a/patches/firebase-tools+14.14.0.patch b/patches/firebase-tools+14.14.0.patch new file mode 100644 index 0000000000..67171b0e8c --- /dev/null +++ b/patches/firebase-tools+14.14.0.patch @@ -0,0 +1,56 @@ +diff --git a/node_modules/firebase-tools/lib/emulator/auth/operations.js b/node_modules/firebase-tools/lib/emulator/auth/operations.js +index 2104c80..c42d2f5 100644 +--- a/node_modules/firebase-tools/lib/emulator/auth/operations.js ++++ b/node_modules/firebase-tools/lib/emulator/auth/operations.js +@@ -721,6 +721,7 @@ function sendVerificationCode(state, reqBody) { + (0, errors_1.assert)(state instanceof state_1.AgentProjectState, "UNSUPPORTED_TENANT_OPERATION"); + (0, errors_1.assert)(reqBody.phoneNumber && (0, utils_1.isValidPhoneNumber)(reqBody.phoneNumber), "INVALID_PHONE_NUMBER : Invalid format."); + const user = state.getUserByPhoneNumber(reqBody.phoneNumber); ++ console.log('getting user by ' + reqBody.phoneNumber + ', got ' + JSON.stringify(user)); + (0, errors_1.assert)(!((_a = user === null || user === void 0 ? void 0 : user.mfaInfo) === null || _a === void 0 ? void 0 : _a.length), "UNSUPPORTED_FIRST_FACTOR : A phone number cannot be set as a first factor on an SMS based MFA user."); + const { sessionInfo, phoneNumber, code } = state.createVerificationCode(reqBody.phoneNumber); + emulatorLogger_1.EmulatorLogger.forEmulator(types_1.Emulators.AUTH).log("BULLET", `To verify the phone number ${phoneNumber}, use the code ${code}.`); +@@ -1485,6 +1486,7 @@ function mfaSignInStart(state, reqBody) { + (0, errors_1.assert)(reqBody.mfaPendingCredential, "MISSING_MFA_PENDING_CREDENTIAL : Request does not have MFA pending credential."); + (0, errors_1.assert)(reqBody.mfaEnrollmentId, "MISSING_MFA_ENROLLMENT_ID : No second factor identifier is provided."); + const { user } = parsePendingCredential(state, reqBody.mfaPendingCredential); ++ console.log('in mfaSignInStart?') + const enrollment = (_b = user.mfaInfo) === null || _b === void 0 ? void 0 : _b.find((factor) => factor.mfaEnrollmentId === reqBody.mfaEnrollmentId); + (0, errors_1.assert)(enrollment, "MFA_ENROLLMENT_NOT_FOUND"); + const phoneNumber = enrollment.unobfuscatedPhoneInfo; +@@ -1511,8 +1513,9 @@ async function mfaSignInFinalize(state, reqBody) { + (0, errors_1.assert)(code, "MISSING_CODE"); + (0, errors_1.assert)(sessionInfo, "MISSING_SESSION_INFO"); + const phoneNumber = verifyPhoneNumber(state, sessionInfo, code); ++ console.error('phoneNumber is ' + phoneNumber); + let { user, signInProvider } = parsePendingCredential(state, reqBody.mfaPendingCredential); +- const enrollment = (_b = user.mfaInfo) === null || _b === void 0 ? void 0 : _b.find((enrollment) => enrollment.unobfuscatedPhoneInfo === phoneNumber); ++ const enrollment = (_b = user.mfaInfo) === null || _b === void 0 ? void 0 : _b.find((enrollment) => { console.log('searching, enrollment is ' + JSON.stringify(enrollment)); return enrollment.unobfuscatedPhoneInfo === phoneNumber || ('+********' + enrollment.unobfuscatedPhoneInfo.substring(enrollment.unobfuscatedPhoneInfo.length - 4)) === phoneNumber; }); + const { updates, extraClaims } = await fetchBlockingFunction(state, state_1.BlockingFunctionEvents.BEFORE_SIGN_IN, user, { signInMethod: signInProvider, signInSecondFactor: "phone" }); + user = state.updateUserByLocalId(user.localId, Object.assign(Object.assign({}, updates), { lastLoginAt: Date.now().toString() })); + (0, errors_1.assert)(enrollment && enrollment.mfaEnrollmentId, "MFA_ENROLLMENT_NOT_FOUND"); +@@ -1657,6 +1660,7 @@ function verifyPhoneNumber(state, sessionInfo, code) { + (0, errors_1.assert)(verification, "INVALID_SESSION_INFO"); + (0, errors_1.assert)(verification.code === code, "INVALID_CODE"); + state.deleteVerificationCodeBySessionInfo(sessionInfo); ++ console.log('verifyPhoneNumber verification is ' + JSON.stringify(verification)); + return verification.phoneNumber; + } + const CUSTOM_ATTRIBUTES_MAX_LENGTH = 1000; +diff --git a/node_modules/firebase-tools/lib/emulator/auth/state.js b/node_modules/firebase-tools/lib/emulator/auth/state.js +index 204ead8..99425c7 100644 +--- a/node_modules/firebase-tools/lib/emulator/auth/state.js ++++ b/node_modules/firebase-tools/lib/emulator/auth/state.js +@@ -328,10 +328,12 @@ class ProjectState { + phoneNumber, + sessionInfo, + }; ++ console.log('creating verification code for ' + phoneNumber + ': ' + JSON.stringify(verification)); + this.verificationCodes.set(sessionInfo, verification); + return verification; + } + getVerificationCodeBySessionInfo(sessionInfo) { ++ console.log('getVerificationCodesBySessionInfo returning ' + JSON.stringify(this.verificationCodes.get(sessionInfo))); + return this.verificationCodes.get(sessionInfo); + } + deleteVerificationCodeBySessionInfo(sessionInfo) { diff --git a/tests/ios/Podfile.lock b/tests/ios/Podfile.lock index 78530f8b63..166ce39017 100644 --- a/tests/ios/Podfile.lock +++ b/tests/ios/Podfile.lock @@ -1803,69 +1803,69 @@ PODS: - Yoga - RNDeviceInfo (14.0.4): - React-Core - - RNFBAnalytics (23.1.2): + - RNFBAnalytics (23.2.1): - FirebaseAnalytics/Core (= 12.2.0) - FirebaseAnalytics/IdentitySupport (= 12.2.0) - GoogleAdsOnDeviceConversion - React-Core - RNFBApp - - RNFBApp (23.1.2): + - RNFBApp (23.2.1): - Firebase/CoreOnly (= 12.2.0) - React-Core - - RNFBAppCheck (23.1.2): + - RNFBAppCheck (23.2.1): - Firebase/AppCheck (= 12.2.0) - React-Core - RNFBApp - - RNFBAppDistribution (23.1.2): + - RNFBAppDistribution (23.2.1): - Firebase/AppDistribution (= 12.2.0) - React-Core - RNFBApp - - RNFBAuth (23.1.2): + - RNFBAuth (23.2.1): - Firebase/Auth (= 12.2.0) - React-Core - RNFBApp - - RNFBCrashlytics (23.1.2): + - RNFBCrashlytics (23.2.1): - Firebase/Crashlytics (= 12.2.0) - FirebaseCoreExtension - React-Core - RNFBApp - - RNFBDatabase (23.1.2): + - RNFBDatabase (23.2.1): - Firebase/Database (= 12.2.0) - React-Core - RNFBApp - - RNFBFirestore (23.1.2): + - RNFBFirestore (23.2.1): - Firebase/Firestore (= 12.2.0) - React-Core - RNFBApp - - RNFBFunctions (23.1.2): + - RNFBFunctions (23.2.1): - Firebase/Functions (= 12.2.0) - React-Core - RNFBApp - - RNFBInAppMessaging (23.1.2): + - RNFBInAppMessaging (23.2.1): - Firebase/InAppMessaging (= 12.2.0) - React-Core - RNFBApp - - RNFBInstallations (23.1.2): + - RNFBInstallations (23.2.1): - Firebase/Installations (= 12.2.0) - React-Core - RNFBApp - - RNFBMessaging (23.1.2): + - RNFBMessaging (23.2.1): - Firebase/Messaging (= 12.2.0) - FirebaseCoreExtension - React-Core - RNFBApp - - RNFBML (23.1.2): + - RNFBML (23.2.1): - React-Core - RNFBApp - - RNFBPerf (23.1.2): + - RNFBPerf (23.2.1): - Firebase/Performance (= 12.2.0) - React-Core - RNFBApp - - RNFBRemoteConfig (23.1.2): + - RNFBRemoteConfig (23.2.1): - Firebase/RemoteConfig (= 12.2.0) - React-Core - RNFBApp - - RNFBStorage (23.1.2): + - RNFBStorage (23.2.1): - Firebase/Storage (= 12.2.0) - React-Core - RNFBApp @@ -2295,22 +2295,22 @@ SPEC CHECKSUMS: RecaptchaInterop: 11e0b637842dfb48308d242afc3f448062325aba RNCAsyncStorage: 6a8127b6987dc9fbce778669b252b14c8355c7ce RNDeviceInfo: d863506092aef7e7af3a1c350c913d867d795047 - RNFBAnalytics: 87dd8ea00809fb99dc44b0b21f9745dd00b3d727 - RNFBApp: 1641151815621803bb8600558aa79724aaeda324 - RNFBAppCheck: 09e2ddb622df967c033685530b2ea0edac82ef02 - RNFBAppDistribution: c7d9d0da5f06c467b24279a9afe3306e9e45e541 - RNFBAuth: bb963a4066d888e6a63b6baebb5c807229352874 - RNFBCrashlytics: 2cb008920d20ec4a1eac214d092a221ef6028005 - RNFBDatabase: 0b4250a531dd486c9d83e0414f0a61aea23363c1 - RNFBFirestore: 1ce7e962be7b416a9912f23b0f84700275e73f9f - RNFBFunctions: 6a9f506401392239c5077272341e9cb84a6180f4 - RNFBInAppMessaging: 6aa0f72eac9aeaf430725d1524d8d162b637aaf6 - RNFBInstallations: 7a362da9f5ddcadbecef937fea070f66430ae981 - RNFBMessaging: 1317e9e196aa8de3170c4adca18fb8b00ac80fed - RNFBML: ad2affe812fb1942c5b3715cf1b32e66e0206f32 - RNFBPerf: a6c706c6e0a08f6a22449f54d748f3469221fcd8 - RNFBRemoteConfig: 8cae1969044bc2b7bf96a17f422d107b33e1ab6b - RNFBStorage: b631bd93d97bac0dc625858160b0fef221f01a06 + RNFBAnalytics: e3a8428b52dd170c147aa583b1b8104ffc4366b8 + RNFBApp: 0f27e0ccf0121255f380cbd8f8c7f84533d12e81 + RNFBAppCheck: 8fed3940251ec37876e43d9eb81318f374f1a741 + RNFBAppDistribution: 0fd0d844cf3bbb735de0e85db5eef0c4f8fa8bfc + RNFBAuth: 1ac38c8d67d812b2d9520bab7c3583d152b12449 + RNFBCrashlytics: d5fedddb8ef41177ad919b7ca8f9828e84cff44e + RNFBDatabase: 2d026e610f2758b57bf507eef2bedd2d2651a1fe + RNFBFirestore: 1bbb553564696d1d1be07cae0aa63c279d1c339c + RNFBFunctions: 45cd16d61906f7a967720e5454e9e4cf2fc00956 + RNFBInAppMessaging: 2e5569895300e3e596d15fe7f77a245569a0ac7e + RNFBInstallations: 876cce8513482ae6b7bcf44f68782544c9c67ffe + RNFBMessaging: e9d29f70829162831d2b480b7acfb00bb5104e4c + RNFBML: b5aba98d94c9e6f1ac11962a4d525d8402e881a2 + RNFBPerf: 0956969f871da3054fe9267aa0e5d76d7793ca55 + RNFBRemoteConfig: 266d3f046153c1ae784249c5446a85bb21449142 + RNFBStorage: 709658ce12d83c05e51abe4235522906a18c4cc7 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: 6eb60fc2c0eef63e7d2ef4a56e0a3353534143a2 diff --git a/tests/local-tests/auth/auth-totp-demonstrator.tsx b/tests/local-tests/auth/auth-mfa-demonstrator.tsx similarity index 65% rename from tests/local-tests/auth/auth-totp-demonstrator.tsx rename to tests/local-tests/auth/auth-mfa-demonstrator.tsx index f505a787dc..785f093f45 100644 --- a/tests/local-tests/auth/auth-totp-demonstrator.tsx +++ b/tests/local-tests/auth/auth-mfa-demonstrator.tsx @@ -37,6 +37,8 @@ import { getMultiFactorResolver, multiFactor, onAuthStateChanged, + PhoneAuthProvider, + PhoneMultiFactorGenerator, reload, sendEmailVerification, signInWithEmailAndPassword, @@ -61,7 +63,7 @@ const Button = (props: { ); }; -export function AuthTOTPDemonstrator() { +export function AuthMFADemonstrator() { const [authReady, setAuthReady] = useState(false); const [user, setUser] = useState(null); @@ -187,7 +189,7 @@ const Login = () => { }; if (mfaError) { - return ; + return setMfaError(undefined)} />; } return ( @@ -225,37 +227,73 @@ const Login = () => { ); }; - -const MfaLogin = ({ error }: { error: FirebaseAuthTypes.MultiFactorError }) => { +const MfaLogin = ({ + error, + clearError, +}: { + error: FirebaseAuthTypes.MultiFactorError; + clearError: () => void; +}) => { const [resolver, setResolver] = useState(); const [activeFactor, setActiveFactor] = useState(); - + const [verificationId, setVerificationId] = useState(''); const [code, setCode] = useState(''); const [isLoading, setLoading] = useState(false); useEffect(() => { const resolver = getMultiFactorResolver(getAuth(), error); setResolver(resolver); - setActiveFactor(resolver.hints[0]); + console.log('Active factors: ' + JSON.stringify(resolver.hints)); + console.log('resolver.hints[0] is ' + JSON.stringify(resolver.hints[0])); if (resolver.hints.length === 1) { - const hint = resolver.hints[0]; - setActiveFactor(hint); + setActiveFactor(resolver.hints[0]); + console.log('activeFactor is ' + JSON.stringify(activeFactor)); + console.log('activeFactor.factorId is ' + JSON.stringify(activeFactor?.factorId)); } }, [error]); - const handleConfirm = async () => { + const requestCode = async () => { if (!resolver) return; try { setLoading(true); - // For demo, assume only 1 hint and it's totp - const multiFactorAssertion = TotpMultiFactorGenerator.assertionForSignIn( - activeFactor!.uid, - code, + setVerificationId( + await new PhoneAuthProvider(getAuth()).verifyPhoneNumber({ + multiFactorHint: activeFactor, + session: resolver.session, + }), ); + } catch (error) { + console.error('Error during MFA Phone code send:', error); + } finally { + setLoading(false); + } + }; + + const handleConfirm = async () => { + if (!resolver || !activeFactor) return; + + try { + setLoading(true); + let multiFactorAssertion: FirebaseAuthTypes.MultiFactorAssertion; + switch (activeFactor.factorId) { + case 'totp': + multiFactorAssertion = TotpMultiFactorGenerator.assertionForSignIn( + activeFactor!.uid, + code, + ); + break; + case 'phone': + const phoneAuthCredential = new PhoneAuthProvider.credential(verificationId, code); + multiFactorAssertion = PhoneMultiFactorGenerator.assertion(phoneAuthCredential); + break; + default: + throw new Error('Unknown MFA factor type: ' + activeFactor.factorId); + } + return await resolver.resolveSignIn(multiFactorAssertion); } catch (error) { - console.error('Error during MFA sign in:', error); + console.error('Error during MFA TOTP sign in:', error); } finally { setLoading(false); } @@ -265,15 +303,57 @@ const MfaLogin = ({ error }: { error: FirebaseAuthTypes.MultiFactorError }) => { return null; } - // For demo, assume only 1 hint and it's totp + if (!activeFactor) { + return ( + + + MFA Factor Selection + + You have multiple second factors enrolled. Please select one. + + {resolver.hints?.map(factor => ( + + ))} + + + Sign Out + + + + ); + } + return ( - Two-Factor Authentication - - Please enter the verification code from your authenticator app - + {/* Show the TOTP code entry if that factor is selected */} + {activeFactor !== undefined && activeFactor.factorId === 'totp' && ( + <> + TOTP Two-Factor Authentication + + Please enter the verification code from your authenticator app + + + )} + {/* Show the Phone verify && code entry if that factor is selected */} + {activeFactor !== undefined && activeFactor.factorId === 'phone' && ( + <> + Phone Two-Factor Authentication + 1) Request SMS code + + + 2) enter the code, then Verify + + )} { + + {/* Allow user to change factor if more than one */} + {activeFactor && resolver.hints.length > 1 && ( + setActiveFactor(undefined)}> + Switch Factor + + )} + + + Sign Out + ); @@ -299,6 +390,7 @@ const Home = () => { const [factors, setFactors] = useState(getAuth().currentUser?.multiFactor?.enrolledFactors); const [addingFactor, setAddingFactor] = useState(false); const [removingFactor, setRemovingFactor] = useState(false); + const [addingPhoneFactor, setAddingPhoneFactor] = useState(false); const [totpSecret, setTotpSecret] = useState(null); @@ -336,11 +428,21 @@ const Home = () => { } }; + if (addingPhoneFactor) { + return ( + { + setFactors(getAuth().currentUser?.multiFactor?.enrolledFactors); + setAddingPhoneFactor(false); + }} + /> + ); + } + if (totpSecret) { return ( { setFactors(getAuth().currentUser?.multiFactor?.enrolledFactors); setTotpSecret(null); @@ -361,6 +463,7 @@ const Home = () => { {factors?.map(factor => ( ))} - + )} + + {factors?.find(factor => factor.factorId === 'phone') === undefined && ( + + )} + + signOut(getAuth())}> + Sign Out + + + + ); +}; + +const EnrollPhone = ({ onComplete }: { onComplete: () => void }) => { + const [waitingForPhoneVerification, setWaitingForPhoneVerification] = useState(false); + const [verificationCode, setVerificationCode] = useState(''); + const [verificationId, setVerificationId] = useState(''); + const [phoneNumber, setPhoneNumber] = useState(''); + const [isLoading, setLoading] = useState(false); + + const handleVerifyPhone = async () => { + setLoading(true); + setWaitingForPhoneVerification(true); + try { + const user = getAuth().currentUser; + if (!user) return; + + const session = await multiFactor(user).getSession(); + setVerificationId( + await new PhoneAuthProvider(getAuth()).verifyPhoneNumber({ + phoneNumber, + session, + }), + ); + } catch (error) { + console.error('Error sending phone verification:', error); + } finally { + setLoading(false); + } + }; + + const handleEnrollPhone = async () => { + setLoading(true); + try { + const user = getAuth().currentUser; + if (!user) return; + const cred = PhoneAuthProvider.credential(verificationId, verificationCode); + const multiFactorAssertion = PhoneMultiFactorGenerator.assertion(cred); + await multiFactor(user).enroll(multiFactorAssertion, 'Phone'); + onComplete(); + } catch (error) { + console.error('Error enrolling Phone:', error); + } finally { + setLoading(false); + setWaitingForPhoneVerification(false); + } + }; + + return ( + + + Enroll Phone + + 1) Enter phone # and press send code + + + 2) Enter the verification code received. + + + signOut(getAuth())}> diff --git a/tests/local-tests/index.js b/tests/local-tests/index.js index 125dafac0b..a128b70e17 100644 --- a/tests/local-tests/index.js +++ b/tests/local-tests/index.js @@ -26,7 +26,7 @@ import { AITestComponent } from './ai/ai'; import { DatabaseOnChildMovedTest } from './database'; import { FirestoreOnSnapshotInSyncTest } from './firestore/onSnapshotInSync'; import { VertexAITestComponent } from './vertexai/vertexai'; -import { AuthTOTPDemonstrator } from './auth/auth-totp-demonstrator'; +import { AuthTOTPDemonstrator } from './auth/auth-mfa-demonstrator'; const testComponents = { // List your imported components here... @@ -35,7 +35,7 @@ const testComponents = { 'Database onChildMoved Test': DatabaseOnChildMovedTest, 'Firestore onSnapshotInSync Test': FirestoreOnSnapshotInSyncTest, 'VertexAI Generation Example': VertexAITestComponent, - 'Auth TOTP Demonstrator': AuthTOTPDemonstrator, + 'Auth MFA Demonstrator': AuthMFADemonstrator, }; export function TestComponents() {