Skip to content

Commit 1c40f84

Browse files
James Batemanmikehardy
authored andcommitted
feat(auth): support TOTP enroll and unenroll
1 parent 9d2a393 commit 1c40f84

File tree

8 files changed

+409
-8
lines changed

8 files changed

+409
-8
lines changed

packages/auth/android/src/main/java/io/invertase/firebase/auth/ReactNativeFirebaseAuthModule.java

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@
6666
import com.google.firebase.auth.PhoneMultiFactorAssertion;
6767
import com.google.firebase.auth.PhoneMultiFactorGenerator;
6868
import com.google.firebase.auth.PhoneMultiFactorInfo;
69+
import com.google.firebase.auth.TotpMultiFactorAssertion;
70+
import com.google.firebase.auth.TotpMultiFactorGenerator;
71+
import com.google.firebase.auth.TotpSecret;
6972
import com.google.firebase.auth.TwitterAuthProvider;
7073
import com.google.firebase.auth.UserInfo;
7174
import com.google.firebase.auth.UserProfileChangeRequest;
@@ -107,6 +110,7 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {
107110

108111
private final HashMap<String, MultiFactorResolver> mCachedResolvers = new HashMap<>();
109112
private final HashMap<String, MultiFactorSession> mMultiFactorSessions = new HashMap<>();
113+
private final HashMap<String, TotpSecret> mTotpSecrets = new HashMap<>();
110114

111115
// storage for anonymous phone auth credentials, used for linkWithCredentials
112116
// https://github.com/invertase/react-native-firebase/issues/4911
@@ -154,6 +158,7 @@ public void invalidate() {
154158

155159
mCachedResolvers.clear();
156160
mMultiFactorSessions.clear();
161+
mTotpSecrets.clear();
157162
}
158163

159164
@ReactMethod
@@ -1130,6 +1135,26 @@ public void getSession(final String appName, final Promise promise) {
11301135
});
11311136
}
11321137

1138+
@ReactMethod
1139+
public void unenrollMultiFactor(
1140+
final String appName, final String factorUID, final Promise promise) {
1141+
FirebaseApp firebaseApp = FirebaseApp.getInstance(appName);
1142+
FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp);
1143+
firebaseAuth
1144+
.getCurrentUser()
1145+
.getMultiFactor()
1146+
.unenroll(factorUID)
1147+
.addOnCompleteListener(
1148+
task -> {
1149+
if (!task.isSuccessful()) {
1150+
rejectPromiseWithExceptionMap(promise, task.getException());
1151+
return;
1152+
}
1153+
1154+
promise.resolve(null);
1155+
});
1156+
}
1157+
11331158
@ReactMethod
11341159
public void verifyPhoneNumberWithMultiFactorInfo(
11351160
final String appName, final String hintUid, final String sessionKey, final Promise promise) {
@@ -1280,6 +1305,42 @@ public void finalizeMultiFactorEnrollment(
12801305
});
12811306
}
12821307

1308+
@ReactMethod
1309+
public void finalizeTotpEnrollment(
1310+
final String appName,
1311+
final String totpSecret,
1312+
final String verificationCode,
1313+
@Nullable final String displayName,
1314+
final Promise promise) {
1315+
1316+
TotpSecret secret = mTotpSecrets.get(totpSecret);
1317+
if (secret == null) {
1318+
rejectPromiseWithCodeAndMessage(
1319+
promise, "invalid-multi-factor-secret", "can't find secret for provided key");
1320+
return;
1321+
}
1322+
1323+
TotpMultiFactorAssertion assertion =
1324+
TotpMultiFactorGenerator.getAssertionForEnrollment(secret, verificationCode);
1325+
1326+
FirebaseApp firebaseApp = FirebaseApp.getInstance(appName);
1327+
FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp);
1328+
1329+
firebaseAuth
1330+
.getCurrentUser()
1331+
.getMultiFactor()
1332+
.enroll(assertion, displayName)
1333+
.addOnCompleteListener(
1334+
task -> {
1335+
if (!task.isSuccessful()) {
1336+
rejectPromiseWithExceptionMap(promise, task.getException());
1337+
return;
1338+
}
1339+
1340+
promise.resolve(null);
1341+
});
1342+
}
1343+
12831344
/**
12841345
* This method is intended to resolve a {@link PhoneAuthCredential} obtained through a
12851346
* multi-factor authentication flow. A credential can either be obtained using:
@@ -1335,6 +1396,70 @@ public void resolveMultiFactorSignIn(
13351396
resolveMultiFactorCredential(credential, session, promise);
13361397
}
13371398

1399+
@ReactMethod
1400+
public void resolveTotpSignIn(
1401+
final String appName,
1402+
final String sessionKey,
1403+
final String uid,
1404+
final String oneTimePassword,
1405+
final Promise promise) {
1406+
1407+
final MultiFactorAssertion assertion =
1408+
TotpMultiFactorGenerator.getAssertionForSignIn(uid, oneTimePassword);
1409+
1410+
final MultiFactorResolver resolver = mCachedResolvers.get(sessionKey);
1411+
if (resolver == null) {
1412+
// See https://firebase.google.com/docs/reference/node/firebase.auth.multifactorresolver for
1413+
// the error code
1414+
rejectPromiseWithCodeAndMessage(
1415+
promise,
1416+
"invalid-multi-factor-session",
1417+
"No resolver for session found. Is the session id correct?");
1418+
return;
1419+
}
1420+
1421+
resolver
1422+
.resolveSignIn(assertion)
1423+
.addOnCompleteListener(
1424+
task -> {
1425+
if (task.isSuccessful()) {
1426+
AuthResult authResult = task.getResult();
1427+
promiseWithAuthResult(authResult, promise);
1428+
} else {
1429+
promiseRejectAuthException(promise, task.getException());
1430+
}
1431+
});
1432+
}
1433+
1434+
@ReactMethod
1435+
public void generateTotpSecret(
1436+
final String appName, final String sessionKey, final Promise promise) {
1437+
1438+
final MultiFactorSession session = mMultiFactorSessions.get(sessionKey);
1439+
if (session == null) {
1440+
rejectPromiseWithCodeAndMessage(
1441+
promise,
1442+
"invalid-multi-factor-session",
1443+
"No resolver for session found. Is the session id correct?");
1444+
return;
1445+
}
1446+
1447+
TotpMultiFactorGenerator.generateSecret(session)
1448+
.addOnCompleteListener(
1449+
task -> {
1450+
if (task.isSuccessful()) {
1451+
TotpSecret totpSecret = task.getResult();
1452+
String totpSecretKey = totpSecret.getSharedSecretKey();
1453+
mTotpSecrets.put(totpSecretKey, totpSecret);
1454+
WritableMap result = Arguments.createMap();
1455+
result.putString("secretKey", totpSecretKey);
1456+
promise.resolve(result);
1457+
} else {
1458+
promiseRejectAuthException(promise, task.getException());
1459+
}
1460+
});
1461+
}
1462+
13381463
@ReactMethod
13391464
public void confirmationResultConfirm(
13401465
String appName, final String verificationCode, final Promise promise) {

packages/auth/ios/RNFBAuth/RNFBAuthModule.m

Lines changed: 112 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
#if TARGET_OS_IOS
5959
static __strong NSMutableDictionary<NSString *, FIRMultiFactorResolver *> *cachedResolver;
6060
static __strong NSMutableDictionary<NSString *, FIRMultiFactorSession *> *cachedSessions;
61+
static __strong NSMutableDictionary<NSString *, FIRTOTPSecret *> *cachedTotpSecrets;
6162
#endif
6263

6364
@implementation RNFBAuthModule
@@ -81,6 +82,7 @@ - (id)init {
8182
#if TARGET_OS_IOS
8283
cachedResolver = [[NSMutableDictionary alloc] init];
8384
cachedSessions = [[NSMutableDictionary alloc] init];
85+
cachedTotpSecrets = [[NSMutableDictionary alloc] init];
8486
#endif
8587
});
8688
return self;
@@ -110,6 +112,7 @@ - (void)invalidate {
110112
#if TARGET_OS_IOS
111113
[cachedResolver removeAllObjects];
112114
[cachedSessions removeAllObjects];
115+
[cachedTotpSecrets removeAllObjects];
113116
#endif
114117
}
115118

@@ -967,6 +970,62 @@ - (void)invalidate {
967970
}];
968971
}
969972

973+
RCT_EXPORT_METHOD(resolveTotpSignIn
974+
: (FIRApp *)firebaseApp
975+
: (NSString *)sessionKey
976+
: (NSString *)uid
977+
: (NSString *)oneTimePassword
978+
: (RCTPromiseResolveBlock)resolve
979+
: (RCTPromiseRejectBlock)reject) {
980+
DLog(@"using instance resolve TotpSignIn: %@", firebaseApp.name);
981+
982+
FIRMultiFactorAssertion *assertion =
983+
[FIRTOTPMultiFactorGenerator assertionForSignInWithEnrollmentID:uid
984+
oneTimePassword:oneTimePassword];
985+
[cachedResolver[sessionKey] resolveSignInWithAssertion:assertion
986+
completion:^(FIRAuthDataResult *_Nullable authResult,
987+
NSError *_Nullable error) {
988+
DLog(@"authError: %@", error) if (error) {
989+
[self promiseRejectAuthException:reject
990+
error:error];
991+
}
992+
else {
993+
[self promiseWithAuthResult:resolve
994+
rejecter:reject
995+
authResult:authResult];
996+
}
997+
}];
998+
}
999+
1000+
RCT_EXPORT_METHOD(generateTotpSecret
1001+
: (FIRApp *)firebaseApp
1002+
: (NSString *)sessionKey
1003+
: (RCTPromiseResolveBlock)resolve
1004+
: (RCTPromiseRejectBlock)reject) {
1005+
DLog(@"using instance resolve generateTotpSecret: %@", firebaseApp.name);
1006+
1007+
FIRMultiFactorSession *session = cachedSessions[sessionKey];
1008+
DLog(@"using sessionKey: %@", sessionKey);
1009+
DLog(@"using session: %@", session);
1010+
[FIRTOTPMultiFactorGenerator
1011+
generateSecretWithMultiFactorSession:session
1012+
completion:^(FIRTOTPSecret *_Nullable totpSecret,
1013+
NSError *_Nullable error) {
1014+
DLog(@"authError: %@", error) if (error) {
1015+
[self promiseRejectAuthException:reject error:error];
1016+
}
1017+
else {
1018+
NSString *secretKey = totpSecret.sharedSecretKey;
1019+
DLog(@"secretKey generated: %@", secretKey);
1020+
cachedTotpSecrets[secretKey] = totpSecret;
1021+
DLog(@"cachedSecret: %@", cachedTotpSecrets[secretKey]);
1022+
resolve(@{
1023+
@"secretKey" : secretKey,
1024+
});
1025+
}
1026+
}];
1027+
}
1028+
9701029
RCT_EXPORT_METHOD(getSession
9711030
: (FIRApp *)firebaseApp
9721031
: (RCTPromiseResolveBlock)resolve
@@ -985,6 +1044,26 @@ - (void)invalidate {
9851044
}];
9861045
}
9871046

1047+
RCT_EXPORT_METHOD(unenrollMultiFactor
1048+
: (FIRApp *)firebaseApp
1049+
: (NSString *)factorUID
1050+
: (RCTPromiseResolveBlock)resolve
1051+
: (RCTPromiseRejectBlock)reject) {
1052+
DLog(@"using instance unenrollMultiFactor: %@", firebaseApp.name);
1053+
1054+
FIRUser *user = [FIRAuth authWithApp:firebaseApp].currentUser;
1055+
[user.multiFactor unenrollWithFactorUID:factorUID
1056+
completion:^(NSError *_Nullable error) {
1057+
if (error != nil) {
1058+
[self promiseRejectAuthException:reject error:error];
1059+
return;
1060+
}
1061+
1062+
resolve(nil);
1063+
return;
1064+
}];
1065+
}
1066+
9881067
RCT_EXPORT_METHOD(finalizeMultiFactorEnrollment
9891068
: (FIRApp *)firebaseApp
9901069
: (NSString *)verificationId
@@ -1014,6 +1093,37 @@ - (void)invalidate {
10141093
}];
10151094
}
10161095

1096+
RCT_EXPORT_METHOD(finalizeTotpEnrollment
1097+
: (FIRApp *)firebaseApp
1098+
: (NSString *)totpSecret
1099+
: (NSString *)verificationCode
1100+
: (NSString *_Nullable)displayName
1101+
: (RCTPromiseResolveBlock)resolve
1102+
: (RCTPromiseRejectBlock)reject) {
1103+
DLog(@"using instance finalizeTotpEnrollment: %@", firebaseApp.name);
1104+
1105+
FIRTOTPSecret *cachedTotpSecret = cachedTotpSecrets[totpSecret];
1106+
DLog(@"using totpSecretKey: %@", totpSecret);
1107+
DLog(@"using cachedSecret: %@", cachedTotpSecret);
1108+
FIRTOTPMultiFactorAssertion *assertion =
1109+
[FIRTOTPMultiFactorGenerator assertionForEnrollmentWithSecret:cachedTotpSecret
1110+
oneTimePassword:verificationCode];
1111+
1112+
FIRUser *user = [FIRAuth authWithApp:firebaseApp].currentUser;
1113+
1114+
[user.multiFactor enrollWithAssertion:assertion
1115+
displayName:displayName
1116+
completion:^(NSError *_Nullable error) {
1117+
if (error != nil) {
1118+
[self promiseRejectAuthException:reject error:error];
1119+
return;
1120+
}
1121+
1122+
resolve(nil);
1123+
return;
1124+
}];
1125+
}
1126+
10171127
RCT_EXPORT_METHOD(verifyPhoneNumber
10181128
: (FIRApp *)firebaseApp
10191129
: (NSString *)phoneNumber
@@ -1741,12 +1851,12 @@ - (NSDictionary *)firebaseUserToDict:(FIRUser *)user {
17411851
@"enrollmentDate" : enrollmentTime,
17421852
} mutableCopy];
17431853

1744-
// only support phone mutli factor
17451854
if ([hint isKindOfClass:[FIRPhoneMultiFactorInfo class]]) {
17461855
FIRPhoneMultiFactorInfo *phoneHint = (FIRPhoneMultiFactorInfo *)hint;
17471856
factorDict[@"phoneNumber"] = phoneHint.phoneNumber;
1748-
[enrolledFactors addObject:factorDict];
17491857
}
1858+
1859+
[enrolledFactors addObject:factorDict];
17501860
}
17511861
return enrolledFactors;
17521862
}

packages/auth/lib/MultiFactorResolver.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@ export default class MultiFactorResolver {
99
}
1010

1111
resolveSignIn(assertion) {
12-
const { token, secret } = assertion;
13-
return this._auth.resolveMultiFactorSignIn(this.session, token, secret);
12+
const { token, secret, uid, verificationCode } = assertion;
13+
14+
if (token && secret) {
15+
return this._auth.resolveMultiFactorSignIn(this.session, token, secret);
16+
}
17+
18+
return this._auth.resolveTotpSignIn(this.session, uid, verificationCode);
1419
}
1520
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { TotpSecret } from './TotpSecret';
2+
3+
/*
4+
* Copyright (c) 2016-present Invertase Limited & Contributors
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this library except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*
18+
*/
19+
20+
export default class TotpMultiFactorGenerator {
21+
static FACTOR_ID = 'totp';
22+
23+
constructor() {
24+
throw new Error(
25+
'`new TotpMultiFactorGenerator()` is not supported on the native Firebase SDKs.',
26+
);
27+
}
28+
29+
static assertionForSignIn(uid, verificationCode) {
30+
return { uid, verificationCode };
31+
}
32+
33+
static assertionForEnrollment(totpSecret, verificationCode) {
34+
return { totpSecret: totpSecret.secretKey, verificationCode };
35+
}
36+
37+
static async generateSecret(session, auth) {
38+
if (!session) {
39+
throw new Error('Session is required to generate a TOTP secret.');
40+
}
41+
const {
42+
secretKey,
43+
// Other properties are not publicly exposed in native APIs
44+
// hashingAlgorithm, codeLength, codeIntervalSeconds, enrollmentCompletionDeadline
45+
} = await auth.native.generateTotpSecret(session);
46+
47+
return new TotpSecret(secretKey);
48+
}
49+
}

0 commit comments

Comments
 (0)