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): TOTP (time-based one-time password) support for multi-factor authentication #11420

Merged
merged 20 commits into from
Aug 30, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,19 @@ public class FlutterFirebaseAuthPlugin
private final FlutterFirebaseAuthUser firebaseAuthUser = new FlutterFirebaseAuthUser();
private final FlutterFirebaseMultiFactor firebaseMultiFactor = new FlutterFirebaseMultiFactor();

private final FlutterFirebaseTotpMultiFactor firebaseTotpMultiFactor =
new FlutterFirebaseTotpMultiFactor();
private final FlutterFirebaseTotpSecret firebaseTotpSecret = new FlutterFirebaseTotpSecret();

private void initInstance(BinaryMessenger messenger) {
registerPlugin(METHOD_CHANNEL_NAME, this);
channel = new MethodChannel(messenger, METHOD_CHANNEL_NAME);
GeneratedAndroidFirebaseAuth.FirebaseAuthHostApi.setup(messenger, this);
GeneratedAndroidFirebaseAuth.FirebaseAuthUserHostApi.setup(messenger, firebaseAuthUser);
GeneratedAndroidFirebaseAuth.MultiFactorUserHostApi.setup(messenger, firebaseMultiFactor);
GeneratedAndroidFirebaseAuth.MultiFactoResolverHostApi.setup(messenger, firebaseMultiFactor);
GeneratedAndroidFirebaseAuth.MultiFactorTotpHostApi.setup(messenger, firebaseTotpMultiFactor);
GeneratedAndroidFirebaseAuth.MultiFactorTotpSecretHostApi.setup(messenger, firebaseTotpSecret);

this.messenger = messenger;
}
Expand All @@ -82,6 +88,8 @@ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
GeneratedAndroidFirebaseAuth.FirebaseAuthUserHostApi.setup(messenger, null);
GeneratedAndroidFirebaseAuth.MultiFactorUserHostApi.setup(messenger, null);
GeneratedAndroidFirebaseAuth.MultiFactoResolverHostApi.setup(messenger, null);
GeneratedAndroidFirebaseAuth.MultiFactorTotpHostApi.setup(messenger, null);
GeneratedAndroidFirebaseAuth.MultiFactorTotpSecretHostApi.setup(messenger, null);

channel = null;
messenger = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ public class FlutterFirebaseMultiFactor
// Map an id to a MultiFactorResolver object.
static final Map<String, MultiFactorResolver> multiFactorResolverMap = new HashMap<>();

static final Map<String, MultiFactorAssertion> multiFactorAssertionMap = new HashMap<>();

MultiFactor getAppMultiFactor(@NonNull GeneratedAndroidFirebaseAuth.PigeonFirebaseApp app)
throws FirebaseNoSignedInUserException {
final FirebaseUser currentUser = FlutterFirebaseAuthUser.getCurrentUserFromPigeon(app);
Expand Down Expand Up @@ -89,6 +91,37 @@ public void enrollPhone(
});
}

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

final MultiFactorAssertion multiFactorAssertion = multiFactorAssertionMap.get(assertionId);

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

@Override
public void getSession(
@NonNull GeneratedAndroidFirebaseAuth.PigeonFirebaseApp app,
Expand Down Expand Up @@ -176,7 +209,8 @@ public void getEnrolledFactors(
@Override
public void resolveSignIn(
@NonNull String resolverId,
@NonNull GeneratedAndroidFirebaseAuth.PigeonPhoneMultiFactorAssertion assertion,
@Nullable GeneratedAndroidFirebaseAuth.PigeonPhoneMultiFactorAssertion assertion,
@Nullable String totpAssertionId,
@NonNull
GeneratedAndroidFirebaseAuth.Result<GeneratedAndroidFirebaseAuth.PigeonUserCredential>
result) {
Expand All @@ -189,11 +223,16 @@ public void resolveSignIn(
return;
}

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

MultiFactorAssertion multiFactorAssertion = PhoneMultiFactorGenerator.getAssertion(credential);
if (assertion != null) {
PhoneAuthCredential credential =
PhoneAuthProvider.getCredential(
assertion.getVerificationId(), assertion.getVerificationCode());
multiFactorAssertion = PhoneMultiFactorGenerator.getAssertion(credential);
} else {
multiFactorAssertion = multiFactorAssertionMap.get(totpAssertionId);
}

resolver
.resolveSignIn(multiFactorAssertion)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright 2023, the Chromium project authors. Please see the AUTHORS file
* 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.
*/

package io.flutter.plugins.firebase.auth;

import androidx.annotation.NonNull;
import com.google.firebase.auth.MultiFactorSession;
import com.google.firebase.auth.TotpMultiFactorAssertion;
import com.google.firebase.auth.TotpMultiFactorGenerator;
import com.google.firebase.auth.TotpSecret;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

public class FlutterFirebaseTotpMultiFactor
implements GeneratedAndroidFirebaseAuth.MultiFactorTotpHostApi {

// Map an app id to a map of user id to a TotpSecret object.
static final Map<String, TotpSecret> multiFactorSecret = new HashMap<>();

@Override
public void generateSecret(
@NonNull String sessionId,
@NonNull
GeneratedAndroidFirebaseAuth.Result<GeneratedAndroidFirebaseAuth.PigeonTotpSecret>
result) {
MultiFactorSession multiFactorSession =
FlutterFirebaseMultiFactor.multiFactorSessionMap.get(sessionId);

assert multiFactorSession != null;
TotpMultiFactorGenerator.generateSecret(multiFactorSession)
.addOnCompleteListener(
task -> {
if (task.isSuccessful()) {
TotpSecret secret = task.getResult();
multiFactorSecret.put(secret.getSharedSecretKey(), secret);
result.success(
new GeneratedAndroidFirebaseAuth.PigeonTotpSecret.Builder()
.setCodeIntervalSeconds((long) secret.getCodeIntervalSeconds())
.setCodeLength((long) secret.getCodeLength())
.setSecretKey(secret.getSharedSecretKey())
.setHashingAlgorithm(secret.getHashAlgorithm())
.setEnrollmentCompletionDeadline(secret.getEnrollmentCompletionDeadline())
.build());
} else {
result.error(
FlutterFirebaseAuthPluginException.parserExceptionToFlutter(
task.getException()));
}
});
}

@Override
public void getAssertionForEnrollment(
@NonNull String secretKey,
@NonNull String oneTimePassword,
@NonNull GeneratedAndroidFirebaseAuth.Result<String> result) {
final TotpSecret secret = multiFactorSecret.get(secretKey);

assert secret != null;
TotpMultiFactorAssertion assertion =
TotpMultiFactorGenerator.getAssertionForEnrollment(secret, oneTimePassword);
String assertionId = UUID.randomUUID().toString();
FlutterFirebaseMultiFactor.multiFactorAssertionMap.put(assertionId, assertion);
result.success(assertionId);
}

@Override
public void getAssertionForSignIn(
@NonNull String enrollmentId,
@NonNull String oneTimePassword,
@NonNull GeneratedAndroidFirebaseAuth.Result<String> result) {
TotpMultiFactorAssertion assertion =
TotpMultiFactorGenerator.getAssertionForSignIn(enrollmentId, oneTimePassword);
String assertionId = UUID.randomUUID().toString();
FlutterFirebaseMultiFactor.multiFactorAssertionMap.put(assertionId, assertion);
result.success(assertionId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright 2023, the Chromium project authors. Please see the AUTHORS file
* 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.
*/

package io.flutter.plugins.firebase.auth;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.firebase.auth.TotpSecret;

public class FlutterFirebaseTotpSecret
implements GeneratedAndroidFirebaseAuth.MultiFactorTotpSecretHostApi {

@Override
public void generateQrCodeUrl(
@NonNull String secretKey,
@Nullable String accountName,
@Nullable String issuer,
@NonNull GeneratedAndroidFirebaseAuth.Result<String> result) {
final TotpSecret secret = FlutterFirebaseTotpMultiFactor.multiFactorSecret.get(secretKey);

assert secret != null;
if (accountName == null || issuer == null) {
result.success(secret.generateQrCodeUrl());
return;
}
result.success(secret.generateQrCodeUrl(accountName, issuer));
}

@Override
public void openInOtpApp(
@NonNull String secretKey,
@NonNull String qrCodeUrl,
@NonNull GeneratedAndroidFirebaseAuth.Result<Void> result) {
final TotpSecret secret = FlutterFirebaseTotpMultiFactor.multiFactorSecret.get(secretKey);
assert secret != null;
secret.openInOtpApp(qrCodeUrl);
result.success(null);
}
}