Skip to content

feat(auth): TOTP and unenroll MFA #8621

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

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,11 @@
import com.google.firebase.auth.PhoneAuthOptions;
import com.google.firebase.auth.PhoneAuthProvider;
import com.google.firebase.auth.PhoneMultiFactorAssertion;
import com.google.firebase.auth.TotpMultiFactorAssertion;
import com.google.firebase.auth.TotpMultiFactorGenerator;
import com.google.firebase.auth.PhoneMultiFactorGenerator;
import com.google.firebase.auth.PhoneMultiFactorInfo;
import com.google.firebase.auth.TotpSecret;
import com.google.firebase.auth.TwitterAuthProvider;
import com.google.firebase.auth.UserInfo;
import com.google.firebase.auth.UserProfileChangeRequest;
Expand Down Expand Up @@ -107,6 +110,7 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {

private final HashMap<String, MultiFactorResolver> mCachedResolvers = new HashMap<>();
private final HashMap<String, MultiFactorSession> mMultiFactorSessions = new HashMap<>();
private final HashMap<String, TotpSecret> mTotpSecrets = new HashMap<>();

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

mCachedResolvers.clear();
mMultiFactorSessions.clear();
mTotpSecrets.clear();
}

@ReactMethod
Expand Down Expand Up @@ -1130,6 +1135,29 @@ public void getSession(final String appName, final Promise promise) {
});
}

@ReactMethod
public void unenrollMultiFactor(
final String appName,
final String factorUID,
final Promise promise) {
FirebaseApp firebaseApp = FirebaseApp.getInstance(appName);
FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp);
firebaseAuth
.getCurrentUser()
.getMultiFactor()
.unenroll(factorUID)
.addOnCompleteListener(
task -> {
if (!task.isSuccessful()) {
rejectPromiseWithExceptionMap(promise, task.getException());
return;
}

promise.resolve(null);
});

}

@ReactMethod
public void verifyPhoneNumberWithMultiFactorInfo(
final String appName, final String hintUid, final String sessionKey, final Promise promise) {
Expand Down Expand Up @@ -1280,6 +1308,42 @@ public void finalizeMultiFactorEnrollment(
});
}

@ReactMethod
public void finalizeTotpEnrollment(
final String appName,
final String totpSecret,
final String verificationCode,
@Nullable final String displayName,
final Promise promise) {

TotpSecret secret = mTotpSecrets.get(totpSecret);
if (secret == null) {
rejectPromiseWithCodeAndMessage(
promise, "invalid-multi-factor-secret", "can't find secret for provided key");
return;
}

TotpMultiFactorAssertion assertion =
TotpMultiFactorGenerator.getAssertionForEnrollment(secret, verificationCode);

FirebaseApp firebaseApp = FirebaseApp.getInstance(appName);
FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp);

firebaseAuth
.getCurrentUser()
.getMultiFactor()
.enroll(assertion, displayName)
.addOnCompleteListener(
task -> {
if (!task.isSuccessful()) {
rejectPromiseWithExceptionMap(promise, task.getException());
return;
}

promise.resolve(null);
});
}

/**
* This method is intended to resolve a {@link PhoneAuthCredential} obtained through a
* multi-factor authentication flow. A credential can either be obtained using:
Expand Down Expand Up @@ -1335,6 +1399,75 @@ public void resolveMultiFactorSignIn(
resolveMultiFactorCredential(credential, session, promise);
}

@ReactMethod
public void resolveTotpSignIn(
final String appName,
final String sessionKey,
final String uid,
final String oneTimePassword,
final Promise promise) {

final MultiFactorAssertion assertion =
TotpMultiFactorGenerator.getAssertionForSignIn(uid, oneTimePassword);

final MultiFactorResolver resolver = mCachedResolvers.get(sessionKey);
if (resolver == null) {
// See https://firebase.google.com/docs/reference/node/firebase.auth.multifactorresolver for
// the error code
rejectPromiseWithCodeAndMessage(
promise,
"invalid-multi-factor-session",
"No resolver for session found. Is the session id correct?");
return;
}


resolver
.resolveSignIn(assertion)
.addOnCompleteListener(
task -> {
if (task.isSuccessful()) {
AuthResult authResult = task.getResult();
promiseWithAuthResult(authResult, promise);
} else {
promiseRejectAuthException(promise, task.getException());
}
});

}

@ReactMethod
public void generateTotpSecret(
final String appName,
final String sessionKey,
final Promise promise) {

final MultiFactorSession session = mMultiFactorSessions.get(sessionKey);
if (session == null) {
rejectPromiseWithCodeAndMessage(
promise,
"invalid-multi-factor-session",
"No resolver for session found. Is the session id correct?");
return;
}

TotpMultiFactorGenerator
.generateSecret(session)
.addOnCompleteListener(
task -> {
if (task.isSuccessful()) {
TotpSecret totpSecret = task.getResult();
String totpSecretKey = totpSecret.getSharedSecretKey();
mTotpSecrets.put(totpSecretKey, totpSecret);
WritableMap result = Arguments.createMap();
result.putString("secretKey", totpSecretKey);
promise.resolve(result);
} else {
promiseRejectAuthException(promise, task.getException());
}
});
}

@ReactMethod
public void confirmationResultConfirm(
String appName, final String verificationCode, final Promise promise) {
Expand Down
142 changes: 130 additions & 12 deletions packages/auth/ios/RNFBAuth/RNFBAuthModule.m
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
#if TARGET_OS_IOS
static __strong NSMutableDictionary<NSString *, FIRMultiFactorResolver *> *cachedResolver;
static __strong NSMutableDictionary<NSString *, FIRMultiFactorSession *> *cachedSessions;
static __strong NSMutableDictionary<NSString *, FIRTOTPSecret *> *cachedTotpSecrets;
#endif

@implementation RNFBAuthModule
Expand All @@ -82,6 +83,7 @@ - (id)init {
#if TARGET_OS_IOS
cachedResolver = [[NSMutableDictionary alloc] init];
cachedSessions = [[NSMutableDictionary alloc] init];
cachedTotpSecrets = [[NSMutableDictionary alloc] init];
#endif
});
return self;
Expand Down Expand Up @@ -111,6 +113,7 @@ - (void)invalidate {
#if TARGET_OS_IOS
[cachedResolver removeAllObjects];
[cachedSessions removeAllObjects];
[cachedTotpSecrets removeAllObjects];
#endif
}

Expand Down Expand Up @@ -968,6 +971,61 @@ - (void)invalidate {
}];
}

RCT_EXPORT_METHOD(resolveTotpSignIn
: (FIRApp *)firebaseApp
: (NSString *)sessionKey
: (NSString *)uid
: (NSString *)oneTimePassword
: (RCTPromiseResolveBlock)resolve
: (RCTPromiseRejectBlock)reject) {
DLog(@"using instance resolve TotpSignIn: %@", firebaseApp.name);

FIRMultiFactorAssertion *assertion =
[FIRTOTPMultiFactorGenerator assertionForSignInWithEnrollmentID:uid oneTimePassword:oneTimePassword];
[cachedResolver[sessionKey] resolveSignInWithAssertion:assertion
completion:^(FIRAuthDataResult *_Nullable authResult,
NSError *_Nullable error) {
DLog(@"authError: %@", error) if (error) {
[self promiseRejectAuthException:reject
error:error];
}
else {
[self promiseWithAuthResult:resolve
rejecter:reject
authResult:authResult];
}
}];
}

RCT_EXPORT_METHOD(generateTotpSecret
: (FIRApp *)firebaseApp
: (NSString *)sessionKey
: (RCTPromiseResolveBlock)resolve
: (RCTPromiseRejectBlock)reject) {
DLog(@"using instance resolve generateTotpSecret: %@", firebaseApp.name);

FIRMultiFactorSession *session = cachedSessions[sessionKey];
DLog(@"using sessionKey: %@", sessionKey);
DLog(@"using session: %@", session);
[FIRTOTPMultiFactorGenerator generateSecretWithMultiFactorSession:session
completion:^(FIRTOTPSecret * _Nullable totpSecret, NSError * _Nullable error) {
DLog(@"authError: %@", error) if (error) {
[self promiseRejectAuthException:reject
error:error];
}
else {
NSString *secretKey = totpSecret.sharedSecretKey;
DLog(@"secretKey generated: %@", secretKey);
cachedTotpSecrets[secretKey] = totpSecret;
DLog(@"cachedSecret: %@", cachedTotpSecrets[secretKey]);
resolve(@{
@"secretKey": secretKey,
});
}
}];

}

RCT_EXPORT_METHOD(getSession
: (FIRApp *)firebaseApp
: (RCTPromiseResolveBlock)resolve
Expand All @@ -986,6 +1044,26 @@ - (void)invalidate {
}];
}

RCT_EXPORT_METHOD(unenrollMultiFactor
: (FIRApp *)firebaseApp
: (NSString *)factorUID
: (RCTPromiseResolveBlock)resolve
: (RCTPromiseRejectBlock)reject) {
DLog(@"using instance unenrollMultiFactor: %@", firebaseApp.name);

FIRUser *user = [FIRAuth authWithApp:firebaseApp].currentUser;
[user.multiFactor unenrollWithFactorUID:factorUID completion:^(NSError * _Nullable error) {
if (error != nil) {
[self promiseRejectAuthException:reject error:error];
return;
}

resolve(nil);
return;
}];

}

RCT_EXPORT_METHOD(finalizeMultiFactorEnrollment
: (FIRApp *)firebaseApp
: (NSString *)verificationId
Expand Down Expand Up @@ -1015,6 +1093,35 @@ - (void)invalidate {
}];
}

RCT_EXPORT_METHOD(finalizeTotpEnrollment
: (FIRApp *)firebaseApp
: (NSString *)totpSecret
: (NSString *)verificationCode
: (NSString *_Nullable)displayName
: (RCTPromiseResolveBlock)resolve
: (RCTPromiseRejectBlock)reject) {
DLog(@"using instance finalizeTotpEnrollment: %@", firebaseApp.name);

FIRTOTPSecret *cachedTotpSecret = cachedTotpSecrets[totpSecret];
DLog(@"using totpSecretKey: %@", totpSecret);
DLog(@"using cachedSecret: %@", cachedTotpSecret);
FIRTOTPMultiFactorAssertion *assertion =
[FIRTOTPMultiFactorGenerator assertionForEnrollmentWithSecret:cachedTotpSecret oneTimePassword:verificationCode];

FIRUser *user = [FIRAuth authWithApp:firebaseApp].currentUser;

[user.multiFactor enrollWithAssertion:assertion displayName:displayName completion:^(NSError * _Nullable error) {
if (error != nil) {
[self promiseRejectAuthException:reject error:error];
return;
}

resolve(nil);
return;
}];

}

RCT_EXPORT_METHOD(verifyPhoneNumber
: (FIRApp *)firebaseApp
: (NSString *)phoneNumber
Expand Down Expand Up @@ -1739,18 +1846,29 @@ - (NSDictionary *)firebaseUserToDict:(FIRUser *)user {
NSMutableArray *enrolledFactors = [NSMutableArray array];

for (FIRPhoneMultiFactorInfo *hint in hints) {
NSString *enrollmentTime =
[[[NSISO8601DateFormatter alloc] init] stringFromDate:hint.enrollmentDate];
[enrolledFactors addObject:@{
@"uid" : hint.UID,
@"factorId" : [self getJSFactorId:(hint.factorID)],
@"displayName" : hint.displayName == nil ? [NSNull null] : hint.displayName,
@"enrollmentTime" : enrollmentTime,
// @deprecated enrollmentDate kept for backwards compatibility, please use enrollmentTime
@"enrollmentDate" : enrollmentTime,
// phoneNumber only present on FIRPhoneMultiFactorInfo
@"phoneNumber" : hint.phoneNumber == nil ? [NSNull null] : hint.phoneNumber,
}];
NSString *enrollmentTime =
[[[NSISO8601DateFormatter alloc] init] stringFromDate:hint.enrollmentDate];
if ([hint.factorID isEqualToString:@"phone"]) {
[enrolledFactors addObject:@{
@"uid" : hint.UID,
@"factorId" : [self getJSFactorId:(hint.factorID)],
@"displayName" : hint.displayName == nil ? [NSNull null] : hint.displayName,
@"enrollmentTime" : enrollmentTime,
// @deprecated enrollmentDate kept for backwards compatibility, please use enrollmentTime
@"enrollmentDate" : enrollmentTime,
// phoneNumber only present on FIRPhoneMultiFactorInfo
@"phoneNumber" : hint.phoneNumber == nil ? [NSNull null] : hint.phoneNumber,
}];
} else if ([hint.factorID isEqualToString:@"totp"]) {
[enrolledFactors addObject:@{
@"uid" : hint.UID,
@"factorId" : @"totp",
@"displayName" : hint.displayName == nil ? [NSNull null] : hint.displayName,
@"enrollmentTime" : enrollmentTime,
// @deprecated enrollmentDate kept for backwards compatibility, please use enrollmentTime
@"enrollmentDate" : enrollmentTime,
}];
}
}
return enrolledFactors;
}
Expand Down
9 changes: 7 additions & 2 deletions packages/auth/lib/MultiFactorResolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ export default class MultiFactorResolver {
}

resolveSignIn(assertion) {
const { token, secret } = assertion;
return this._auth.resolveMultiFactorSignIn(this.session, token, secret);
const { token, secret, uid, verificationCode } = assertion;

if (token && secret) {
return this._auth.resolveMultiFactorSignIn(this.session, token, secret);
}

return this._auth.resolveTotpSignIn(this.session, uid, verificationCode);
}
}
Loading
Loading