From bfd26954f80fdeb4308b95eab2aaa02bc95f3104 Mon Sep 17 00:00:00 2001 From: Guillaume Bernos Date: Mon, 18 Jul 2022 10:50:40 +0200 Subject: [PATCH] feat(auth, web): add phone mfa (#9031) * feat(auth): add multifactoruser * feat(auth, web): bridge * feat(auth, web): solve bridging errors * feat(auth, web): finish phone web mfa * feat(auth, web): finish phone web mfa * feat(auth, web): fix analyze * feat(auth, web): fix analyze * feat(auth, web): fix wrong argument --- .../firebase_auth/example/lib/auth.dart | 1 - .../firebase_auth/example/lib/profile.dart | 1 - .../firebase_auth/lib/src/firebase_auth.dart | 2 +- .../firebase_auth/lib/src/multi_factor.dart | 13 +- .../lib/src/recaptcha_verifier.dart | 2 + .../firebase_auth/lib/src/user.dart | 2 +- .../method_channel_multi_factor.dart | 42 ++++- .../platform_interface_multi_factor.dart | 56 ++++-- ..._interface_recaptcha_verifier_factory.dart | 1 + ...rface_recaptcha_verifier_factory_test.dart | 4 +- .../lib/firebase_auth_web.dart | 112 ++++++++---- ...firebase_auth_web_confirmation_result.dart | 3 +- .../src/firebase_auth_web_multi_factor.dart | 135 ++++++++++++++- ...e_auth_web_recaptcha_verifier_factory.dart | 27 ++- .../firebase_auth_web_user_credential.dart | 6 +- .../lib/src/interop/auth.dart | 37 ++-- .../lib/src/interop/auth_interop.dart | 118 ++++++++++++- .../lib/src/interop/multi_factor.dart | 163 ++++++++++++++++++ .../lib/src/interop/utils/utils.dart | 7 +- .../lib/src/utils/web_utils.dart | 66 ++++++- .../firebase_auth_multi_factor_e2e.dart | 1 - 21 files changed, 688 insertions(+), 111 deletions(-) create mode 100644 packages/firebase_auth/firebase_auth_web/lib/src/interop/multi_factor.dart diff --git a/packages/firebase_auth/firebase_auth/example/lib/auth.dart b/packages/firebase_auth/firebase_auth/example/lib/auth.dart index 840fb2e6dbdf..1574203c2f52 100644 --- a/packages/firebase_auth/firebase_auth/example/lib/auth.dart +++ b/packages/firebase_auth/firebase_auth/example/lib/auth.dart @@ -2,7 +2,6 @@ import 'dart:io'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_auth_example/config.dart'; -import 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; diff --git a/packages/firebase_auth/firebase_auth/example/lib/profile.dart b/packages/firebase_auth/firebase_auth/example/lib/profile.dart index c79017adf03c..7c9d430e0459 100644 --- a/packages/firebase_auth/firebase_auth/example/lib/profile.dart +++ b/packages/firebase_auth/firebase_auth/example/lib/profile.dart @@ -1,7 +1,6 @@ import 'dart:developer'; import 'package:firebase_auth/firebase_auth.dart'; -import 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart'; import 'package:flutter/material.dart'; import 'package:google_sign_in/google_sign_in.dart'; diff --git a/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart b/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart index 43f24ad30976..742399cfe156 100644 --- a/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart +++ b/packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart @@ -607,7 +607,7 @@ class FirebaseAuth extends FirebasePluginPlatform { // If we add a recaptcha to the page by creating a new instance, we must // also clear that instance before proceeding. bool mustClear = verifier == null; - verifier ??= RecaptchaVerifier(); + verifier ??= RecaptchaVerifier(auth: _delegate); final result = await _delegate.signInWithPhoneNumber(phoneNumber, verifier.delegate); if (mustClear) { diff --git a/packages/firebase_auth/firebase_auth/lib/src/multi_factor.dart b/packages/firebase_auth/firebase_auth/lib/src/multi_factor.dart index 5ff7899d6186..4fa685133f85 100644 --- a/packages/firebase_auth/firebase_auth/lib/src/multi_factor.dart +++ b/packages/firebase_auth/firebase_auth/lib/src/multi_factor.dart @@ -17,7 +17,7 @@ class MultiFactor { /// /// [displayName] can be used to provide a display name for the second factor. Future enroll( - MultiFactorAssertion assertion, { + MultiFactorAssertionPlatform assertion, { String? displayName, }) async { return _delegate.enroll(assertion, displayName: displayName); @@ -40,3 +40,14 @@ class MultiFactor { return _delegate.getEnrolledFactors(); } } + +/// Provider for generating a PhoneMultiFactorAssertion. +class PhoneMultiFactorGenerator { + /// Transforms a PhoneAuthCredential into a [MultiFactorAssertion] + /// which can be used to confirm ownership of a phone second factor. + static MultiFactorAssertionPlatform getAssertion( + PhoneAuthCredential credential, + ) { + return PhoneMultiFactorGeneratorPlatform.instance.getAssertion(credential); + } +} diff --git a/packages/firebase_auth/firebase_auth/lib/src/recaptcha_verifier.dart b/packages/firebase_auth/firebase_auth/lib/src/recaptcha_verifier.dart index 9721757c622e..7aa4bbf74d2b 100644 --- a/packages/firebase_auth/firebase_auth/lib/src/recaptcha_verifier.dart +++ b/packages/firebase_auth/firebase_auth/lib/src/recaptcha_verifier.dart @@ -46,6 +46,7 @@ class RecaptchaVerifier { /// /// [onExpired] An optional callback which is called when the reCAPTCHA expires. factory RecaptchaVerifier({ + required FirebaseAuthPlatform auth, String? container, RecaptchaVerifierSize size = RecaptchaVerifierSize.normal, RecaptchaVerifierTheme theme = RecaptchaVerifierTheme.light, @@ -55,6 +56,7 @@ class RecaptchaVerifier { }) { return RecaptchaVerifier._( _factory.delegateFor( + auth: auth, container: container, size: size, theme: theme, diff --git a/packages/firebase_auth/firebase_auth/lib/src/user.dart b/packages/firebase_auth/firebase_auth/lib/src/user.dart index a9c7d4d1b670..bedd87541445 100644 --- a/packages/firebase_auth/firebase_auth/lib/src/user.dart +++ b/packages/firebase_auth/firebase_auth/lib/src/user.dart @@ -262,7 +262,7 @@ class User { // If we add a recaptcha to the page by creating a new instance, we must // also clear that instance before proceeding. bool mustClear = verifier == null; - verifier ??= RecaptchaVerifier(); + verifier ??= RecaptchaVerifier(auth: _delegate.auth); final result = await _delegate.linkWithPhoneNumber(phoneNumber, verifier.delegate); if (mustClear) { diff --git a/packages/firebase_auth/firebase_auth_platform_interface/lib/src/method_channel/method_channel_multi_factor.dart b/packages/firebase_auth/firebase_auth_platform_interface/lib/src/method_channel/method_channel_multi_factor.dart index d632b8d5bbfe..2012cb5a846f 100644 --- a/packages/firebase_auth/firebase_auth_platform_interface/lib/src/method_channel/method_channel_multi_factor.dart +++ b/packages/firebase_auth/firebase_auth_platform_interface/lib/src/method_channel/method_channel_multi_factor.dart @@ -18,11 +18,13 @@ class MethodChannelMultiFactor extends MultiFactorPlatform { @override Future enroll( - MultiFactorAssertion assertion, { + MultiFactorAssertionPlatform assertion, { String? displayName, }) async { - if (assertion.credential is PhoneAuthCredential) { - final credential = assertion.credential as PhoneAuthCredential; + final _assertion = assertion as MultiFactorAssertion; + + if (_assertion.credential is PhoneAuthCredential) { + final credential = _assertion.credential as PhoneAuthCredential; final verificationId = credential.verificationId; final verificationCode = credential.smsCode; @@ -43,7 +45,7 @@ class MethodChannelMultiFactor extends MultiFactorPlatform { ); } else { throw UnimplementedError( - 'Credential type ${assertion.credential} is not supported yet', + 'Credential type ${_assertion.credential} is not supported yet', ); } } @@ -90,10 +92,12 @@ class MethodChannelMultiFactorResolver extends MultiFactorResolverPlatform { @override Future resolveSignIn( - MultiFactorAssertion assertion, + MultiFactorAssertionPlatform assertion, ) async { - if (assertion.credential is PhoneAuthCredential) { - final credential = assertion.credential as PhoneAuthCredential; + final _assertion = assertion as MultiFactorAssertion; + + if (_assertion.credential is PhoneAuthCredential) { + final credential = _assertion.credential as PhoneAuthCredential; final verificationId = credential.verificationId; final verificationCode = credential.smsCode; @@ -118,8 +122,30 @@ class MethodChannelMultiFactorResolver extends MultiFactorResolverPlatform { return userCredential; } else { throw UnimplementedError( - 'Credential type ${assertion.credential} is not supported yet', + 'Credential type ${_assertion.credential} is not supported yet', ); } } } + +/// Represents an assertion that the Firebase Authentication server +/// can use to authenticate a user as part of a multi-factor flow. +class MultiFactorAssertion extends MultiFactorAssertionPlatform { + MultiFactorAssertion(this.credential) : super(); + + /// Associated credential to the assertion + final AuthCredential credential; +} + +/// Helper class used to generate PhoneMultiFactorAssertions. +class MethodChannelPhoneMultiFactorGenerator + extends PhoneMultiFactorGeneratorPlatform { + /// Transforms a PhoneAuthCredential into a [MultiFactorAssertion] + /// which can be used to confirm ownership of a phone second factor. + @override + MultiFactorAssertionPlatform getAssertion( + PhoneAuthCredential credential, + ) { + return MultiFactorAssertion(credential); + } +} diff --git a/packages/firebase_auth/firebase_auth_platform_interface/lib/src/platform_interface/platform_interface_multi_factor.dart b/packages/firebase_auth/firebase_auth_platform_interface/lib/src/platform_interface/platform_interface_multi_factor.dart index bec8ce78438b..90a9537e74ec 100644 --- a/packages/firebase_auth/firebase_auth_platform_interface/lib/src/platform_interface/platform_interface_multi_factor.dart +++ b/packages/firebase_auth/firebase_auth_platform_interface/lib/src/platform_interface/platform_interface_multi_factor.dart @@ -1,4 +1,5 @@ import 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart'; +import 'package:firebase_auth_platform_interface/src/method_channel/method_channel_multi_factor.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; /// {@template .platformInterfaceMultiFactor} @@ -19,7 +20,10 @@ abstract class MultiFactorPlatform extends PlatformInterface { /// Enrolls a second factor as identified by the [MultiFactorAssertion] parameter for the current user. /// /// [displayName] can be used to provide a display name for the second factor. - Future enroll(MultiFactorAssertion assertion, {String? displayName}) { + Future enroll( + MultiFactorAssertionPlatform assertion, { + String? displayName, + }) { throw UnimplementedError('enroll() is not implemented'); } @@ -54,21 +58,7 @@ class MultiFactorSession { /// Represents an assertion that the Firebase Authentication server /// can use to authenticate a user as part of a multi-factor flow. -class MultiFactorAssertion { - const MultiFactorAssertion._(this.credential); - - /// Associated credential to the assertion - final AuthCredential credential; -} - -/// Helper class used to generate PhoneMultiFactorAssertions. -class PhoneMultiFactorGenerator { - /// Transforms a PhoneAuthCredential into a [MultiFactorAssertion] - /// which can be used to confirm ownership of a phone second factor. - static MultiFactorAssertion getAssertion(PhoneAuthCredential credential) { - return MultiFactorAssertion._(credential); - } -} +class MultiFactorAssertionPlatform {} /// {@macro .platformInterfaceMultiFactorResolverPlatform} /// Utility class that contains methods to resolve second factor @@ -91,7 +81,7 @@ class MultiFactorResolverPlatform { /// Completes sign in with a second factor using an MultiFactorAssertion which /// confirms that the user has successfully completed the second factor challenge. Future resolveSignIn( - MultiFactorAssertion assertion, + MultiFactorAssertionPlatform assertion, ) { throw UnimplementedError('resolveSignIn() is not implemented'); } @@ -139,3 +129,35 @@ class PhoneMultiFactorInfo extends MultiFactorInfo { /// The phone number associated with this second factor verification method. final String phoneNumber; } + +/// Helper class used to generate PhoneMultiFactorAssertions. +class PhoneMultiFactorGeneratorPlatform extends PlatformInterface { + static PhoneMultiFactorGeneratorPlatform? _instance; + + static final Object _token = Object(); + + PhoneMultiFactorGeneratorPlatform() : super(token: _token); + + /// The current default [PhoneMultiFactorGeneratorPlatform] instance. + /// + /// It will always default to [MethodChannelPhoneMultiFactorGenerator] + /// if no other implementation was provided. + static PhoneMultiFactorGeneratorPlatform get instance { + _instance ??= MethodChannelPhoneMultiFactorGenerator(); + return _instance!; + } + + /// Sets the [PhoneMultiFactorGeneratorPlatform.instance] + static set instance(PhoneMultiFactorGeneratorPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + /// Transforms a PhoneAuthCredential into a [MultiFactorAssertion] + /// which can be used to confirm ownership of a phone second factor. + MultiFactorAssertionPlatform getAssertion( + PhoneAuthCredential credential, + ) { + throw UnimplementedError('getAssertion() is not implemented'); + } +} diff --git a/packages/firebase_auth/firebase_auth_platform_interface/lib/src/platform_interface/platform_interface_recaptcha_verifier_factory.dart b/packages/firebase_auth/firebase_auth_platform_interface/lib/src/platform_interface/platform_interface_recaptcha_verifier_factory.dart index 4e802f497899..90cc8a963fce 100644 --- a/packages/firebase_auth/firebase_auth_platform_interface/lib/src/platform_interface/platform_interface_recaptcha_verifier_factory.dart +++ b/packages/firebase_auth/firebase_auth_platform_interface/lib/src/platform_interface/platform_interface_recaptcha_verifier_factory.dart @@ -80,6 +80,7 @@ abstract class RecaptchaVerifierFactoryPlatform extends PlatformInterface { /// Underlying implementations can use this method to create the underlying /// implementation of a Recaptcha Verifier. RecaptchaVerifierFactoryPlatform delegateFor({ + required FirebaseAuthPlatform auth, String? container, RecaptchaVerifierSize size = RecaptchaVerifierSize.normal, RecaptchaVerifierTheme theme = RecaptchaVerifierTheme.light, diff --git a/packages/firebase_auth/firebase_auth_platform_interface/test/platform_interface_tests/platform_interface_recaptcha_verifier_factory_test.dart b/packages/firebase_auth/firebase_auth_platform_interface/test/platform_interface_tests/platform_interface_recaptcha_verifier_factory_test.dart index 9b740357947e..9dec1897780f 100644 --- a/packages/firebase_auth/firebase_auth_platform_interface/test/platform_interface_tests/platform_interface_recaptcha_verifier_factory_test.dart +++ b/packages/firebase_auth/firebase_auth_platform_interface/test/platform_interface_tests/platform_interface_recaptcha_verifier_factory_test.dart @@ -68,7 +68,9 @@ void main() { group('delegateFor()', () { test('throws UnimplementedError error', () async { try { - recaptchaVerifierFactoryPlatform.delegateFor(); + recaptchaVerifierFactoryPlatform.delegateFor( + auth: FirebaseAuthPlatform.instance, + ); } on UnimplementedError catch (e) { expect(e.message, equals('delegateFor() is not implemented')); return; diff --git a/packages/firebase_auth/firebase_auth_web/lib/firebase_auth_web.dart b/packages/firebase_auth/firebase_auth_web/lib/firebase_auth_web.dart index 79ad321f5524..006f676a0391 100644 --- a/packages/firebase_auth/firebase_auth_web/lib/firebase_auth_web.dart +++ b/packages/firebase_auth/firebase_auth_web/lib/firebase_auth_web.dart @@ -7,6 +7,7 @@ import 'dart:async'; import 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart'; import 'package:firebase_auth_web/src/firebase_auth_web_multi_factor.dart'; +import 'package:firebase_auth_web/src/interop/utils/utils.dart'; import 'package:firebase_auth_web/src/utils/web_utils.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core_web/firebase_core_web.dart'; @@ -19,6 +20,7 @@ import 'src/firebase_auth_web_recaptcha_verifier_factory.dart'; import 'src/firebase_auth_web_user.dart'; import 'src/firebase_auth_web_user_credential.dart'; import 'src/interop/auth.dart' as auth_interop; +import 'src/interop/multi_factor.dart' as multi_factor; /// The web delegate implementation for [FirebaseAuth]. class FirebaseAuthWeb extends FirebaseAuthPlatform { @@ -41,7 +43,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { StreamController.broadcast(); // TODO(rrousselGit): close StreamSubscription - _delegate.onAuthStateChanged.map((auth_interop.User? webUser) { + delegate.onAuthStateChanged.map((auth_interop.User? webUser) { if (!_initialized.isCompleted) { _initialized.complete(); } @@ -49,7 +51,8 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { if (webUser == null) { return null; } else { - return UserWeb(this, MultiFactorWeb(this), webUser); + return UserWeb(this, + MultiFactorWeb(this, multi_factor.multiFactor(webUser)), webUser); } }).listen((UserWeb? webUser) { _authStateChangesListeners[app.name]!.add(webUser); @@ -57,11 +60,12 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { // TODO(rrousselGit): close StreamSubscription // Also triggers `userChanged` events - _delegate.onIdTokenChanged.map((auth_interop.User? webUser) { + delegate.onIdTokenChanged.map((auth_interop.User? webUser) { if (webUser == null) { return null; } else { - return UserWeb(this, MultiFactorWeb(this), webUser); + return UserWeb(this, + MultiFactorWeb(this, multi_factor.multiFactor(webUser)), webUser); } }).listen((UserWeb? webUser) { _idTokenChangesListeners[app.name]!.add(webUser); @@ -73,6 +77,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { static void registerWith(Registrar registrar) { FirebaseCoreWeb.registerService('auth'); FirebaseAuthPlatform.instance = FirebaseAuthWeb.instance; + PhoneMultiFactorGeneratorPlatform.instance = PhoneMultiFactorGeneratorWeb(); RecaptchaVerifierFactoryPlatform.instance = RecaptchaVerifierFactoryWeb.instance; } @@ -94,7 +99,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { /// instance of Auth from the web plugin auth_interop.Auth? _webAuth; - auth_interop.Auth get _delegate { + auth_interop.Auth get delegate { return _webAuth ??= auth_interop.getAuthInstance(core_interop.app(app.name)); } @@ -115,23 +120,26 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { @override UserPlatform? get currentUser { - auth_interop.User? webCurrentUser = _delegate.currentUser; + auth_interop.User? webCurrentUser = delegate.currentUser; if (webCurrentUser == null) { return null; } - return UserWeb(this, MultiFactorWeb(this), _delegate.currentUser!); + return UserWeb( + this, + MultiFactorWeb(this, multi_factor.multiFactor(delegate.currentUser!)), + delegate.currentUser!); } @override String? get tenantId { - return _delegate.tenantId; + return delegate.tenantId; } @override set tenantId(String? tenantId) { - _delegate.tenantId = tenantId; + delegate.tenantId = tenantId; } @override @@ -144,7 +152,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { @override Future applyActionCode(String code) async { try { - await _delegate.applyActionCode(code); + await delegate.applyActionCode(code); } catch (e) { throw getFirebaseAuthException(e); } @@ -153,7 +161,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { @override Future checkActionCode(String code) async { try { - return convertWebActionCodeInfo(await _delegate.checkActionCode(code))!; + return convertWebActionCodeInfo(await delegate.checkActionCode(code))!; } catch (e) { throw getFirebaseAuthException(e); } @@ -162,7 +170,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { @override Future confirmPasswordReset(String code, String newPassword) async { try { - await _delegate.confirmPasswordReset(code, newPassword); + await delegate.confirmPasswordReset(code, newPassword); } catch (e) { throw getFirebaseAuthException(e); } @@ -174,7 +182,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { try { return UserCredentialWeb( this, - await _delegate.createUserWithEmailAndPassword(email, password), + await delegate.createUserWithEmailAndPassword(email, password), ); } catch (e) { throw getFirebaseAuthException(e); @@ -184,7 +192,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { @override Future> fetchSignInMethodsForEmail(String email) async { try { - return await _delegate.fetchSignInMethodsForEmail(email); + return await delegate.fetchSignInMethodsForEmail(email); } catch (e) { throw getFirebaseAuthException(e); } @@ -193,7 +201,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { @override Future getRedirectResult() async { try { - return UserCredentialWeb(this, await _delegate.getRedirectResult()); + return UserCredentialWeb(this, await delegate.getRedirectResult()); } catch (e) { throw getFirebaseAuthException(e); } @@ -226,7 +234,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { ActionCodeSettings? actionCodeSettings, ]) async { try { - await _delegate.sendPasswordResetEmail( + await delegate.sendPasswordResetEmail( email, convertPlatformActionCodeSettings(actionCodeSettings)); } catch (e) { throw getFirebaseAuthException(e); @@ -239,7 +247,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { ActionCodeSettings? actionCodeSettings, ]) async { try { - await _delegate.sendSignInLinkToEmail( + await delegate.sendSignInLinkToEmail( email, convertPlatformActionCodeSettings(actionCodeSettings)); } catch (e) { throw getFirebaseAuthException(e); @@ -248,12 +256,12 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { @override String get languageCode { - return _delegate.languageCode; + return delegate.languageCode; } @override Future setLanguageCode(String? languageCode) async { - _delegate.languageCode = languageCode; + delegate.languageCode = languageCode; } @override @@ -264,14 +272,14 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { String? smsCode, bool? forceRecaptchaFlow, }) async { - _delegate.settings.appVerificationDisabledForTesting = + delegate.settings.appVerificationDisabledForTesting = appVerificationDisabledForTesting; } @override Future setPersistence(Persistence persistence) async { try { - return _delegate.setPersistence(persistence); + return delegate.setPersistence(persistence); } catch (e) { throw getFirebaseAuthException(e); } @@ -280,7 +288,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { @override Future signInAnonymously() async { try { - return UserCredentialWeb(this, await _delegate.signInAnonymously()); + return UserCredentialWeb(this, await delegate.signInAnonymously()); } catch (e) { throw getFirebaseAuthException(e); } @@ -292,7 +300,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { try { return UserCredentialWeb( this, - await _delegate + await delegate .signInWithCredential(convertPlatformCredential(credential)!)); } catch (e) { throw getFirebaseAuthException(e); @@ -303,7 +311,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { Future signInWithCustomToken(String token) async { try { return UserCredentialWeb( - this, await _delegate.signInWithCustomToken(token)); + this, await delegate.signInWithCustomToken(token)); } catch (e) { throw getFirebaseAuthException(e); } @@ -314,9 +322,9 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { String email, String password) async { try { return UserCredentialWeb( - this, await _delegate.signInWithEmailAndPassword(email, password)); + this, await delegate.signInWithEmailAndPassword(email, password)); } catch (e) { - throw getFirebaseAuthException(e); + throw getFirebaseAuthException(e, _webAuth); } } @@ -325,7 +333,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { String email, String emailLink) async { try { return UserCredentialWeb( - this, await _delegate.signInWithEmailLink(email, emailLink)); + this, await delegate.signInWithEmailLink(email, emailLink)); } catch (e) { throw getFirebaseAuthException(e); } @@ -341,7 +349,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { auth_interop.RecaptchaVerifier verifier = applicationVerifier.delegate; return ConfirmationResultWeb( - this, await _delegate.signInWithPhoneNumber(phoneNumber, verifier)); + this, await delegate.signInWithPhoneNumber(phoneNumber, verifier)); } catch (e) { throw getFirebaseAuthException(e); } @@ -352,7 +360,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { try { return UserCredentialWeb( this, - await _delegate.signInWithPopup(convertPlatformAuthProvider(provider)), + await delegate.signInWithPopup(convertPlatformAuthProvider(provider)), ); } catch (e) { throw getFirebaseAuthException(e); @@ -362,8 +370,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { @override Future signInWithRedirect(AuthProvider provider) async { try { - return _delegate - .signInWithRedirect(convertPlatformAuthProvider(provider)); + return delegate.signInWithRedirect(convertPlatformAuthProvider(provider)); } catch (e) { throw getFirebaseAuthException(e); } @@ -372,7 +379,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { @override Future signOut() async { try { - await _delegate.signOut(); + await delegate.signOut(); } catch (e) { throw getFirebaseAuthException(e); } @@ -384,7 +391,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { // The generic platform interface is with host and port split to // centralize logic between android/ios native, but web takes the // origin as a single string - _delegate.useAuthEmulator('http://$host:$port'); + delegate.useAuthEmulator('http://$host:$port'); } catch (e) { throw getFirebaseAuthException(e); } @@ -393,7 +400,7 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { @override Future verifyPasswordResetCode(String code) async { try { - return await _delegate.verifyPasswordResetCode(code); + return await delegate.verifyPasswordResetCode(code); } catch (e) { throw getFirebaseAuthException(e); } @@ -411,8 +418,39 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform { Duration timeout = const Duration(seconds: 30), int? forceResendingToken, MultiFactorSession? multiFactorSession, - }) { - throw UnimplementedError( - 'verifyPhoneNumber() is not supported on the web. Please use `signInWithPhoneNumber` instead.'); + }) async { + try { + Map? data; + if (multiFactorSession != null) { + final _webMultiFactorSession = + multiFactorSession as MultiFactorSessionWeb; + if (multiFactorInfo != null) { + data = { + 'multiFactorUid': multiFactorInfo.uid, + 'session': _webMultiFactorSession.webSession.jsObject, + }; + } else { + data = { + 'phoneNumber': phoneNumber, + 'session': _webMultiFactorSession.webSession.jsObject, + }; + } + } + + final phoneOptions = (data ?? phoneNumber)!; + + final provider = auth_interop.PhoneAuthProvider(_webAuth); + final verifier = RecaptchaVerifierFactoryWeb( + auth: this, + ).delegate; + + /// We add the passthrough method for LegacyJsObject + final verificationId = await provider.verifyPhoneNumber( + jsify(phoneOptions, (object) => object), verifier); + + codeSent(verificationId, null); + } catch (e) { + verificationFailed(getFirebaseAuthException(e)); + } } } diff --git a/packages/firebase_auth/firebase_auth_web/lib/src/firebase_auth_web_confirmation_result.dart b/packages/firebase_auth/firebase_auth_web/lib/src/firebase_auth_web_confirmation_result.dart index 68ace46db4ce..80fbf1d6cd3e 100644 --- a/packages/firebase_auth/firebase_auth_web/lib/src/firebase_auth_web_confirmation_result.dart +++ b/packages/firebase_auth/firebase_auth_web/lib/src/firebase_auth_web_confirmation_result.dart @@ -5,9 +5,10 @@ import 'dart:async'; -import 'interop/auth.dart' as auth_interop; import 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart'; import 'package:firebase_auth_web/src/firebase_auth_web_user_credential.dart'; + +import 'interop/auth.dart' as auth_interop; import 'utils/web_utils.dart'; /// The web delegate implementation for [ConfirmationResultPlatform]. diff --git a/packages/firebase_auth/firebase_auth_web/lib/src/firebase_auth_web_multi_factor.dart b/packages/firebase_auth/firebase_auth_web/lib/src/firebase_auth_web_multi_factor.dart index 2d24bdc4c15d..d4ec8846d8a0 100644 --- a/packages/firebase_auth/firebase_auth_web/lib/src/firebase_auth_web_multi_factor.dart +++ b/packages/firebase_auth/firebase_auth_web/lib/src/firebase_auth_web_multi_factor.dart @@ -4,8 +4,141 @@ // found in the LICENSE file. import 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart'; +import 'package:firebase_auth_web/firebase_auth_web.dart'; +import 'package:firebase_auth_web/src/firebase_auth_web_user_credential.dart'; + +import 'interop/auth.dart' as auth; +import 'interop/multi_factor.dart' as multi_factor_interop; +import 'utils/web_utils.dart'; /// Web delegate implementation of [UserPlatform]. class MultiFactorWeb extends MultiFactorPlatform { - MultiFactorWeb(FirebaseAuthPlatform auth) : super(auth); + MultiFactorWeb(FirebaseAuthPlatform auth, this._webMultiFactorUser) + : super(auth); + + final multi_factor_interop.MultiFactorUser _webMultiFactorUser; + + @override + Future getSession() async { + try { + return convertMultiFactorSession(await _webMultiFactorUser.session); + } catch (e) { + throw getFirebaseAuthException(e); + } + } + + @override + Future enroll( + MultiFactorAssertionPlatform assertion, { + String? displayName, + }) async { + try { + final webAssertion = assertion as MultiFactorAssertionWeb; + return await _webMultiFactorUser.enroll( + webAssertion.assertion, + displayName, + ); + } catch (e) { + throw getFirebaseAuthException(e); + } + } + + @override + Future unenroll({ + String? factorUid, + MultiFactorInfo? multiFactorInfo, + }) { + final uidToUnenroll = factorUid ?? multiFactorInfo?.uid; + if (uidToUnenroll == null) { + throw ArgumentError( + 'Either factorUid or multiFactorInfo must not be null', + ); + } + + return _webMultiFactorUser.unenroll( + uidToUnenroll, + ); + } + + @override + Future> getEnrolledFactors() async { + final data = _webMultiFactorUser.enrolledFactors; + return data + .map((e) => MultiFactorInfo( + factorId: e.factorId, + enrollmentTimestamp: + DateTime.parse(e.enrollmentTime).millisecondsSinceEpoch / + 1000, + displayName: e.displayName, + uid: e.uid, + )) + .toList(); + } +} + +class MultiFactorAssertionWeb extends MultiFactorAssertionPlatform { + MultiFactorAssertionWeb( + this.assertion, + ) : super(); + + final multi_factor_interop.MultiFactorAssertion assertion; +} + +class MultiFactorResolverWeb extends MultiFactorResolverPlatform { + MultiFactorResolverWeb( + List hints, + MultiFactorSession session, + this._auth, + this._webMultiFactorResolver, + ) : super(hints, session); + + final multi_factor_interop.MultiFactorResolver _webMultiFactorResolver; + final FirebaseAuthWeb _auth; + + @override + Future resolveSignIn( + MultiFactorAssertionPlatform assertion, + ) async { + final webAssertion = assertion as MultiFactorAssertionWeb; + + return UserCredentialWeb( + _auth, + await _webMultiFactorResolver.resolveSignIn(webAssertion.assertion), + ); + } +} + +class MultiFactorSessionWeb extends MultiFactorSession { + MultiFactorSessionWeb( + String id, + this.webSession, + ) : super(id); + + final multi_factor_interop.MultiFactorSession webSession; +} + +/// Helper class used to generate PhoneMultiFactorAssertions. +class PhoneMultiFactorGeneratorWeb extends PhoneMultiFactorGeneratorPlatform { + /// Transforms a PhoneAuthCredential into a [MultiFactorAssertion] + /// which can be used to confirm ownership of a phone second factor. + @override + MultiFactorAssertionPlatform getAssertion( + PhoneAuthCredential credential, + ) { + final verificationId = credential.verificationId; + final verificationCode = credential.smsCode; + + if (verificationCode == null) { + throw ArgumentError('verificationCode must not be null'); + } + if (verificationId == null) { + throw ArgumentError('verificationId must not be null'); + } + + final cred = + auth.PhoneAuthProvider.credential(verificationId, verificationCode); + + return MultiFactorAssertionWeb( + multi_factor_interop.PhoneMultiFactorGenerator.assertion(cred)); + } } diff --git a/packages/firebase_auth/firebase_auth_web/lib/src/firebase_auth_web_recaptcha_verifier_factory.dart b/packages/firebase_auth/firebase_auth_web/lib/src/firebase_auth_web_recaptcha_verifier_factory.dart index 2dca05bcc5d5..73a475a7bd55 100644 --- a/packages/firebase_auth/firebase_auth_web/lib/src/firebase_auth_web_recaptcha_verifier_factory.dart +++ b/packages/firebase_auth/firebase_auth_web/lib/src/firebase_auth_web_recaptcha_verifier_factory.dart @@ -6,8 +6,10 @@ import 'dart:async'; import 'dart:html'; -import 'interop/auth.dart' as auth_interop; import 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart'; +import 'package:firebase_auth_web/firebase_auth_web.dart'; + +import 'interop/auth.dart' as auth_interop; import 'utils/web_utils.dart'; const String _kInvisibleElementId = '__ff-recaptcha-container'; @@ -32,6 +34,7 @@ class RecaptchaVerifierFactoryWeb extends RecaptchaVerifierFactoryPlatform { /// Creates a new [RecaptchaVerifierFactoryWeb] with a container and parameters. RecaptchaVerifierFactoryWeb({ + required FirebaseAuthWeb auth, String? container, RecaptchaVerifierSize size = RecaptchaVerifierSize.normal, RecaptchaVerifierTheme theme = RecaptchaVerifierTheme.light, @@ -86,11 +89,16 @@ class RecaptchaVerifierFactoryWeb extends RecaptchaVerifierFactoryPlatform { element = container; } - _delegate = auth_interop.RecaptchaVerifier(element, parameters); + _delegate = auth_interop.RecaptchaVerifier( + element, + parameters, + auth.delegate, + ); } @override RecaptchaVerifierFactoryPlatform delegateFor({ + required FirebaseAuthPlatform auth, String? container, RecaptchaVerifierSize size = RecaptchaVerifierSize.normal, RecaptchaVerifierTheme theme = RecaptchaVerifierTheme.light, @@ -98,13 +106,16 @@ class RecaptchaVerifierFactoryWeb extends RecaptchaVerifierFactoryPlatform { RecaptchaVerifierOnError? onError, RecaptchaVerifierOnExpired? onExpired, }) { + final _webAuth = auth as FirebaseAuthWeb; return RecaptchaVerifierFactoryWeb( - container: container, - size: size, - theme: theme, - onSuccess: onSuccess, - onError: onError, - onExpired: onExpired); + auth: _webAuth, + container: container, + size: size, + theme: theme, + onSuccess: onSuccess, + onError: onError, + onExpired: onExpired, + ); } @override diff --git a/packages/firebase_auth/firebase_auth_web/lib/src/firebase_auth_web_user_credential.dart b/packages/firebase_auth/firebase_auth_web/lib/src/firebase_auth_web_user_credential.dart index 569a628a130f..2c88b54c791b 100644 --- a/packages/firebase_auth/firebase_auth_web/lib/src/firebase_auth_web_user_credential.dart +++ b/packages/firebase_auth/firebase_auth_web/lib/src/firebase_auth_web_user_credential.dart @@ -8,6 +8,7 @@ import 'package:firebase_auth_web/src/firebase_auth_web_multi_factor.dart'; import 'firebase_auth_web_user.dart'; import 'interop/auth.dart' as auth_interop; +import 'interop/multi_factor.dart'; import 'utils/web_utils.dart'; /// Web delegate implementation of [UserCredentialPlatform]. @@ -22,6 +23,9 @@ class UserCredentialWeb extends UserCredentialPlatform { webUserCredential.additionalUserInfo, ), credential: convertWebOAuthCredential(webUserCredential.credential), - user: UserWeb(auth, MultiFactorWeb(auth), webUserCredential.user!), + user: UserWeb( + auth, + MultiFactorWeb(auth, multiFactor(webUserCredential.user!)), + webUserCredential.user!), ); } diff --git a/packages/firebase_auth/firebase_auth_web/lib/src/interop/auth.dart b/packages/firebase_auth/firebase_auth_web/lib/src/interop/auth.dart index 7b61a8d8049e..229e7748b290 100644 --- a/packages/firebase_auth/firebase_auth_web/lib/src/interop/auth.dart +++ b/packages/firebase_auth/firebase_auth_web/lib/src/interop/auth.dart @@ -9,10 +9,11 @@ import 'dart:async'; import 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart'; -import 'package:http_parser/http_parser.dart'; -import 'package:js/js.dart'; import 'package:firebase_core_web/firebase_core_web_interop.dart' hide jsify, dartify; +import 'package:http_parser/http_parser.dart'; +import 'package:js/js.dart'; + import 'auth_interop.dart' as auth_interop; import 'utils/utils.dart'; @@ -908,7 +909,7 @@ class PhoneAuthProvider /// Creates a new PhoneAuthProvider with the optional [Auth] instance /// in which sign-ins should occur. factory PhoneAuthProvider([Auth? auth]) => - PhoneAuthProvider.fromJsObject((auth != null) + PhoneAuthProvider.fromJsObject(auth != null ? auth_interop.PhoneAuthProviderJsImpl(auth.jsObject) : auth_interop.PhoneAuthProviderJsImpl()); @@ -923,17 +924,17 @@ class PhoneAuthProvider /// /// For abuse prevention, this method also requires an [ApplicationVerifier]. Future verifyPhoneNumber( - String phoneNumber, ApplicationVerifier applicationVerifier) => + dynamic phoneOptions, ApplicationVerifier applicationVerifier) => handleThenable(jsObject.verifyPhoneNumber( - phoneNumber, applicationVerifier.jsObject)); + phoneOptions, applicationVerifier.jsObject)); /// Creates a phone auth credential given the verification ID /// from [verifyPhoneNumber] and the [verificationCode] that was sent to the /// user's mobile device. - static auth_interop.OAuthCredential credential( + static auth_interop.PhoneAuthCredentialJsImpl credential( String verificationId, String verificationCode) => auth_interop.PhoneAuthProviderJsImpl.credential( - verificationId, verificationCode) as auth_interop.OAuthCredential; + verificationId, verificationCode); } /// A verifier for domain verification and abuse prevention. @@ -987,18 +988,16 @@ class RecaptchaVerifier /// print('Response expired'); /// } /// }); - factory RecaptchaVerifier(container, - [Map? parameters, App? app]) => - (parameters != null) - ? ((app != null) - ? RecaptchaVerifier.fromJsObject( - auth_interop.RecaptchaVerifierJsImpl( - container, jsify(parameters), app.jsObject)) - : RecaptchaVerifier.fromJsObject( - auth_interop.RecaptchaVerifierJsImpl( - container, jsify(parameters)))) - : RecaptchaVerifier.fromJsObject( - auth_interop.RecaptchaVerifierJsImpl(container)); + factory RecaptchaVerifier( + container, Map parameters, Auth auth) { + return RecaptchaVerifier.fromJsObject( + auth_interop.RecaptchaVerifierJsImpl( + container, + jsify(parameters), + auth.jsObject, + ), + ); + } /// Creates a new RecaptchaVerifier from a [jsObject]. RecaptchaVerifier.fromJsObject(auth_interop.RecaptchaVerifierJsImpl jsObject) diff --git a/packages/firebase_auth/firebase_auth_web/lib/src/interop/auth_interop.dart b/packages/firebase_auth/firebase_auth_web/lib/src/interop/auth_interop.dart index 36e618ca2276..6f9ad2479dea 100644 --- a/packages/firebase_auth/firebase_auth_web/lib/src/interop/auth_interop.dart +++ b/packages/firebase_auth/firebase_auth_web/lib/src/interop/auth_interop.dart @@ -9,8 +9,9 @@ @JS('firebase_auth') library firebase_interop.auth; -import 'package:js/js.dart'; +import 'package:firebase_auth_web/src/interop/auth.dart'; import 'package:firebase_core_web/firebase_core_web_interop.dart'; +import 'package:js/js.dart'; @JS() external AuthJsImpl getAuth([AppJsImpl? app]); @@ -233,6 +234,19 @@ external PromiseJsImpl updateProfile( UserProfile profile, ); +/// https://firebase.google.com/docs/reference/js/auth.md#multifactor +@JS() +external MultiFactorUserJsImpl multiFactor( + UserJsImpl user, +); + +/// https://firebase.google.com/docs/reference/js/auth.md#multifactor +@JS() +external MultiFactorResolverJsImpl getMultiFactorResolver( + AuthJsImpl auth, + MultiFactorError error, +); + @JS('Auth') abstract class AuthJsImpl { external AppJsImpl get app; @@ -443,10 +457,10 @@ class PhoneAuthProviderJsImpl extends AuthProviderJsImpl { external factory PhoneAuthProviderJsImpl([AuthJsImpl? auth]); external static String get PROVIDER_ID; external PromiseJsImpl verifyPhoneNumber( - String phoneNumber, + dynamic /* PhoneInfoOptions | string */ phoneOptions, ApplicationVerifierJsImpl applicationVerifier, ); - external static AuthCredential credential( + external static PhoneAuthCredentialJsImpl credential( String verificationId, String verificationCode, ); @@ -461,10 +475,10 @@ abstract class ApplicationVerifierJsImpl { @JS('RecaptchaVerifier') class RecaptchaVerifierJsImpl extends ApplicationVerifierJsImpl { external factory RecaptchaVerifierJsImpl( - container, [ - Object? parameters, - AppJsImpl? app, - ]); + containerOrId, + Object parameters, + AuthJsImpl authExtern, + ); external void clear(); external PromiseJsImpl render(); } @@ -621,7 +635,7 @@ class AndroidSettings { }); } -/// https://firebase.google.com/docs/reference/js/firebase.auth#.UserCredential +/// https://firebase.google.com/docs/reference/js/auth.usercredential @JS() @anonymous class UserCredentialJsImpl { @@ -649,3 +663,91 @@ class AuthSettings { external set appVerificationDisabledForTesting(bool? b); // external factory AuthSettings({bool appVerificationDisabledForTesting}); } + +/// https://firebase.google.com/docs/reference/js/auth.multifactoruser.md#multifactoruser_interface +@JS() +@anonymous +class MultiFactorUserJsImpl { + external List get enrolledFactors; + external PromiseJsImpl enroll( + MultiFactorAssertionJsImpl assertion, String? displayName); + external PromiseJsImpl getSession(); + external PromiseJsImpl unenroll( + dynamic /* MultiFactorInfo | string */ option); +} + +/// https://firebase.google.com/docs/reference/js/auth.multifactorinfo +@JS() +@anonymous +class MultiFactorInfoJsImpl { + external String? get displayName; + external String get enrollmentTime; + external String get factorId; + external String get uid; +} + +/// https://firebase.google.com/docs/reference/js/auth.multifactorassertion +@JS() +@anonymous +class MultiFactorAssertionJsImpl { + external String get factorId; +} + +/// https://firebase.google.com/docs/reference/js/auth.multifactorerror +@JS('Error') +@anonymous +class MultiFactorError extends AuthError { + external dynamic get customData; +} + +/// https://firebase.google.com/docs/reference/js/auth.multifactorresolver +@JS() +@anonymous +class MultiFactorResolverJsImpl { + external List get hints; + external MultiFactorSessionJsImpl get session; + external PromiseJsImpl resolveSignIn( + MultiFactorAssertionJsImpl assertion); +} + +/// https://firebase.google.com/docs/reference/js/auth.multifactorresolver +@JS() +@anonymous +class MultiFactorSessionJsImpl {} + +/// https://firebase.google.com/docs/reference/js/auth.phonemultifactorinfo +@JS() +@anonymous +class PhoneMultiFactorInfoJsImpl extends MultiFactorInfoJsImpl { + external String get phoneNumber; +} + +/// https://firebase.google.com/docs/reference/js/auth.phonemultifactorenrollinfooptions +@JS() +@anonymous +class PhoneMultiFactorEnrollInfoOptionsJsImpl { + external String get phoneNumber; + external MultiFactorSessionJsImpl? get session; +} + +/// https://firebase.google.com/docs/reference/js/auth.phonemultifactorgenerator +@JS('PhoneMultiFactorGenerator') +class PhoneMultiFactorGeneratorJsImpl { + external static String get FACTOR_ID; + external static PhoneMultiFactorAssertionJsImpl? assertion( + PhoneAuthCredentialJsImpl credential); +} + +/// https://firebase.google.com/docs/reference/js/auth.phonemultifactorassertion +@JS() +@anonymous +class PhoneMultiFactorAssertionJsImpl extends MultiFactorAssertionJsImpl {} + +/// https://firebase.google.com/docs/reference/js/auth.phoneauthcredential +@JS() +@anonymous +class PhoneAuthCredentialJsImpl extends AuthCredential { + external static PhoneAuthCredentialJsImpl fromJSON( + dynamic /*object | string*/ json); + external Object toJSON(); +} diff --git a/packages/firebase_auth/firebase_auth_web/lib/src/interop/multi_factor.dart b/packages/firebase_auth/firebase_auth_web/lib/src/interop/multi_factor.dart new file mode 100644 index 000000000000..888108b39b21 --- /dev/null +++ b/packages/firebase_auth/firebase_auth_web/lib/src/interop/multi_factor.dart @@ -0,0 +1,163 @@ +// ignore_for_file: require_trailing_commas +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: non_constant_identifier_names +// ignore_for_file: public_member_api_docs + +import 'package:firebase_core_web/firebase_core_web_interop.dart' + hide jsify, dartify; + +import 'auth.dart' as auth; +import 'auth_interop.dart' as auth_interop; + +/// Given an AppJSImp, return the Auth instance. +MultiFactorUser multiFactor(auth.User user) { + return MultiFactorUser.getInstance(auth_interop.multiFactor(user.jsObject)); +} + +/// Given an AppJSImp, return the Auth instance. +MultiFactorResolver getMultiFactorResolver( + auth.Auth auth, auth_interop.MultiFactorError error) { + return MultiFactorResolver.fromJsObject( + auth_interop.getMultiFactorResolver(auth.jsObject, error)); +} + +/// The Firebase MultiFactorUser service class. +/// +/// See: https://firebase.google.com/docs/reference/js/auth.md#multifactor. +class MultiFactorUser + extends JsObjectWrapper { + static final _expando = Expando(); + + /// Creates a new Auth from a [jsObject]. + static MultiFactorUser getInstance( + auth_interop.MultiFactorUserJsImpl jsObject) { + return _expando[jsObject] ??= MultiFactorUser._fromJsObject(jsObject); + } + + MultiFactorUser._fromJsObject(auth_interop.MultiFactorUserJsImpl jsObject) + : super.fromJsObject(jsObject); + + /// Returns a list of the user's enrolled second factors. + List get enrolledFactors => + jsObject.enrolledFactors.map(MultiFactorInfo.fromJsObject).toList(); + + /// Returns the session identifier for a second factor enrollment operation. + /// + /// This is used to identify the user trying to enroll a second factor. + Future get session => + handleThenable(jsObject.getSession()) + .then(MultiFactorSession.fromJsObject); + + /// Enrolls a second factor as identified by the [MultiFactorAssertion] for the user. + /// + /// On resolution, the user tokens are updated to reflect the change in the JWT payload. + /// Accepts an additional display name parameter used to identify the second factor to the end user. + /// Recent re-authentication is required for this operation to succeed. On successful enrollment, + /// existing Firebase sessions (refresh tokens) are revoked. When a new factor is enrolled, + /// an email notification is sent to the user’s email. + Future enroll(MultiFactorAssertion assertion, String? displayName) { + return handleThenable(jsObject.enroll(assertion.jsObject, displayName)); + } + + /// Unenrolls the specified second factor. + /// + /// To specify the factor to remove, pass a [MultiFactorInfo] object + /// (retrieved from [MultiFactorUser.enrolledFactors]) or the factor's UID string. + /// Sessions are not revoked when the account is unenrolled. + /// An email notification is likely to be sent to the user notifying them of the change. + /// Recent re-authentication is required for this operation to succeed. + /// When an existing factor is unenrolled, an email notification is sent to the user’s email. + Future unenroll(String multiFactorInfoId) { + return handleThenable(jsObject.unenroll(multiFactorInfoId)); + } +} + +/// https://firebase.google.com/docs/reference/js/auth.multifactorinfo +class MultiFactorInfo + extends JsObjectWrapper { + MultiFactorInfo.fromJsObject(T jsObject) : super.fromJsObject(jsObject); + + /// The user friendly name of the current second factor. + String? get displayName => jsObject.displayName; + + /// The enrollment date of the second factor formatted as a UTC string. + String get enrollmentTime => jsObject.enrollmentTime; + + /// The identifier of the second factor. + String get factorId => jsObject.factorId; + + /// The multi-factor enrollment ID. + String get uid => jsObject.uid; +} + +class PhoneMultiFactorInfo + extends MultiFactorInfo { + PhoneMultiFactorInfo.fromJsObject( + auth_interop.PhoneMultiFactorInfoJsImpl jsObject) + : super.fromJsObject(jsObject); + + /// The user friendly name of the current second factor. + String get phoneNumber => jsObject.phoneNumber; +} + +/// https://firebase.google.com/docs/reference/js/auth.multifactorsession.md#multifactorsession_interface +class MultiFactorSession + extends JsObjectWrapper { + MultiFactorSession.fromJsObject(auth.MultiFactorSessionJsImpl jsObject) + : super.fromJsObject(jsObject); +} + +/// https://firebase.google.com/docs/reference/js/auth.multifactorsession.md#multifactorsession_interface +class MultiFactorAssertion + extends JsObjectWrapper { + MultiFactorAssertion.fromJsObject(T jsObject) : super.fromJsObject(jsObject); + + String get factorId => jsObject.factorId; +} + +/// https://firebase.google.com/docs/reference/js/auth.multifactorsession.md#multifactorsession_interface +class PhoneMultiFactorAssertion + extends MultiFactorAssertion { + PhoneMultiFactorAssertion.fromJsObject( + auth.PhoneMultiFactorAssertionJsImpl jsObject) + : super.fromJsObject(jsObject); +} + +/// https://firebase.google.com/docs/reference/js/auth#getmultifactorresolver +class MultiFactorResolver + extends JsObjectWrapper { + MultiFactorResolver.fromJsObject(auth.MultiFactorResolverJsImpl jsObject) + : super.fromJsObject(jsObject); + + List get hints => jsObject.hints.map((e) { + if (e is auth_interop.PhoneMultiFactorInfoJsImpl) { + return PhoneMultiFactorInfo.fromJsObject(e); + } else { + return MultiFactorInfo.fromJsObject(e); + } + }).toList(); + MultiFactorSession get session => + MultiFactorSession.fromJsObject(jsObject.session); + + Future resolveSignIn(MultiFactorAssertion assertion) { + return handleThenable(jsObject.resolveSignIn(assertion.jsObject)) + .then(auth.UserCredential.fromJsObject); + } +} + +/// https://firebase.google.com/docs/reference/js/auth.multifactorsession.md#multifactorsession_interface +class PhoneMultiFactorGenerator + extends JsObjectWrapper { + PhoneMultiFactorGenerator.fromJsObject( + auth.PhoneMultiFactorGeneratorJsImpl jsObject) + : super.fromJsObject(jsObject); + + static PhoneMultiFactorAssertion assertion( + auth.PhoneAuthCredentialJsImpl credential) { + return PhoneMultiFactorAssertion.fromJsObject( + auth_interop.PhoneMultiFactorGeneratorJsImpl.assertion(credential)!); + } +} diff --git a/packages/firebase_auth/firebase_auth_web/lib/src/interop/utils/utils.dart b/packages/firebase_auth/firebase_auth_web/lib/src/interop/utils/utils.dart index e23176107c13..4996b5e59449 100644 --- a/packages/firebase_auth/firebase_auth_web/lib/src/interop/utils/utils.dart +++ b/packages/firebase_auth/firebase_auth_web/lib/src/interop/utils/utils.dart @@ -12,6 +12,9 @@ dynamic dartify(Object jsObject) { } /// Returns the JS implementation from Dart Object. -dynamic jsify(Object dartObject) { - return core_interop.jsify(dartObject); +dynamic jsify( + Object dartObject, [ + Object? Function(Object? object)? customJsify, +]) { + return core_interop.jsify(dartObject, customJsify); } diff --git a/packages/firebase_auth/firebase_auth_web/lib/src/utils/web_utils.dart b/packages/firebase_auth/firebase_auth_web/lib/src/utils/web_utils.dart index 25f2d94351c0..55b07c67112a 100644 --- a/packages/firebase_auth/firebase_auth_web/lib/src/utils/web_utils.dart +++ b/packages/firebase_auth/firebase_auth_web/lib/src/utils/web_utils.dart @@ -3,17 +3,25 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'dart:io'; + import 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart'; +import 'package:firebase_auth_web/firebase_auth_web.dart'; +import 'package:firebase_auth_web/src/firebase_auth_web_multi_factor.dart'; import 'package:firebase_core_web/firebase_core_web_interop.dart' as core_interop; import '../interop/auth.dart' as auth_interop; +import '../interop/multi_factor.dart' as multi_factor_interop; /// Given a web error, an [Exception] is returned. /// /// The firebase-dart wrapper exposes a [core_interop.FirebaseError], allowing us to /// use the code and message and convert it into an expected [FirebaseAuthException]. -FirebaseAuthException getFirebaseAuthException(Object exception) { +FirebaseAuthException getFirebaseAuthException( + Object exception, [ + auth_interop.Auth? auth, +]) { if (exception is! core_interop.FirebaseError) { return FirebaseAuthException( code: 'unknown', @@ -28,6 +36,54 @@ FirebaseAuthException getFirebaseAuthException(Object exception) { .replaceFirst(' (${firebaseError.code}).', '') .replaceFirst('Firebase: ', ''); + if (code == 'multi-factor-auth-required') { + final _auth = auth; + if (_auth == null) { + throw ArgumentError( + 'Multi-factor authentication is required, but the auth instance is null. ' + 'Please ensure that the auth instance is not null before calling ' + '`getFirebaseAuthException()`.', + ); + } + final resolverWeb = multi_factor_interop.getMultiFactorResolver( + _auth, + firebaseError as auth_interop.MultiFactorError, + ); + + return FirebaseAuthMultiFactorException( + code: code, + message: message, + email: firebaseError.email, + phoneNumber: firebaseError.phoneNumber, + tenantId: firebaseError.tenantId, + resolver: MultiFactorResolverWeb( + resolverWeb.hints.map((e) { + if (e is multi_factor_interop.PhoneMultiFactorInfo) { + return PhoneMultiFactorInfo( + displayName: e.displayName, + factorId: e.factorId, + enrollmentTimestamp: + HttpDate.parse(e.enrollmentTime).millisecondsSinceEpoch / + 1000, + uid: e.uid, + phoneNumber: e.phoneNumber, + ); + } + return MultiFactorInfo( + displayName: e.displayName, + factorId: e.factorId, + enrollmentTimestamp: + HttpDate.parse(e.enrollmentTime).millisecondsSinceEpoch / 1000, + uid: e.uid, + ); + }).toList(), + MultiFactorSessionWeb('web', resolverWeb.session), + FirebaseAuthWeb.instance, + resolverWeb, + ), + ); + } + return FirebaseAuthException( code: code, message: message, @@ -252,7 +308,7 @@ auth_interop.OAuthCredential? convertPlatformCredential( return auth_interop.PhoneAuthProvider.credential( credential.verificationId!, credential.smsCode!, - ); + ) as auth_interop.OAuthCredential; } if (credential is OAuthCredential) { @@ -290,3 +346,9 @@ String convertRecaptchaVerifierTheme(RecaptchaVerifierTheme theme) { return 'light'; } } + +/// Converts a [multi_factor_interop.MultiFactorSession] into a [MultiFactorSession]. +MultiFactorSession convertMultiFactorSession( + multi_factor_interop.MultiFactorSession multiFactorSession) { + return MultiFactorSessionWeb('web', multiFactorSession); +} diff --git a/tests/test_driver/firebase_auth/firebase_auth_multi_factor_e2e.dart b/tests/test_driver/firebase_auth/firebase_auth_multi_factor_e2e.dart index df8fb6480599..75e53f0f9e75 100644 --- a/tests/test_driver/firebase_auth/firebase_auth_multi_factor_e2e.dart +++ b/tests/test_driver/firebase_auth/firebase_auth_multi_factor_e2e.dart @@ -5,7 +5,6 @@ import 'dart:async'; import 'package:firebase_auth/firebase_auth.dart'; -import 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart';