Skip to content

Commit dc65a52

Browse files
Lyokonerussellwheatley
authored andcommitted
feat(auth): TOTP (time-based one-time password) support for multi-factor authentication (#11420)
Co-authored-by: russellwheatley <[email protected]>
1 parent 432c356 commit dc65a52

28 files changed

+2371
-113
lines changed

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,19 @@ public class FlutterFirebaseAuthPlugin
5757
private final FlutterFirebaseAuthUser firebaseAuthUser = new FlutterFirebaseAuthUser();
5858
private final FlutterFirebaseMultiFactor firebaseMultiFactor = new FlutterFirebaseMultiFactor();
5959

60+
private final FlutterFirebaseTotpMultiFactor firebaseTotpMultiFactor =
61+
new FlutterFirebaseTotpMultiFactor();
62+
private final FlutterFirebaseTotpSecret firebaseTotpSecret = new FlutterFirebaseTotpSecret();
63+
6064
private void initInstance(BinaryMessenger messenger) {
6165
registerPlugin(METHOD_CHANNEL_NAME, this);
6266
channel = new MethodChannel(messenger, METHOD_CHANNEL_NAME);
6367
GeneratedAndroidFirebaseAuth.FirebaseAuthHostApi.setup(messenger, this);
6468
GeneratedAndroidFirebaseAuth.FirebaseAuthUserHostApi.setup(messenger, firebaseAuthUser);
6569
GeneratedAndroidFirebaseAuth.MultiFactorUserHostApi.setup(messenger, firebaseMultiFactor);
6670
GeneratedAndroidFirebaseAuth.MultiFactoResolverHostApi.setup(messenger, firebaseMultiFactor);
71+
GeneratedAndroidFirebaseAuth.MultiFactorTotpHostApi.setup(messenger, firebaseTotpMultiFactor);
72+
GeneratedAndroidFirebaseAuth.MultiFactorTotpSecretHostApi.setup(messenger, firebaseTotpSecret);
6773

6874
this.messenger = messenger;
6975
}
@@ -82,6 +88,8 @@ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
8288
GeneratedAndroidFirebaseAuth.FirebaseAuthUserHostApi.setup(messenger, null);
8389
GeneratedAndroidFirebaseAuth.MultiFactorUserHostApi.setup(messenger, null);
8490
GeneratedAndroidFirebaseAuth.MultiFactoResolverHostApi.setup(messenger, null);
91+
GeneratedAndroidFirebaseAuth.MultiFactorTotpHostApi.setup(messenger, null);
92+
GeneratedAndroidFirebaseAuth.MultiFactorTotpSecretHostApi.setup(messenger, null);
8593

8694
channel = null;
8795
messenger = null;

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

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ public class FlutterFirebaseMultiFactor
3737
// Map an id to a MultiFactorResolver object.
3838
static final Map<String, MultiFactorResolver> multiFactorResolverMap = new HashMap<>();
3939

40+
static final Map<String, MultiFactorAssertion> multiFactorAssertionMap = new HashMap<>();
41+
4042
MultiFactor getAppMultiFactor(@NonNull GeneratedAndroidFirebaseAuth.PigeonFirebaseApp app)
4143
throws FirebaseNoSignedInUserException {
4244
final FirebaseUser currentUser = FlutterFirebaseAuthUser.getCurrentUserFromPigeon(app);
@@ -89,6 +91,37 @@ public void enrollPhone(
8991
});
9092
}
9193

94+
@Override
95+
public void enrollTotp(
96+
@NonNull GeneratedAndroidFirebaseAuth.PigeonFirebaseApp app,
97+
@NonNull String assertionId,
98+
@Nullable String displayName,
99+
@NonNull GeneratedAndroidFirebaseAuth.Result<Void> result) {
100+
final MultiFactor multiFactor;
101+
try {
102+
multiFactor = getAppMultiFactor(app);
103+
} catch (FirebaseNoSignedInUserException e) {
104+
result.error(e);
105+
return;
106+
}
107+
108+
final MultiFactorAssertion multiFactorAssertion = multiFactorAssertionMap.get(assertionId);
109+
110+
assert multiFactorAssertion != null;
111+
multiFactor
112+
.enroll(multiFactorAssertion, displayName)
113+
.addOnCompleteListener(
114+
task -> {
115+
if (task.isSuccessful()) {
116+
result.success(null);
117+
} else {
118+
result.error(
119+
FlutterFirebaseAuthPluginException.parserExceptionToFlutter(
120+
task.getException()));
121+
}
122+
});
123+
}
124+
92125
@Override
93126
public void getSession(
94127
@NonNull GeneratedAndroidFirebaseAuth.PigeonFirebaseApp app,
@@ -176,7 +209,8 @@ public void getEnrolledFactors(
176209
@Override
177210
public void resolveSignIn(
178211
@NonNull String resolverId,
179-
@NonNull GeneratedAndroidFirebaseAuth.PigeonPhoneMultiFactorAssertion assertion,
212+
@Nullable GeneratedAndroidFirebaseAuth.PigeonPhoneMultiFactorAssertion assertion,
213+
@Nullable String totpAssertionId,
180214
@NonNull
181215
GeneratedAndroidFirebaseAuth.Result<GeneratedAndroidFirebaseAuth.PigeonUserCredential>
182216
result) {
@@ -189,11 +223,16 @@ public void resolveSignIn(
189223
return;
190224
}
191225

192-
PhoneAuthCredential credential =
193-
PhoneAuthProvider.getCredential(
194-
assertion.getVerificationId(), assertion.getVerificationCode());
226+
MultiFactorAssertion multiFactorAssertion;
195227

196-
MultiFactorAssertion multiFactorAssertion = PhoneMultiFactorGenerator.getAssertion(credential);
228+
if (assertion != null) {
229+
PhoneAuthCredential credential =
230+
PhoneAuthProvider.getCredential(
231+
assertion.getVerificationId(), assertion.getVerificationCode());
232+
multiFactorAssertion = PhoneMultiFactorGenerator.getAssertion(credential);
233+
} else {
234+
multiFactorAssertion = multiFactorAssertionMap.get(totpAssertionId);
235+
}
197236

198237
resolver
199238
.resolveSignIn(multiFactorAssertion)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright 2023, the Chromium project authors. Please see the AUTHORS file
3+
* for details. All rights reserved. Use of this source code is governed by a
4+
* BSD-style license that can be found in the LICENSE file.
5+
*/
6+
7+
package io.flutter.plugins.firebase.auth;
8+
9+
import androidx.annotation.NonNull;
10+
import com.google.firebase.auth.MultiFactorSession;
11+
import com.google.firebase.auth.TotpMultiFactorAssertion;
12+
import com.google.firebase.auth.TotpMultiFactorGenerator;
13+
import com.google.firebase.auth.TotpSecret;
14+
import java.util.HashMap;
15+
import java.util.Map;
16+
import java.util.UUID;
17+
18+
public class FlutterFirebaseTotpMultiFactor
19+
implements GeneratedAndroidFirebaseAuth.MultiFactorTotpHostApi {
20+
21+
// Map an app id to a map of user id to a TotpSecret object.
22+
static final Map<String, TotpSecret> multiFactorSecret = new HashMap<>();
23+
24+
@Override
25+
public void generateSecret(
26+
@NonNull String sessionId,
27+
@NonNull
28+
GeneratedAndroidFirebaseAuth.Result<GeneratedAndroidFirebaseAuth.PigeonTotpSecret>
29+
result) {
30+
MultiFactorSession multiFactorSession =
31+
FlutterFirebaseMultiFactor.multiFactorSessionMap.get(sessionId);
32+
33+
assert multiFactorSession != null;
34+
TotpMultiFactorGenerator.generateSecret(multiFactorSession)
35+
.addOnCompleteListener(
36+
task -> {
37+
if (task.isSuccessful()) {
38+
TotpSecret secret = task.getResult();
39+
multiFactorSecret.put(secret.getSharedSecretKey(), secret);
40+
result.success(
41+
new GeneratedAndroidFirebaseAuth.PigeonTotpSecret.Builder()
42+
.setCodeIntervalSeconds((long) secret.getCodeIntervalSeconds())
43+
.setCodeLength((long) secret.getCodeLength())
44+
.setSecretKey(secret.getSharedSecretKey())
45+
.setHashingAlgorithm(secret.getHashAlgorithm())
46+
.setEnrollmentCompletionDeadline(secret.getEnrollmentCompletionDeadline())
47+
.build());
48+
} else {
49+
result.error(
50+
FlutterFirebaseAuthPluginException.parserExceptionToFlutter(
51+
task.getException()));
52+
}
53+
});
54+
}
55+
56+
@Override
57+
public void getAssertionForEnrollment(
58+
@NonNull String secretKey,
59+
@NonNull String oneTimePassword,
60+
@NonNull GeneratedAndroidFirebaseAuth.Result<String> result) {
61+
final TotpSecret secret = multiFactorSecret.get(secretKey);
62+
63+
assert secret != null;
64+
TotpMultiFactorAssertion assertion =
65+
TotpMultiFactorGenerator.getAssertionForEnrollment(secret, oneTimePassword);
66+
String assertionId = UUID.randomUUID().toString();
67+
FlutterFirebaseMultiFactor.multiFactorAssertionMap.put(assertionId, assertion);
68+
result.success(assertionId);
69+
}
70+
71+
@Override
72+
public void getAssertionForSignIn(
73+
@NonNull String enrollmentId,
74+
@NonNull String oneTimePassword,
75+
@NonNull GeneratedAndroidFirebaseAuth.Result<String> result) {
76+
TotpMultiFactorAssertion assertion =
77+
TotpMultiFactorGenerator.getAssertionForSignIn(enrollmentId, oneTimePassword);
78+
String assertionId = UUID.randomUUID().toString();
79+
FlutterFirebaseMultiFactor.multiFactorAssertionMap.put(assertionId, assertion);
80+
result.success(assertionId);
81+
}
82+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2023, the Chromium project authors. Please see the AUTHORS file
3+
* for details. All rights reserved. Use of this source code is governed by a
4+
* BSD-style license that can be found in the LICENSE file.
5+
*/
6+
7+
package io.flutter.plugins.firebase.auth;
8+
9+
import androidx.annotation.NonNull;
10+
import androidx.annotation.Nullable;
11+
import com.google.firebase.auth.TotpSecret;
12+
13+
public class FlutterFirebaseTotpSecret
14+
implements GeneratedAndroidFirebaseAuth.MultiFactorTotpSecretHostApi {
15+
16+
@Override
17+
public void generateQrCodeUrl(
18+
@NonNull String secretKey,
19+
@Nullable String accountName,
20+
@Nullable String issuer,
21+
@NonNull GeneratedAndroidFirebaseAuth.Result<String> result) {
22+
final TotpSecret secret = FlutterFirebaseTotpMultiFactor.multiFactorSecret.get(secretKey);
23+
24+
assert secret != null;
25+
if (accountName == null || issuer == null) {
26+
result.success(secret.generateQrCodeUrl());
27+
return;
28+
}
29+
result.success(secret.generateQrCodeUrl(accountName, issuer));
30+
}
31+
32+
@Override
33+
public void openInOtpApp(
34+
@NonNull String secretKey,
35+
@NonNull String qrCodeUrl,
36+
@NonNull GeneratedAndroidFirebaseAuth.Result<Void> result) {
37+
final TotpSecret secret = FlutterFirebaseTotpMultiFactor.multiFactorSecret.get(secretKey);
38+
assert secret != null;
39+
secret.openInOtpApp(qrCodeUrl);
40+
result.success(null);
41+
}
42+
}

0 commit comments

Comments
 (0)