diff --git a/packages/auth/android/src/main/java/io/invertase/firebase/auth/ReactNativeFirebaseAuthModule.java b/packages/auth/android/src/main/java/io/invertase/firebase/auth/ReactNativeFirebaseAuthModule.java index ef2ff264b9..71af2b0fa8 100644 --- a/packages/auth/android/src/main/java/io/invertase/firebase/auth/ReactNativeFirebaseAuthModule.java +++ b/packages/auth/android/src/main/java/io/invertase/firebase/auth/ReactNativeFirebaseAuthModule.java @@ -65,6 +65,9 @@ import com.google.firebase.auth.PhoneMultiFactorAssertion; import com.google.firebase.auth.PhoneMultiFactorGenerator; import com.google.firebase.auth.PhoneMultiFactorInfo; +import com.google.firebase.auth.TotpMultiFactorAssertion; +import com.google.firebase.auth.TotpMultiFactorGenerator; +import com.google.firebase.auth.TotpSecret; import com.google.firebase.auth.TwitterAuthProvider; import com.google.firebase.auth.UserInfo; import com.google.firebase.auth.UserProfileChangeRequest; @@ -106,6 +109,7 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule { private final HashMap mCachedResolvers = new HashMap<>(); private final HashMap mMultiFactorSessions = new HashMap<>(); + private TotpSecret totpSecret = null; ReactNativeFirebaseAuthModule(ReactApplicationContext reactContext) { super(reactContext, TAG); @@ -1105,6 +1109,126 @@ public void getSession(final String appName, final Promise promise) { }); } + @ReactMethod + public void generateSecret( + final String appName, + final String sessionKey, + final Boolean openInApp, + final Promise promise) { + final MultiFactorSession session = mMultiFactorSessions.get(sessionKey); + if (session == null) { + rejectPromiseWithCodeAndMessage(promise, "unknown", "can't find session for provided key"); + return; + } + + TotpMultiFactorGenerator.generateSecret(session) + .addOnCompleteListener( + task -> { + if (!task.isSuccessful()) { + rejectPromiseWithExceptionMap(promise, task.getException()); + return; + } + + final TotpSecret secret = task.getResult(); + final String sharedSecret = secret.getSharedSecretKey(); + + if (openInApp) { + FirebaseApp firebaseApp = FirebaseApp.getInstance(appName); + final FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp); + FirebaseUser user = firebaseAuth.getCurrentUser(); + + if (user == null) { + rejectPromiseWithCodeAndMessage(promise, "unknown", "current user must be set"); + return; + } + String email = user.getEmail(); + if (email == null) { + rejectPromiseWithCodeAndMessage(promise, "unknown", "email must be set"); + return; + } + final String qrCodeUrl = secret.generateQrCodeUrl(email, appName); + secret.openInOtpApp(qrCodeUrl); + } + this.totpSecret = secret; + promise.resolve(sharedSecret); + }); + } + + private void resolveTotpMultiFactorCredential( + final String verificationId, + final String verificationCode, + final String sessionKey, + final Promise promise) { + + final MultiFactorAssertion multiFactorAssertion = + TotpMultiFactorGenerator.getAssertionForSignIn(verificationId, verificationCode); + + final MultiFactorResolver resolver = mCachedResolvers.get(sessionKey); + if (resolver == null) { + rejectPromiseWithCodeAndMessage( + promise, + "invalid-multi-factor-session", + "No resolver for session found. Is the session id correct?"); + return; + } + + resolver + .resolveSignIn(multiFactorAssertion) + .addOnCompleteListener( + task -> { + if (task.isSuccessful()) { + AuthResult authResult = task.getResult(); + promiseWithAuthResult(authResult, promise); + } else { + promiseRejectAuthException(promise, task.getException()); + } + }); + } + + @ReactMethod + public void resolveTotpMultiFactorSignIn( + final String appName, + final String session, + final String verificationId, + final String verificationCode, + final Promise promise) { + resolveTotpMultiFactorCredential(verificationId, verificationCode, session, promise); + } + + @ReactMethod + public void enrollTotp( + final String appName, + final String verificationCode, + @Nullable final String displayName, + final Promise promise) { + + if (this.totpSecret == null) { + rejectPromiseWithCodeAndMessage(promise, "unknown", "totp secret isn't set yet"); + return; + } + FirebaseApp firebaseApp = FirebaseApp.getInstance(appName); + FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp); + + final TotpMultiFactorAssertion assertion = + TotpMultiFactorGenerator.getAssertionForEnrollment(this.totpSecret, verificationCode); + firebaseAuth + .getCurrentUser() + .getMultiFactor() + .enroll(assertion, displayName) + .addOnCompleteListener( + task -> { + if (task.isSuccessful()) { + Log.d(TAG, "finalizeMultiFactorEnrollment:onComplete:success"); + this.totpSecret = null; + promise.resolve(null); + } else { + Exception exception = task.getException(); + Log.e(TAG, "finalizeMultiFactorEnrollment:onComplete:failure", exception); + promiseRejectAuthException(promise, exception); + } + }); + } + @ReactMethod public void verifyPhoneNumberWithMultiFactorInfo( final String appName, final String hintUid, final String sessionKey, final Promise promise) { diff --git a/packages/auth/ios/RNFBAuth/RNFBAuthModule.m b/packages/auth/ios/RNFBAuth/RNFBAuthModule.m index 76862bc069..e8d844ac25 100644 --- a/packages/auth/ios/RNFBAuth/RNFBAuthModule.m +++ b/packages/auth/ios/RNFBAuth/RNFBAuthModule.m @@ -57,6 +57,7 @@ static __strong NSMutableDictionary *credentials; static __strong NSMutableDictionary *cachedResolver; static __strong NSMutableDictionary *cachedSessions; +static __strong TotpSecret *totpSecret; @implementation RNFBAuthModule #pragma mark - @@ -948,6 +949,124 @@ - (void)invalidate { }]; } +RCT_EXPORT_METHOD(generateSecret + : (NSString *)appName sessionKey + : (NSString *)sessionKey openInApp + : (BOOL)openInApp resolver + : (RCTPromiseResolveBlock)resolve rejecter + : (RCTPromiseRejectBlock)reject) { + if ([cachedResolver valueForKey:sessionKey] == nil) { + [RNFBSharedUtils + rejectPromiseWithUserInfo:reject + userInfo:(NSMutableDictionary *)@{ + @"code" : @"invalid-multi-factor-session", + @"message" : @"No resolver for session found. Is the session id correct?" + }]; + return; + } + FIRMultiFactorSession *session = cachedResolver[sessionKey].session; + + [TotpMultiFactorGenerator + generateSecretWithSession:session + completion:^(TotpSecret *_Nullable secret, NSError *_Nullable error) { + if (error) { + [self promiseRejectAuthException:reject error:error]; + return; + } + + NSString *sharedSecret = secret.sharedSecretKey; + + if (openInApp) { + FirebaseAuth *firebaseAuth = + [FIRAuth authWithApp:[FIRApp appNamed:appName]]; + FIRUser *user = [firebaseAuth currentUser]; + if (user == nil) { + reject(@"unknown", @"current user must be set", nil); + return; + } + NSString *email = user.email; + if (email == nil) { + reject(@"unknown", @"email must be set", nil); + return; + } + NSString *qrCodeUrl = [secret generateQrCodeUrlWithEmail:email + appName:appName]; + [secret openInOtpAppWithQrCodeUrl:qrCodeUrl]; + } + self.totpSecret = secret; + resolve(sharedSecret); + }]; +} + +RCT_EXPORT_METHOD(resolveTotpMultiFactorSignIn + : (NSString *)appName session + : (NSString *)session verificationId + : (NSString *)verificationId verificationCode + : (NSString *)verificationCode resolver + : (RCTPromiseResolveBlock)resolve rejecter + : (RCTPromiseRejectBlock)reject) { + [self resolveTotpMultiFactorCredentialWithVerificationId:verificationId + verificationCode:verificationCode + sessionKey:session + resolver:resolve + rejecter:reject]; +} + +RCT_EXPORT_METHOD(enrollTotp + : (NSString *)appName verificationCode + : (NSString *)verificationCode displayName + : (NSString *)displayName resolver + : (RCTPromiseResolveBlock)resolve rejecter + : (RCTPromiseRejectBlock)reject) { + if (self.totpSecret == nil) { + reject(@"unknown", @"totp secret isn't set yet", nil); + return; + } + TotpMultiFactorAssertion *assertion = + [TotpMultiFactorGenerator getAssertionForEnrollmentWithSecret:self.totpSecret + verificationCode: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; + }]; +} + +- (void)resolveTotpMultiFactorCredentialWithVerificationId:(NSString *)verificationId + verificationCode:(NSString *)verificationCode + sessionKey:(NSString *)sessionKey + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject { + MultiFactorAssertion *multiFactorAssertion = + [TotpMultiFactorGenerator getAssertionForSignInWithVerificationId:verificationId + verificationCode:verificationCode]; + + MultiFactorResolver *resolver = [self.cachedResolvers objectForKey:sessionKey]; + if (resolver == nil) { + reject(@"invalid-multi-factor-session", + @"No resolver for session found. Is the session id correct?", nil); + return; + } + + [resolver resolveSignInWithAssertion:multiFactorAssertion + completion:^(FIRAuthDataResult *_Nullable authResult, + NSError *_Nullable error) { + if (error) { + reject(@"unknown", error.localizedDescription, error); + } else { + // Handle the authResult as needed + resolve(authResult); + } + }]; +} + RCT_EXPORT_METHOD(finalizeMultiFactorEnrollment : (FIRApp *)firebaseApp : (NSString *)verificationId @@ -1745,4 +1864,4 @@ - (FIRActionCodeSettings *)buildActionCodeSettings:(NSDictionary *)actionCodeSet return settings; } -@end \ No newline at end of file +@end diff --git a/packages/auth/lib/index.d.ts b/packages/auth/lib/index.d.ts index 90650342bf..1342a2fcc8 100644 --- a/packages/auth/lib/index.d.ts +++ b/packages/auth/lib/index.d.ts @@ -577,6 +577,12 @@ export namespace FirebaseAuthTypes { * The method will ensure the user state is reloaded after successfully enrolling a factor. */ enroll(assertion: MultiFactorAssertion, displayName?: string): Promise; + + /** + * Enroll TOTP factor. Provide an optional display name that can be shown to the user. + * The method will ensure the user state is reloaded after successfully enrolling a factor. + */ + enrollTotp(verificationCode: string, displayName?: string): Promise; } /** @@ -1841,6 +1847,10 @@ export namespace FirebaseAuthTypes { */ signInWithCredential(credential: AuthCredential): Promise; + generateSecret(session: MultiFactorSession, openInApp?: boolean): Promise; + + resolveTotpMultiFactorSignIn(session: MultiFactorSession, verificationId: string, verificationCode: string): Promise; + /** * Signs the user in with a specified provider. This is a web-compatible API along with signInWithRedirect. * They both share the same call to the underlying native SDK signInWithProvider method. diff --git a/packages/auth/lib/index.js b/packages/auth/lib/index.js index 9c8c1e511b..67a75d0b32 100644 --- a/packages/auth/lib/index.js +++ b/packages/auth/lib/index.js @@ -375,6 +375,16 @@ class FirebaseAuthModule extends FirebaseModule { .then(userCredential => this._setUserCredential(userCredential)); } + generateSecret(session, openInApp = false) { + return this.native.generateSecret(session, openInApp); + } + + resolveTotpMultiFactorSignIn(session, verificationId, verificationCode) { + return this.native + .resolveTotpMultiFactorSignIn(session, verificationId, verificationCode) + .then(userCredential => this._setUserCredential(userCredential)); + } + revokeToken(authorizationCode) { return this.native.revokeToken(authorizationCode); } diff --git a/packages/auth/lib/multiFactor.js b/packages/auth/lib/multiFactor.js index 889f01b617..35405a8bf9 100644 --- a/packages/auth/lib/multiFactor.js +++ b/packages/auth/lib/multiFactor.js @@ -34,6 +34,12 @@ export class MultiFactorUser { return this._auth.currentUser.reload(); } + async enrollTotp(verificationCode, displayName) { + await this._auth.native.enrollTotp(verificationCode, displayName); + + return this._auth.currentUser.reload(); + } + unenroll() { return Promise.reject(new Error('No implemented yet.')); }