Skip to content
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

feat(auth): TOTP #7718

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
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 @@ -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;
Expand Down Expand Up @@ -106,6 +109,7 @@ class ReactNativeFirebaseAuthModule extends ReactNativeFirebaseModule {

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

ReactNativeFirebaseAuthModule(ReactApplicationContext reactContext) {
super(reactContext, TAG);
Expand Down Expand Up @@ -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) {
Expand Down
121 changes: 120 additions & 1 deletion packages/auth/ios/RNFBAuth/RNFBAuthModule.m
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
static __strong NSMutableDictionary<NSString *, FIRAuthCredential *> *credentials;
static __strong NSMutableDictionary<NSString *, FIRMultiFactorResolver *> *cachedResolver;
static __strong NSMutableDictionary<NSString *, FIRMultiFactorSession *> *cachedSessions;
static __strong TotpSecret *totpSecret;

@implementation RNFBAuthModule
#pragma mark -
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1745,4 +1864,4 @@ - (FIRActionCodeSettings *)buildActionCodeSettings:(NSDictionary *)actionCodeSet
return settings;
}

@end
@end
10 changes: 10 additions & 0 deletions packages/auth/lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>;

/**
* 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<void>;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting - I don't think this is strictly necessary? It is not present in the firebase-js-sdk so we need to change this to make sure we are 100% consistent with them.

What I think we need is to add FactorId.TOTP to the FactorIdMap and package the TOTP information in to the MultiFactorAssertion so we can use it in the enroll API above

Then this API is not needed

}

/**
Expand Down Expand Up @@ -1841,6 +1847,10 @@ export namespace FirebaseAuthTypes {
*/
signInWithCredential(credential: AuthCredential): Promise<UserCredential>;

generateSecret(session: MultiFactorSession, openInApp?: boolean): Promise<string>;

resolveTotpMultiFactorSignIn(session: MultiFactorSession, verificationId: string, verificationCode: string): Promise<UserCredential>;

/**
* 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.
Expand Down
10 changes: 10 additions & 0 deletions packages/auth/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
6 changes: 6 additions & 0 deletions packages/auth/lib/multiFactor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.'));
}
Expand Down
Loading