Skip to content

Commit

Permalink
feat(auth): add phone MFA (#9044)
Browse files Browse the repository at this point in the history
* 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 <mike.diarmid@gmail.com>

Co-authored-by: Mike Diarmid <mike.diarmid@gmail.com>

* 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 <mike.diarmid@gmail.com>

* fix: fix rebase

Co-authored-by: Mike Diarmid <mike.diarmid@gmail.com>

* 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 <mike.diarmid@gmail.com>

Co-authored-by: Mike Diarmid <mike.diarmid@gmail.com>

* 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 <mike.diarmid@gmail.com>

* fix: fix rebase

Co-authored-by: Mike Diarmid <mike.diarmid@gmail.com>

* 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 <mike.diarmid@gmail.com>

* 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 <mike.diarmid@gmail.com>
  • Loading branch information
Lyokone and Salakar committed Jul 20, 2022
1 parent 97f6417 commit 1b85c8b
Show file tree
Hide file tree
Showing 69 changed files with 4,485 additions and 353 deletions.
5 changes: 3 additions & 2 deletions melos.yaml
Expand Up @@ -186,8 +186,9 @@ scripts:

generate:pigeon:
run: |
melos exec -- "flutter pub run pigeon --input ./pigeons/messages.dart"
melos run generate:pigeon:macos
melos exec -- "flutter pub run pigeon --input ./pigeons/messages.dart" && \
melos run generate:pigeon:macos --no-select && \
melos run format --no-select
select-package:
file-exists: 'pigeons/messages.dart'
description: Generate the pigeon messages for all the supported packages.
Expand Down
11 changes: 11 additions & 0 deletions packages/firebase_auth/analysis_options.yaml
@@ -0,0 +1,11 @@
# Copyright 2021 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# in the LICENSE file.

include: ../../analysis_options.yaml

analyzer:
# TODO(Lyokone): not working if added on the root analysis file
exclude:
- firebase_auth_platform_interface/lib/src/pigeon/messages.pigeon.dart
- firebase_auth_platform_interface/test/pigeon/test_api.dart
Expand Up @@ -83,4 +83,10 @@ public class Constants {
public static final String APP_VERIFICATION_DISABLED_FOR_TESTING =
"appVerificationDisabledForTesting";
public static final String FORCE_RECAPTCHA_FLOW = "forceRecaptchaFlow";

// MultiFactor
public static final String MULTI_FACTOR_HINTS = "multiFactorHints";
public static final String MULTI_FACTOR_SESSION_ID = "multiFactorSessionId";
public static final String MULTI_FACTOR_RESOLVER_ID = "multiFactorResolverId";
public static final String MULTI_FACTOR_INFO = "multiFactorInfo";
}
Expand Up @@ -28,19 +28,28 @@
import com.google.firebase.auth.FacebookAuthProvider;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseAuthException;
import com.google.firebase.auth.FirebaseAuthMultiFactorException;
import com.google.firebase.auth.FirebaseAuthProvider;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.auth.FirebaseUserMetadata;
import com.google.firebase.auth.GetTokenResult;
import com.google.firebase.auth.GithubAuthProvider;
import com.google.firebase.auth.GoogleAuthProvider;
import com.google.firebase.auth.MultiFactor;
import com.google.firebase.auth.MultiFactorAssertion;
import com.google.firebase.auth.MultiFactorInfo;
import com.google.firebase.auth.MultiFactorResolver;
import com.google.firebase.auth.MultiFactorSession;
import com.google.firebase.auth.OAuthProvider;
import com.google.firebase.auth.PhoneAuthCredential;
import com.google.firebase.auth.PhoneAuthProvider;
import com.google.firebase.auth.PhoneMultiFactorGenerator;
import com.google.firebase.auth.PhoneMultiFactorInfo;
import com.google.firebase.auth.SignInMethodQueryResult;
import com.google.firebase.auth.TwitterAuthProvider;
import com.google.firebase.auth.UserInfo;
import com.google.firebase.auth.UserProfileChangeRequest;
import com.google.firebase.internal.api.FirebaseNoSignedInUserException;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
Expand All @@ -63,7 +72,12 @@

/** Flutter plugin for Firebase Auth. */
public class FlutterFirebaseAuthPlugin
implements FlutterFirebasePlugin, MethodCallHandler, FlutterPlugin, ActivityAware {
implements FlutterFirebasePlugin,
MethodCallHandler,
FlutterPlugin,
ActivityAware,
GeneratedAndroidFirebaseAuth.MultiFactorUserHostApi,
GeneratedAndroidFirebaseAuth.MultiFactoResolverHostApi {

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

Expand Down Expand Up @@ -98,6 +112,8 @@ private void initInstance(BinaryMessenger messenger) {
registerPlugin(METHOD_CHANNEL_NAME, this);
channel = new MethodChannel(messenger, METHOD_CHANNEL_NAME);
channel.setMethodCallHandler(this);
GeneratedAndroidFirebaseAuth.MultiFactorUserHostApi.setup(messenger, this);
GeneratedAndroidFirebaseAuth.MultiFactoResolverHostApi.setup(messenger, this);

this.messenger = messenger;
}
Expand All @@ -112,6 +128,8 @@ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
channel.setMethodCallHandler(null);
channel = null;
messenger = null;
GeneratedAndroidFirebaseAuth.MultiFactorUserHostApi.setup(null, this);
GeneratedAndroidFirebaseAuth.MultiFactoResolverHostApi.setup(null, this);

removeEventListeners();
}
Expand Down Expand Up @@ -159,6 +177,11 @@ private FirebaseUser getCurrentUser(Map<String, Object> arguments) {
return FirebaseAuth.getInstance(app).getCurrentUser();
}

private FirebaseUser getCurrentUser(String appName) {
FirebaseApp app = FirebaseApp.getInstance(appName);
return FirebaseAuth.getInstance(app).getCurrentUser();
}

private AuthCredential getCredential(Map<String, Object> arguments)
throws FlutterFirebaseAuthPluginException {
@SuppressWarnings("unchecked")
Expand Down Expand Up @@ -790,13 +813,79 @@ private Task<Map<String, Object>> signInWithEmailAndPassword(Map<String, Object>

taskCompletionSource.setResult(parseAuthResult(authResult));
} catch (Exception e) {
taskCompletionSource.setException(e);
if (e.getCause() instanceof FirebaseAuthMultiFactorException) {
final FirebaseAuthMultiFactorException multiFactorException =
(FirebaseAuthMultiFactorException) e.getCause();
Map<String, Object> output = new HashMap<>();

MultiFactorResolver multiFactorResolver = multiFactorException.getResolver();
final List<MultiFactorInfo> hints = multiFactorResolver.getHints();

final MultiFactorSession session = multiFactorResolver.getSession();
final String sessionId = UUID.randomUUID().toString();
multiFactorSessionMap.put(sessionId, session);

final String resolverId = UUID.randomUUID().toString();
multiFactorResolverMap.put(resolverId, multiFactorResolver);

final List<Map<String, Object>> pigeonHints = multiFactorInfoToMap(hints);

output.put(Constants.APP_NAME, getAuth(arguments).getApp().getName());

output.put(Constants.MULTI_FACTOR_HINTS, pigeonHints);

output.put(Constants.MULTI_FACTOR_SESSION_ID, sessionId);
output.put(Constants.MULTI_FACTOR_RESOLVER_ID, resolverId);

taskCompletionSource.setException(
new FlutterFirebaseAuthPluginException(
multiFactorException.getErrorCode(),
multiFactorException.getLocalizedMessage(),
output));
} else {
taskCompletionSource.setException(e);
}
}
});

return taskCompletionSource.getTask();
}

private List<GeneratedAndroidFirebaseAuth.PigeonMultiFactorInfo> multiFactorInfoToPigeon(
List<MultiFactorInfo> hints) {
List<GeneratedAndroidFirebaseAuth.PigeonMultiFactorInfo> pigeonHints = new ArrayList<>();
for (MultiFactorInfo info : hints) {
if (info instanceof PhoneMultiFactorInfo) {
pigeonHints.add(
new GeneratedAndroidFirebaseAuth.PigeonMultiFactorInfo.Builder()
.setPhoneNumber(((PhoneMultiFactorInfo) info).getPhoneNumber())
.setDisplayName(info.getDisplayName())
.setEnrollmentTimestamp((double) info.getEnrollmentTimestamp())
.setUid(info.getUid())
.setFactorId(info.getFactorId())
.build());

} else {
pigeonHints.add(
new GeneratedAndroidFirebaseAuth.PigeonMultiFactorInfo.Builder()
.setDisplayName(info.getDisplayName())
.setEnrollmentTimestamp((double) info.getEnrollmentTimestamp())
.setUid(info.getUid())
.setFactorId(info.getFactorId())
.build());
}
}
return pigeonHints;
}

private List<Map<String, Object>> multiFactorInfoToMap(List<MultiFactorInfo> hints) {
List<Map<String, Object>> pigeonHints = new ArrayList<>();
for (GeneratedAndroidFirebaseAuth.PigeonMultiFactorInfo info : multiFactorInfoToPigeon(hints)) {
pigeonHints.add(info.toMap());
}
return pigeonHints;
}

private Task<Map<String, Object>> signInWithEmailLink(Map<String, Object> arguments) {
TaskCompletionSource<Map<String, Object>> taskCompletionSource = new TaskCompletionSource<>();

Expand Down Expand Up @@ -883,10 +972,36 @@ private Task<String> verifyPhoneNumber(Map<String, Object> arguments) {
String eventChannelName =
METHOD_CHANNEL_NAME + "/phone/" + UUID.randomUUID().toString();
EventChannel channel = new EventChannel(messenger, eventChannelName);

final String multiFactorSessionId =
(String) arguments.get(Constants.MULTI_FACTOR_SESSION_ID);
MultiFactorSession multiFactorSession = null;

if (multiFactorSessionId != null) {
multiFactorSession = multiFactorSessionMap.get(multiFactorSessionId);
}

final String multiFactorInfoId = (String) arguments.get(Constants.MULTI_FACTOR_INFO);
PhoneMultiFactorInfo multiFactorInfo = null;

if (multiFactorInfoId != null) {
for (String resolverId : multiFactorResolverMap.keySet()) {
for (MultiFactorInfo info : multiFactorResolverMap.get(resolverId).getHints()) {
if (info.getUid().equals(multiFactorInfoId)
&& info instanceof PhoneMultiFactorInfo) {
multiFactorInfo = (PhoneMultiFactorInfo) info;
break;
}
}
}
}

PhoneNumberVerificationStreamHandler handler =
new PhoneNumberVerificationStreamHandler(
getActivity(),
arguments,
multiFactorSession,
multiFactorInfo,
credential -> {
int hashCode = credential.hashCode();
authCredentials.put(hashCode, credential);
Expand Down Expand Up @@ -1573,4 +1688,169 @@ private void removeEventListeners() {
}
streamHandlers.clear();
}

// Map an app id to a map of user id to a MultiFactorUser object.
private final Map<String, Map<String, MultiFactor>> multiFactorUserMap = new HashMap<>();

// Map an id to a MultiFactorSession object.
private final Map<String, MultiFactorSession> multiFactorSessionMap = new HashMap<>();

// Map an id to a MultiFactorSession object.
private final Map<String, MultiFactorResolver> multiFactorResolverMap = new HashMap<>();

private MultiFactor getAppMultiFactor(@NonNull String appName)
throws FirebaseNoSignedInUserException {
final FirebaseUser currentUser = getCurrentUser(appName);
if (currentUser == null) {
throw new FirebaseNoSignedInUserException("No user is signed in");
}
if (multiFactorUserMap.get(appName) == null) {
multiFactorUserMap.put(appName, new HashMap<>());
}

final Map<String, MultiFactor> appMultiFactorUser = multiFactorUserMap.get(appName);
if (appMultiFactorUser.get(currentUser.getUid()) == null) {
appMultiFactorUser.put(currentUser.getUid(), currentUser.getMultiFactor());
}

final MultiFactor multiFactor = appMultiFactorUser.get(currentUser.getUid());
return multiFactor;
}

@Override
public void enrollPhone(
@NonNull String appName,
@NonNull GeneratedAndroidFirebaseAuth.PigeonPhoneMultiFactorAssertion assertion,
@Nullable String displayName,
GeneratedAndroidFirebaseAuth.Result<Void> result) {
final MultiFactor multiFactor;
try {
multiFactor = getAppMultiFactor(appName);
} catch (FirebaseNoSignedInUserException e) {
result.error(e);
return;
}

PhoneAuthCredential credential =
PhoneAuthProvider.getCredential(
assertion.getVerificationId(), assertion.getVerificationCode());

MultiFactorAssertion multiFactorAssertion = PhoneMultiFactorGenerator.getAssertion(credential);

multiFactor
.enroll(multiFactorAssertion, displayName)
.addOnCompleteListener(
task -> {
if (task.isSuccessful()) {
result.success(null);
} else {
result.error(task.getException());
}
});
}

@Override
public void getSession(
@NonNull String appName,
GeneratedAndroidFirebaseAuth.Result<GeneratedAndroidFirebaseAuth.PigeonMultiFactorSession>
result) {
final MultiFactor multiFactor;
try {
multiFactor = getAppMultiFactor(appName);
} catch (FirebaseNoSignedInUserException e) {
result.error(e);
return;
}

multiFactor
.getSession()
.addOnCompleteListener(
task -> {
if (task.isSuccessful()) {
final MultiFactorSession sessionResult = task.getResult();
final String id = UUID.randomUUID().toString();
multiFactorSessionMap.put(id, sessionResult);
result.success(
new GeneratedAndroidFirebaseAuth.PigeonMultiFactorSession.Builder()
.setId(id)
.build());
} else {
Exception exception = task.getException();
result.error(exception);
}
});
}

@Override
public void unenroll(
@NonNull String appName,
@Nullable String factorUid,
GeneratedAndroidFirebaseAuth.Result<Void> result) {
final MultiFactor multiFactor;
try {
multiFactor = getAppMultiFactor(appName);
} catch (FirebaseNoSignedInUserException e) {
result.error(e);
return;
}

multiFactor
.unenroll(factorUid)
.addOnCompleteListener(
task -> {
if (task.isSuccessful()) {
result.success(null);
} else {
result.error(task.getException());
}
});
}

@Override
public void getEnrolledFactors(
@NonNull String appName,
GeneratedAndroidFirebaseAuth.Result<List<GeneratedAndroidFirebaseAuth.PigeonMultiFactorInfo>>
result) {
final MultiFactor multiFactor;
try {
multiFactor = getAppMultiFactor(appName);
} catch (FirebaseNoSignedInUserException e) {
result.error(e);
return;
}

final List<MultiFactorInfo> factors = multiFactor.getEnrolledFactors();

final List<GeneratedAndroidFirebaseAuth.PigeonMultiFactorInfo> resultFactors =
multiFactorInfoToPigeon(factors);

result.success(resultFactors);
}

@Override
public void resolveSignIn(
@NonNull String resolverId,
@NonNull GeneratedAndroidFirebaseAuth.PigeonPhoneMultiFactorAssertion assertion,
GeneratedAndroidFirebaseAuth.Result<Map<String, Object>> result) {
final MultiFactorResolver resolver = multiFactorResolverMap.get(resolverId);

PhoneAuthCredential credential =
PhoneAuthProvider.getCredential(
assertion.getVerificationId(), assertion.getVerificationCode());

MultiFactorAssertion multiFactorAssertion = PhoneMultiFactorGenerator.getAssertion(credential);

resolver
.resolveSignIn(multiFactorAssertion)
.addOnCompleteListener(
task -> {
if (task.isSuccessful()) {
final AuthResult authResult = task.getResult();
result.success(parseAuthResult(authResult));
} else {
Exception exception = task.getException();
result.error(exception);
}
});
}
}

0 comments on commit 1b85c8b

Please sign in to comment.