Skip to content

Commit 1b85c8b

Browse files
LyokoneSalakar
andauthored
feat(auth): add phone MFA (#9044)
* feat(auth, android): add phone MFA to Android (#8998) * chores: update format CI step to match local melos format * chores: update format CI step to match local melos format * feat(auth, android): mfa * feat(auth, android): add MFA to phone options * feat(auth, android): add phone MFA * feat(auth, android): add MFA signin * feat(auth): add documentation * feat(auth, android): fix typings * feat(auth, android): fix tests * feat(auth, android): add input for phone number to example app * feat(auth, android): add unenroll and getEnrolledFactors * feat(auth, android): fix formatting * feat(auth, android): fix formatting * feat(ui, android): use mocktail for mocking dependencies * feat(auth, android): fix analyze * feat(auth): fix melos generate command * Update packages/firebase_auth/firebase_auth_platform_interface/lib/src/method_channel/method_channel_multi_factor.dart Co-authored-by: Mike Diarmid <[email protected]> Co-authored-by: Mike Diarmid <[email protected]> * feat(auth, ios): add phone MFA (#9008) * chores: update format CI step to match local melos format * chores: update format CI step to match local melos format * feat(auth, android): mfa * feat(auth, android): add MFA to phone options * feat(auth, android): add phone MFA * feat(auth, android): add MFA signin * feat(auth, android): fix tests * feat(auth, android): add input for phone number to example app * feat(auth, android): add unenroll and getEnrolledFactors * feat(auth, android): fix formatting * feat(auth, android): fix formatting * feat(auth, android): fix analyze * feat(auth, ios): implement enroll * feat(auth, ios): implement MFA for signin * feat(auth, ios): add conditions to allow macos to build * feat(auth, ios): add conditions to allow macos to build * feat(auth, ios): fix ios build * Update packages/firebase_auth/firebase_auth/ios/Classes/FLTFirebaseAuthPlugin.m Co-authored-by: Mike Diarmid <[email protected]> * fix: fix rebase Co-authored-by: Mike Diarmid <[email protected]> * feat(auth): fix android merge * feat(auth): fix ios merge * test(auth): add MFA e2e (#9034) * feat(auth, android): add phone MFA to Android (#8998) * chores: update format CI step to match local melos format * chores: update format CI step to match local melos format * feat(auth, android): mfa * feat(auth, android): add MFA to phone options * feat(auth, android): add phone MFA * feat(auth, android): add MFA signin * feat(auth): add documentation * feat(auth, android): fix typings * feat(auth, android): fix tests * feat(auth, android): add input for phone number to example app * feat(auth, android): add unenroll and getEnrolledFactors * feat(auth, android): fix formatting * feat(auth, android): fix formatting * feat(ui, android): use mocktail for mocking dependencies * feat(auth, android): fix analyze * feat(auth): fix melos generate command * Update packages/firebase_auth/firebase_auth_platform_interface/lib/src/method_channel/method_channel_multi_factor.dart Co-authored-by: Mike Diarmid <[email protected]> Co-authored-by: Mike Diarmid <[email protected]> * feat(auth, ios): add phone MFA (#9008) * chores: update format CI step to match local melos format * chores: update format CI step to match local melos format * feat(auth, android): mfa * feat(auth, android): add MFA to phone options * feat(auth, android): add phone MFA * feat(auth, android): add MFA signin * feat(auth, android): fix tests * feat(auth, android): add input for phone number to example app * feat(auth, android): add unenroll and getEnrolledFactors * feat(auth, android): fix formatting * feat(auth, android): fix formatting * feat(auth, android): fix analyze * feat(auth, ios): implement enroll * feat(auth, ios): implement MFA for signin * feat(auth, ios): add conditions to allow macos to build * feat(auth, ios): add conditions to allow macos to build * feat(auth, ios): fix ios build * Update packages/firebase_auth/firebase_auth/ios/Classes/FLTFirebaseAuthPlugin.m Co-authored-by: Mike Diarmid <[email protected]> * fix: fix rebase Co-authored-by: Mike Diarmid <[email protected]> * feat(auth): fix android merge * feat(auth): fix ios merge * feat(auth): add e2e tests * fix(auth): fix reintroduce tests * feat(auth): add documentation * feat(auth): add e2e tests Co-authored-by: Mike Diarmid <[email protected]> * 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 * feat(auth, android): add early return * feat(auth, android): change dynamic to Object Co-authored-by: Mike Diarmid <[email protected]>
1 parent 97f6417 commit 1b85c8b

File tree

69 files changed

+4485
-353
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+4485
-353
lines changed

melos.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,9 @@ scripts:
186186

187187
generate:pigeon:
188188
run: |
189-
melos exec -- "flutter pub run pigeon --input ./pigeons/messages.dart"
190-
melos run generate:pigeon:macos
189+
melos exec -- "flutter pub run pigeon --input ./pigeons/messages.dart" && \
190+
melos run generate:pigeon:macos --no-select && \
191+
melos run format --no-select
191192
select-package:
192193
file-exists: 'pigeons/messages.dart'
193194
description: Generate the pigeon messages for all the supported packages.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Copyright 2021 The Chromium Authors. All rights reserved.
2+
# Use of this source code is governed by a BSD-style license that can be
3+
# in the LICENSE file.
4+
5+
include: ../../analysis_options.yaml
6+
7+
analyzer:
8+
# TODO(Lyokone): not working if added on the root analysis file
9+
exclude:
10+
- firebase_auth_platform_interface/lib/src/pigeon/messages.pigeon.dart
11+
- firebase_auth_platform_interface/test/pigeon/test_api.dart

packages/firebase_auth/firebase_auth/android/src/main/java/io/flutter/plugins/firebase/auth/Constants.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,10 @@ public class Constants {
8383
public static final String APP_VERIFICATION_DISABLED_FOR_TESTING =
8484
"appVerificationDisabledForTesting";
8585
public static final String FORCE_RECAPTCHA_FLOW = "forceRecaptchaFlow";
86+
87+
// MultiFactor
88+
public static final String MULTI_FACTOR_HINTS = "multiFactorHints";
89+
public static final String MULTI_FACTOR_SESSION_ID = "multiFactorSessionId";
90+
public static final String MULTI_FACTOR_RESOLVER_ID = "multiFactorResolverId";
91+
public static final String MULTI_FACTOR_INFO = "multiFactorInfo";
8692
}

packages/firebase_auth/firebase_auth/android/src/main/java/io/flutter/plugins/firebase/auth/FlutterFirebaseAuthPlugin.java

Lines changed: 282 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,28 @@
2828
import com.google.firebase.auth.FacebookAuthProvider;
2929
import com.google.firebase.auth.FirebaseAuth;
3030
import com.google.firebase.auth.FirebaseAuthException;
31+
import com.google.firebase.auth.FirebaseAuthMultiFactorException;
3132
import com.google.firebase.auth.FirebaseAuthProvider;
3233
import com.google.firebase.auth.FirebaseUser;
3334
import com.google.firebase.auth.FirebaseUserMetadata;
3435
import com.google.firebase.auth.GetTokenResult;
3536
import com.google.firebase.auth.GithubAuthProvider;
3637
import com.google.firebase.auth.GoogleAuthProvider;
38+
import com.google.firebase.auth.MultiFactor;
39+
import com.google.firebase.auth.MultiFactorAssertion;
40+
import com.google.firebase.auth.MultiFactorInfo;
41+
import com.google.firebase.auth.MultiFactorResolver;
42+
import com.google.firebase.auth.MultiFactorSession;
3743
import com.google.firebase.auth.OAuthProvider;
3844
import com.google.firebase.auth.PhoneAuthCredential;
3945
import com.google.firebase.auth.PhoneAuthProvider;
46+
import com.google.firebase.auth.PhoneMultiFactorGenerator;
47+
import com.google.firebase.auth.PhoneMultiFactorInfo;
4048
import com.google.firebase.auth.SignInMethodQueryResult;
4149
import com.google.firebase.auth.TwitterAuthProvider;
4250
import com.google.firebase.auth.UserInfo;
4351
import com.google.firebase.auth.UserProfileChangeRequest;
52+
import com.google.firebase.internal.api.FirebaseNoSignedInUserException;
4453
import io.flutter.embedding.engine.plugins.FlutterPlugin;
4554
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
4655
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
@@ -63,7 +72,12 @@
6372

6473
/** Flutter plugin for Firebase Auth. */
6574
public class FlutterFirebaseAuthPlugin
66-
implements FlutterFirebasePlugin, MethodCallHandler, FlutterPlugin, ActivityAware {
75+
implements FlutterFirebasePlugin,
76+
MethodCallHandler,
77+
FlutterPlugin,
78+
ActivityAware,
79+
GeneratedAndroidFirebaseAuth.MultiFactorUserHostApi,
80+
GeneratedAndroidFirebaseAuth.MultiFactoResolverHostApi {
6781

6882
private static final String METHOD_CHANNEL_NAME = "plugins.flutter.io/firebase_auth";
6983

@@ -98,6 +112,8 @@ private void initInstance(BinaryMessenger messenger) {
98112
registerPlugin(METHOD_CHANNEL_NAME, this);
99113
channel = new MethodChannel(messenger, METHOD_CHANNEL_NAME);
100114
channel.setMethodCallHandler(this);
115+
GeneratedAndroidFirebaseAuth.MultiFactorUserHostApi.setup(messenger, this);
116+
GeneratedAndroidFirebaseAuth.MultiFactoResolverHostApi.setup(messenger, this);
101117

102118
this.messenger = messenger;
103119
}
@@ -112,6 +128,8 @@ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
112128
channel.setMethodCallHandler(null);
113129
channel = null;
114130
messenger = null;
131+
GeneratedAndroidFirebaseAuth.MultiFactorUserHostApi.setup(null, this);
132+
GeneratedAndroidFirebaseAuth.MultiFactoResolverHostApi.setup(null, this);
115133

116134
removeEventListeners();
117135
}
@@ -159,6 +177,11 @@ private FirebaseUser getCurrentUser(Map<String, Object> arguments) {
159177
return FirebaseAuth.getInstance(app).getCurrentUser();
160178
}
161179

180+
private FirebaseUser getCurrentUser(String appName) {
181+
FirebaseApp app = FirebaseApp.getInstance(appName);
182+
return FirebaseAuth.getInstance(app).getCurrentUser();
183+
}
184+
162185
private AuthCredential getCredential(Map<String, Object> arguments)
163186
throws FlutterFirebaseAuthPluginException {
164187
@SuppressWarnings("unchecked")
@@ -790,13 +813,79 @@ private Task<Map<String, Object>> signInWithEmailAndPassword(Map<String, Object>
790813

791814
taskCompletionSource.setResult(parseAuthResult(authResult));
792815
} catch (Exception e) {
793-
taskCompletionSource.setException(e);
816+
if (e.getCause() instanceof FirebaseAuthMultiFactorException) {
817+
final FirebaseAuthMultiFactorException multiFactorException =
818+
(FirebaseAuthMultiFactorException) e.getCause();
819+
Map<String, Object> output = new HashMap<>();
820+
821+
MultiFactorResolver multiFactorResolver = multiFactorException.getResolver();
822+
final List<MultiFactorInfo> hints = multiFactorResolver.getHints();
823+
824+
final MultiFactorSession session = multiFactorResolver.getSession();
825+
final String sessionId = UUID.randomUUID().toString();
826+
multiFactorSessionMap.put(sessionId, session);
827+
828+
final String resolverId = UUID.randomUUID().toString();
829+
multiFactorResolverMap.put(resolverId, multiFactorResolver);
830+
831+
final List<Map<String, Object>> pigeonHints = multiFactorInfoToMap(hints);
832+
833+
output.put(Constants.APP_NAME, getAuth(arguments).getApp().getName());
834+
835+
output.put(Constants.MULTI_FACTOR_HINTS, pigeonHints);
836+
837+
output.put(Constants.MULTI_FACTOR_SESSION_ID, sessionId);
838+
output.put(Constants.MULTI_FACTOR_RESOLVER_ID, resolverId);
839+
840+
taskCompletionSource.setException(
841+
new FlutterFirebaseAuthPluginException(
842+
multiFactorException.getErrorCode(),
843+
multiFactorException.getLocalizedMessage(),
844+
output));
845+
} else {
846+
taskCompletionSource.setException(e);
847+
}
794848
}
795849
});
796850

797851
return taskCompletionSource.getTask();
798852
}
799853

854+
private List<GeneratedAndroidFirebaseAuth.PigeonMultiFactorInfo> multiFactorInfoToPigeon(
855+
List<MultiFactorInfo> hints) {
856+
List<GeneratedAndroidFirebaseAuth.PigeonMultiFactorInfo> pigeonHints = new ArrayList<>();
857+
for (MultiFactorInfo info : hints) {
858+
if (info instanceof PhoneMultiFactorInfo) {
859+
pigeonHints.add(
860+
new GeneratedAndroidFirebaseAuth.PigeonMultiFactorInfo.Builder()
861+
.setPhoneNumber(((PhoneMultiFactorInfo) info).getPhoneNumber())
862+
.setDisplayName(info.getDisplayName())
863+
.setEnrollmentTimestamp((double) info.getEnrollmentTimestamp())
864+
.setUid(info.getUid())
865+
.setFactorId(info.getFactorId())
866+
.build());
867+
868+
} else {
869+
pigeonHints.add(
870+
new GeneratedAndroidFirebaseAuth.PigeonMultiFactorInfo.Builder()
871+
.setDisplayName(info.getDisplayName())
872+
.setEnrollmentTimestamp((double) info.getEnrollmentTimestamp())
873+
.setUid(info.getUid())
874+
.setFactorId(info.getFactorId())
875+
.build());
876+
}
877+
}
878+
return pigeonHints;
879+
}
880+
881+
private List<Map<String, Object>> multiFactorInfoToMap(List<MultiFactorInfo> hints) {
882+
List<Map<String, Object>> pigeonHints = new ArrayList<>();
883+
for (GeneratedAndroidFirebaseAuth.PigeonMultiFactorInfo info : multiFactorInfoToPigeon(hints)) {
884+
pigeonHints.add(info.toMap());
885+
}
886+
return pigeonHints;
887+
}
888+
800889
private Task<Map<String, Object>> signInWithEmailLink(Map<String, Object> arguments) {
801890
TaskCompletionSource<Map<String, Object>> taskCompletionSource = new TaskCompletionSource<>();
802891

@@ -883,10 +972,36 @@ private Task<String> verifyPhoneNumber(Map<String, Object> arguments) {
883972
String eventChannelName =
884973
METHOD_CHANNEL_NAME + "/phone/" + UUID.randomUUID().toString();
885974
EventChannel channel = new EventChannel(messenger, eventChannelName);
975+
976+
final String multiFactorSessionId =
977+
(String) arguments.get(Constants.MULTI_FACTOR_SESSION_ID);
978+
MultiFactorSession multiFactorSession = null;
979+
980+
if (multiFactorSessionId != null) {
981+
multiFactorSession = multiFactorSessionMap.get(multiFactorSessionId);
982+
}
983+
984+
final String multiFactorInfoId = (String) arguments.get(Constants.MULTI_FACTOR_INFO);
985+
PhoneMultiFactorInfo multiFactorInfo = null;
986+
987+
if (multiFactorInfoId != null) {
988+
for (String resolverId : multiFactorResolverMap.keySet()) {
989+
for (MultiFactorInfo info : multiFactorResolverMap.get(resolverId).getHints()) {
990+
if (info.getUid().equals(multiFactorInfoId)
991+
&& info instanceof PhoneMultiFactorInfo) {
992+
multiFactorInfo = (PhoneMultiFactorInfo) info;
993+
break;
994+
}
995+
}
996+
}
997+
}
998+
886999
PhoneNumberVerificationStreamHandler handler =
8871000
new PhoneNumberVerificationStreamHandler(
8881001
getActivity(),
8891002
arguments,
1003+
multiFactorSession,
1004+
multiFactorInfo,
8901005
credential -> {
8911006
int hashCode = credential.hashCode();
8921007
authCredentials.put(hashCode, credential);
@@ -1573,4 +1688,169 @@ private void removeEventListeners() {
15731688
}
15741689
streamHandlers.clear();
15751690
}
1691+
1692+
// Map an app id to a map of user id to a MultiFactorUser object.
1693+
private final Map<String, Map<String, MultiFactor>> multiFactorUserMap = new HashMap<>();
1694+
1695+
// Map an id to a MultiFactorSession object.
1696+
private final Map<String, MultiFactorSession> multiFactorSessionMap = new HashMap<>();
1697+
1698+
// Map an id to a MultiFactorSession object.
1699+
private final Map<String, MultiFactorResolver> multiFactorResolverMap = new HashMap<>();
1700+
1701+
private MultiFactor getAppMultiFactor(@NonNull String appName)
1702+
throws FirebaseNoSignedInUserException {
1703+
final FirebaseUser currentUser = getCurrentUser(appName);
1704+
if (currentUser == null) {
1705+
throw new FirebaseNoSignedInUserException("No user is signed in");
1706+
}
1707+
if (multiFactorUserMap.get(appName) == null) {
1708+
multiFactorUserMap.put(appName, new HashMap<>());
1709+
}
1710+
1711+
final Map<String, MultiFactor> appMultiFactorUser = multiFactorUserMap.get(appName);
1712+
if (appMultiFactorUser.get(currentUser.getUid()) == null) {
1713+
appMultiFactorUser.put(currentUser.getUid(), currentUser.getMultiFactor());
1714+
}
1715+
1716+
final MultiFactor multiFactor = appMultiFactorUser.get(currentUser.getUid());
1717+
return multiFactor;
1718+
}
1719+
1720+
@Override
1721+
public void enrollPhone(
1722+
@NonNull String appName,
1723+
@NonNull GeneratedAndroidFirebaseAuth.PigeonPhoneMultiFactorAssertion assertion,
1724+
@Nullable String displayName,
1725+
GeneratedAndroidFirebaseAuth.Result<Void> result) {
1726+
final MultiFactor multiFactor;
1727+
try {
1728+
multiFactor = getAppMultiFactor(appName);
1729+
} catch (FirebaseNoSignedInUserException e) {
1730+
result.error(e);
1731+
return;
1732+
}
1733+
1734+
PhoneAuthCredential credential =
1735+
PhoneAuthProvider.getCredential(
1736+
assertion.getVerificationId(), assertion.getVerificationCode());
1737+
1738+
MultiFactorAssertion multiFactorAssertion = PhoneMultiFactorGenerator.getAssertion(credential);
1739+
1740+
multiFactor
1741+
.enroll(multiFactorAssertion, displayName)
1742+
.addOnCompleteListener(
1743+
task -> {
1744+
if (task.isSuccessful()) {
1745+
result.success(null);
1746+
} else {
1747+
result.error(task.getException());
1748+
}
1749+
});
1750+
}
1751+
1752+
@Override
1753+
public void getSession(
1754+
@NonNull String appName,
1755+
GeneratedAndroidFirebaseAuth.Result<GeneratedAndroidFirebaseAuth.PigeonMultiFactorSession>
1756+
result) {
1757+
final MultiFactor multiFactor;
1758+
try {
1759+
multiFactor = getAppMultiFactor(appName);
1760+
} catch (FirebaseNoSignedInUserException e) {
1761+
result.error(e);
1762+
return;
1763+
}
1764+
1765+
multiFactor
1766+
.getSession()
1767+
.addOnCompleteListener(
1768+
task -> {
1769+
if (task.isSuccessful()) {
1770+
final MultiFactorSession sessionResult = task.getResult();
1771+
final String id = UUID.randomUUID().toString();
1772+
multiFactorSessionMap.put(id, sessionResult);
1773+
result.success(
1774+
new GeneratedAndroidFirebaseAuth.PigeonMultiFactorSession.Builder()
1775+
.setId(id)
1776+
.build());
1777+
} else {
1778+
Exception exception = task.getException();
1779+
result.error(exception);
1780+
}
1781+
});
1782+
}
1783+
1784+
@Override
1785+
public void unenroll(
1786+
@NonNull String appName,
1787+
@Nullable String factorUid,
1788+
GeneratedAndroidFirebaseAuth.Result<Void> result) {
1789+
final MultiFactor multiFactor;
1790+
try {
1791+
multiFactor = getAppMultiFactor(appName);
1792+
} catch (FirebaseNoSignedInUserException e) {
1793+
result.error(e);
1794+
return;
1795+
}
1796+
1797+
multiFactor
1798+
.unenroll(factorUid)
1799+
.addOnCompleteListener(
1800+
task -> {
1801+
if (task.isSuccessful()) {
1802+
result.success(null);
1803+
} else {
1804+
result.error(task.getException());
1805+
}
1806+
});
1807+
}
1808+
1809+
@Override
1810+
public void getEnrolledFactors(
1811+
@NonNull String appName,
1812+
GeneratedAndroidFirebaseAuth.Result<List<GeneratedAndroidFirebaseAuth.PigeonMultiFactorInfo>>
1813+
result) {
1814+
final MultiFactor multiFactor;
1815+
try {
1816+
multiFactor = getAppMultiFactor(appName);
1817+
} catch (FirebaseNoSignedInUserException e) {
1818+
result.error(e);
1819+
return;
1820+
}
1821+
1822+
final List<MultiFactorInfo> factors = multiFactor.getEnrolledFactors();
1823+
1824+
final List<GeneratedAndroidFirebaseAuth.PigeonMultiFactorInfo> resultFactors =
1825+
multiFactorInfoToPigeon(factors);
1826+
1827+
result.success(resultFactors);
1828+
}
1829+
1830+
@Override
1831+
public void resolveSignIn(
1832+
@NonNull String resolverId,
1833+
@NonNull GeneratedAndroidFirebaseAuth.PigeonPhoneMultiFactorAssertion assertion,
1834+
GeneratedAndroidFirebaseAuth.Result<Map<String, Object>> result) {
1835+
final MultiFactorResolver resolver = multiFactorResolverMap.get(resolverId);
1836+
1837+
PhoneAuthCredential credential =
1838+
PhoneAuthProvider.getCredential(
1839+
assertion.getVerificationId(), assertion.getVerificationCode());
1840+
1841+
MultiFactorAssertion multiFactorAssertion = PhoneMultiFactorGenerator.getAssertion(credential);
1842+
1843+
resolver
1844+
.resolveSignIn(multiFactorAssertion)
1845+
.addOnCompleteListener(
1846+
task -> {
1847+
if (task.isSuccessful()) {
1848+
final AuthResult authResult = task.getResult();
1849+
result.success(parseAuthResult(authResult));
1850+
} else {
1851+
Exception exception = task.getException();
1852+
result.error(exception);
1853+
}
1854+
});
1855+
}
15761856
}

0 commit comments

Comments
 (0)