Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(auth): add phone MFA #9044

Merged
merged 8 commits into from Jul 20, 2022
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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" && \
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that &&\ needed? It wasn't there before

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without it, the second command would run even if the first one fails.
If there is an issue with the generation, it's better to not run the other commands IMO

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,145 @@ 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, GeneratedAndroidFirebaseAuth.Result result) {
final FirebaseUser currentUser = getCurrentUser(appName);
if (currentUser == null) {
result.error(new FirebaseNoSignedInUserException("No user is signed in"));
Lyokone marked this conversation as resolved.
Show resolved Hide resolved
}
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 = getAppMultiFactor(appName, result);

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 = getAppMultiFactor(appName, result);

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 = getAppMultiFactor(appName, result);

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 = getAppMultiFactor(appName, result);

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);
}
});
}
}
Expand Up @@ -24,6 +24,15 @@ public class FlutterFirebaseAuthPluginException extends Exception {
this.message = message;
}

FlutterFirebaseAuthPluginException(
@NonNull String code, @NonNull String message, @NonNull Map<String, Object> additionalData) {
super(message, null);

this.code = code;
this.message = message;
this.additionalData = additionalData;
}

FlutterFirebaseAuthPluginException(@NonNull Exception nativeException, Throwable cause) {
super(nativeException.getMessage(), cause);

Expand Down