Skip to content
Merged
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
1 change: 1 addition & 0 deletions .spellcheck.dict.txt
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ TFUA
thenable
ThreadPoolExecutor
timezones
TOTP
triaging
TurboModule
TurboModules
Expand Down
24 changes: 15 additions & 9 deletions docs/auth/multi-factor-auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,24 @@ next: /firestore/usage
previous: /auth/oidc-auth
---

# iOS Setup
> Before a user can enroll a second factor they need to verify their email. See
> [`User`](/reference/auth/user#sendEmailVerification) interface is returned.

# TOTP MFA

The [official guide for Firebase web TOTP authentication](https://firebase.google.com/docs/auth/web/totp-mfa) explains the TOTP process well, including project prerequisites to enable the feature, as well as code examples.

The API details and usage examples may be combined with the full Phone auth example below to give you an MFA solution that fully supports TOTP or SMS MFA.

# Phone MFA

## iOS Setup

Make sure to follow [the official Identity Platform
documentation](https://cloud.google.com/identity-platform/docs/ios/mfa#enabling_multi-factor_authentication)
to enable multi-factor authentication for your project and verify your app.

# Enroll a new factor

> Before a user can enroll a second factor they need to verify their email. See
> [`User`](/reference/auth/user#sendEmailVerification) interface is returned.
## Enroll a new factor

Begin by obtaining a [`MultiFactorUser`](/reference/auth/multifactoruser)
instance for the current user. This is the entry point for most multi-factor
Expand Down Expand Up @@ -57,7 +65,7 @@ await multiFactorUser.enroll(multiFactorAssertion, 'Optional display name for th
You can inspect [`User#multiFactor`](/reference/auth/user#multiFactor) for
information about the user's enrolled factors.

# Sign-in flow using multi-factor
## Sign-in flow using phone multi-factor

Ensure the account has already enrolled a second factor. Begin by calling the
default sign-in methods, for example email and password. If the account requires
Expand Down Expand Up @@ -103,7 +111,6 @@ if (resolver.hints.length > 1) {
// Use resolver.hints to display a list of second factors to the user
}

// Currently only phone based factors are supported
if (resolver.hints[0].factorId === PhoneMultiFactorGenerator.FACTOR_ID) {
// Continue with the sign-in flow
}
Expand Down Expand Up @@ -163,7 +170,6 @@ signInWithEmailAndPassword(getAuth(), email, password)
// Use resolver.hints to display a list of second factors to the user
}

// Currently only phone based factors are supported
if (resolver.hints[0].factorId === PhoneMultiFactorGenerator.FACTOR_ID) {
const hint = resolver.hints[0];

Expand All @@ -185,7 +191,7 @@ signInWithEmailAndPassword(getAuth(), email, password)
});
```

# Testing
## Testing

You can define test phone numbers and corresponding verification codes. The
official[official
Expand Down
12 changes: 12 additions & 0 deletions packages/app/lib/internal/NativeFirebaseError.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,18 @@ export default class NativeFirebaseError extends Error {
value: userInfo,
});

// Needed for MFA processing of errors on web
Object.defineProperty(this, 'customData', {
enumerable: false,
value: nativeError.customData || null,
});

// Needed for MFA processing of errors on web
Object.defineProperty(this, 'operationType', {
enumerable: false,
value: nativeError.operationType || null,
});

Object.defineProperty(this, 'nativeErrorCode', {
enumerable: false,
value: userInfo.nativeErrorCode || null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,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 @@ -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,26 @@ 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 +1305,67 @@ public void finalizeMultiFactorEnrollment(
});
}

@ReactMethod
public void generateQrCodeUrl(
final String appName,
final String secretKey,
final String account,
final String issuer,
final Promise promise) {

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

@ReactMethod
public void openInOtpApp(final String appName, final String secretKey, final String qrCodeUri) {
TotpSecret secret = mTotpSecrets.get(secretKey);
if (secret != null) {
secret.openInOtpApp(qrCodeUri);
}
}

@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 +1421,70 @@ 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
Loading
Loading