Skip to content

Commit f1b95ad

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

File tree

8 files changed

+414
-8
lines changed

8 files changed

+414
-8
lines changed

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

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,11 @@
6464
import com.google.firebase.auth.PhoneAuthOptions;
6565
import com.google.firebase.auth.PhoneAuthProvider;
6666
import com.google.firebase.auth.PhoneMultiFactorAssertion;
67+
import com.google.firebase.auth.TotpMultiFactorAssertion;
68+
import com.google.firebase.auth.TotpMultiFactorGenerator;
6769
import com.google.firebase.auth.PhoneMultiFactorGenerator;
6870
import com.google.firebase.auth.PhoneMultiFactorInfo;
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,29 @@ public void getSession(final String appName, final Promise promise) {
11301135
});
11311136
}
11321137

1138+
@ReactMethod
1139+
public void unenrollMultiFactor(
1140+
final String appName,
1141+
final String factorUID,
1142+
final Promise promise) {
1143+
FirebaseApp firebaseApp = FirebaseApp.getInstance(appName);
1144+
FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp);
1145+
firebaseAuth
1146+
.getCurrentUser()
1147+
.getMultiFactor()
1148+
.unenroll(factorUID)
1149+
.addOnCompleteListener(
1150+
task -> {
1151+
if (!task.isSuccessful()) {
1152+
rejectPromiseWithExceptionMap(promise, task.getException());
1153+
return;
1154+
}
1155+
1156+
promise.resolve(null);
1157+
});
1158+
1159+
}
1160+
11331161
@ReactMethod
11341162
public void verifyPhoneNumberWithMultiFactorInfo(
11351163
final String appName, final String hintUid, final String sessionKey, final Promise promise) {
@@ -1280,6 +1308,42 @@ public void finalizeMultiFactorEnrollment(
12801308
});
12811309
}
12821310

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

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

packages/auth/ios/RNFBAuth/RNFBAuthModule.m

Lines changed: 109 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,61 @@ - (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 oneTimePassword:oneTimePassword];
984+
[cachedResolver[sessionKey] resolveSignInWithAssertion:assertion
985+
completion:^(FIRAuthDataResult *_Nullable authResult,
986+
NSError *_Nullable error) {
987+
DLog(@"authError: %@", error) if (error) {
988+
[self promiseRejectAuthException:reject
989+
error:error];
990+
}
991+
else {
992+
[self promiseWithAuthResult:resolve
993+
rejecter:reject
994+
authResult:authResult];
995+
}
996+
}];
997+
}
998+
999+
RCT_EXPORT_METHOD(generateTotpSecret
1000+
: (FIRApp *)firebaseApp
1001+
: (NSString *)sessionKey
1002+
: (RCTPromiseResolveBlock)resolve
1003+
: (RCTPromiseRejectBlock)reject) {
1004+
DLog(@"using instance resolve generateTotpSecret: %@", firebaseApp.name);
1005+
1006+
FIRMultiFactorSession *session = cachedSessions[sessionKey];
1007+
DLog(@"using sessionKey: %@", sessionKey);
1008+
DLog(@"using session: %@", session);
1009+
[FIRTOTPMultiFactorGenerator generateSecretWithMultiFactorSession:session
1010+
completion:^(FIRTOTPSecret * _Nullable totpSecret, NSError * _Nullable error) {
1011+
DLog(@"authError: %@", error) if (error) {
1012+
[self promiseRejectAuthException:reject
1013+
error:error];
1014+
}
1015+
else {
1016+
NSString *secretKey = totpSecret.sharedSecretKey;
1017+
DLog(@"secretKey generated: %@", secretKey);
1018+
cachedTotpSecrets[secretKey] = totpSecret;
1019+
DLog(@"cachedSecret: %@", cachedTotpSecrets[secretKey]);
1020+
resolve(@{
1021+
@"secretKey": secretKey,
1022+
});
1023+
}
1024+
}];
1025+
1026+
}
1027+
9701028
RCT_EXPORT_METHOD(getSession
9711029
: (FIRApp *)firebaseApp
9721030
: (RCTPromiseResolveBlock)resolve
@@ -985,6 +1043,26 @@ - (void)invalidate {
9851043
}];
9861044
}
9871045

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

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

1744-
// only support phone mutli factor
17451851
if ([hint isKindOfClass:[FIRPhoneMultiFactorInfo class]]) {
17461852
FIRPhoneMultiFactorInfo *phoneHint = (FIRPhoneMultiFactorInfo *)hint;
17471853
factorDict[@"phoneNumber"] = phoneHint.phoneNumber;
1748-
[enrolledFactors addObject:factorDict];
17491854
}
1855+
1856+
[enrolledFactors addObject:factorDict];
17501857
}
17511858
return enrolledFactors;
17521859
}

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
}

0 commit comments

Comments
 (0)