Habilita la MFA de TOTP para tu app

En este documento, se describe cómo agregar la autenticación de varios factores (MFA) con contraseña de un solo uso basada en el tiempo (TOTP) a tu app.

Identity Platform te permite usar un TOTP como un factor adicional para la MFA. Cuando habilitas esta función, los usuarios que intentan acceder a tu app ven una solicitud de una TOTP. Para generarla, deben usar una app de autenticador capaz de generar códigos TOTP válidos, como Google Authenticator.

Antes de comenzar

  1. Habilita al menos un proveedor que admita la MFA. Ten en cuenta que todos los proveedores, excepto los siguientes, admiten la MFA:

    • Autenticación telefónica
    • Autenticación anónima
    • Tokens de autenticación personalizados
    • Apple Game Center
  2. Asegúrate de que tu app verifique las direcciones de correo electrónico de los usuarios. La MFA requiere verificación por correo electrónico. Esto evita que los actores maliciosos se registren en un servicio con una dirección de correo electrónico que no les pertenece y, luego, bloqueen al propietario real de la dirección de correo electrónico agregando un segundo factor.

  3. Asegúrate de tener la versión correcta de la plataforma. La MFA de TOTP solo es compatible con las siguientes versiones del SDK:

    Plataforma Versiones
    SDK web v9.19.1 o posterior
    SDK de Android 22.1.0+
    SDK de iOS 10.12.0+

Habilita la MFA del TOTP a nivel del proyecto

Para habilitar TOTP como el segundo factor, usa el SDK de Admin o llama al extremo de REST de configuración del proyecto.

Para usar el SDK de Admin, haz lo siguiente:

  1. Si aún no lo has hecho, instala el SDK de Firebase Admin Node.js.

    La MFA de TOTP solo es compatible con las versiones 11.6.0 y posteriores del SDK de Firebase Admin Node.js.

  2. Ejecuta este comando:

    import { getAuth } from 'firebase-admin/auth';
    
    getAuth().projectConfigManager().updateProjectConfig(
    {
          multiFactorConfig: {
              providerConfigs: [{
                  state: "ENABLED",
                  totpProviderConfig: {
                      adjacentIntervals: {
                          NUM_ADJ_INTERVALS
                      },
                  }
              }]
          }
    })
    

    Reemplaza lo siguiente:

    • NUM_ADJ_INTERVALS: La cantidad de intervalos de ventana de tiempo adyacentes a partir de los cuales se aceptarán TOTP, de cero a diez. El valor predeterminado es cinco.

      Los TOTP garantizan que cuando dos partes (el verificador y el validador) generen OTP en el mismo período (por lo general, de 30 segundos), generen la misma contraseña. Sin embargo, para adaptarse al desvío del reloj entre las partes y el tiempo de respuesta manual, puedes configurar el servicio TOTP para que también acepte los TOTP de las ventanas adyacentes.

Para habilitar la MFA de TOTP mediante la API de REST, ejecuta lo siguiente:

curl -X PATCH "https://identitytoolkit.googleapis.com/admin/v2/projects/PROJECT_ID/config?updateMask=mfa" \
    -H "Authorization: Bearer $(gcloud auth print-access-token)" \
    -H "Content-Type: application/json" \
    -H "X-Goog-User-Project: PROJECT_ID" \
    -d \
    '{
        "mfa": {
          "providerConfigs": [{
            "state": "ENABLED",
            "totpProviderConfig": {
              "adjacentIntervals": "NUM_ADJ_INTERVALS"
            }
          }]
       }
    }'

Reemplaza lo siguiente:

  • PROJECT_ID: El ID del proyecto
  • NUM_ADJ_INTERVALS: Es la cantidad de intervalos de tiempo, de cero a diez. El valor predeterminado es cinco.

    Los TOTP garantizan que cuando dos partes (el verificador y el validador) generen OTP en el mismo período (por lo general, de 30 segundos), generen la misma contraseña. Sin embargo, para adaptarse al desvío del reloj entre las partes y el tiempo de respuesta manual, puedes configurar el servicio TOTP para que también acepte los TOTP de las ventanas adyacentes.

Habilita la MFA de TOTP a nivel del usuario

Si deseas habilitar el TOTP como segundo factor para la MFA a nivel del usuario, usa el siguiente código:

getAuth().tenantManager().updateTenant(TENANT_ID,
{
      multiFactorConfig: {
          providerConfigs: [{
              state: "ENABLED",
              totpProviderConfig: {
                  adjacentIntervals: {
                      NUM_ADJ_INTERVALS
                  },
              }
          }]
      }
})

Reemplaza lo siguiente:

  • TENANT_ID: El ID del usuario de la string
  • NUM_ADJ_INTERVALS: Es la cantidad de intervalos de tiempo, de cero a diez. El valor predeterminado es cinco.

Para habilitar la MFA de TOTP con la API de REST a nivel del usuario, ejecuta lo siguiente:

curl -X PATCH "https://identitytoolkit.googleapis.com/v2/projects/PROJECT_ID/tenants/TENANT_ID?updateMask=mfaConfig" \
    -H "Authorization: Bearer $(gcloud auth print-access-token)" \
    -H "Content-Type: application/json" \
    -H "X-Goog-User-Project: PROJECT_ID" \
    -d \
    '{
        "mfaConfig": {
          "providerConfigs": [{
            "state": "ENABLED",
            "totpProviderConfig": {
              "adjacentIntervals": "NUM_ADJ_INTERVALS"
            }
          }]
       }
    }'

Reemplaza lo siguiente:

  • PROJECT_ID: el ID del proyecto
  • TENANT_ID: El ID del usuario
  • NUM_ADJ_INTERVALS: Es la cantidad de intervalos de tiempo, de cero a diez. El valor predeterminado es cinco.

Elige un patrón de inscripción

Puedes elegir si tu app requerirá una autenticación de varios factores, además de cómo y cuándo inscribir a tus usuarios. Estos son algunos patrones comunes:

  • Inscribir el segundo factor del usuario como parte del registro. Usa este método si tu app requiere la autenticación de varios factores para todos los usuarios.

  • Ofrecer una opción que se puede omitir para inscribir un segundo factor durante el registro. Si quieres fomentar el proceso de autenticación de varios factores en tu app, pero no exigirlo, puedes usar este enfoque.

  • Proporcionar la capacidad de agregar un segundo factor desde la página de administración de la cuenta o el perfil del usuario, en lugar de la pantalla de registro. Esto minimiza la fricción durante el proceso de registro y, a la vez, permite que la autenticación de varios factores esté disponible para los usuarios sensibles a la seguridad.

  • Requiere agregar un segundo factor de manera incremental cuando el usuario quiera acceder a las funciones con requisitos de seguridad mayores.

Inscribir a los usuarios en la MFA de TOTP

Después de habilitar la MFA de TOTP como segundo factor para tu app, implementa la lógica del cliente para inscribir a los usuarios en la MFA de TOTP:

Web

import {
    multiFactor,
    TotpMultiFactorGenerator,
    TotpSecret
} from "firebase/auth";

multiFactorSession = await multiFactor(activeUser()).getSession();
totpSecret = await TotpMultiFactorGenerator.generateSecret(
    multiFactorSession
);

// Display this URL as a QR code.
const url = totpSecret.generateQrCodeUrl( <user account id> , <app name> );

// Ask the user for the verification code from the OTP app by scanning the QR
// code.
const multiFactorAssertion = TotpMultiFactorGenerator.assertionForEnrollment(
    totpSecret,
    verificationCode
);

// Finalize the enrollment.
return multiFactor(user).enroll(multiFactorAssertion, mfaDisplayName);

Java

user.getMultiFactor().getSession()
  .addOnCompleteListener(
  new OnCompleteListener<MultiFactorSession>() {
      @Override
      public void onComplete(@NonNull Task<MultiFactorSession> task) {
        if (task.isSuccessful()) {

          // Get a multi-factor session for the user.
          MultiFactorSession multiFactorSession = task.getResult();

          TotpMultiFactorGenerator.generateSecret(multiFactorSession)
              .addOnCompleteListener(
              new OnCompleteListener<TotpSecret>() {
                  @Override
                  public void onComplete(@NonNull Task<TotpSecret> task){
                    if (task.isSuccessful()) {

                      TotpSecret secret = task.getResult();

                      // Display this URL as a QR code for the user to scan.
                      String qrCodeUrl = secret.generateQrCodeUrl();

                      // Display the QR code
                      // ...

                      // Alternatively, you can automatically load the QR code
                      // into a TOTP authenticator app with either default or
                      // specified fallback URL and activity.
                      // Default fallback URL and activity.
                      secret.openInOtpApp(qrCodeUrl);

                      // Specified fallback URL and activity.
                      // secret.openInOtpApp(qrCodeUrl, fallbackUrl, activity);

                    }
                  }
              });
        }
      }
    });

  // Ask the user for the one-time password (otp) from the TOTP authenticator app.
  MultiFactorAssertion multiFactorAssertion =
    TotpMultiFactorGenerator.getAssertionForEnrollment(
    secret, otp);

  // Complete the enrollment.
  user
    .getMultiFactor()
    .enroll(multiFactorAssertion, /* displayName= */ "My TOTP second factor")
    .addOnCompleteListener(
      new OnCompleteListener<Void>() {
        @Override
        public void onComplete(@NonNull Task<Void> task) {
          if (task.isSuccessful()) {
            showToast("Successfully enrolled TOTP second factor!");
            setResult(Activity.RESULT_OK);
            finish();
          }
        }
    });

Kotlin+KTX

user.multiFactor.session.addOnCompleteListener { task ->
  if (task.isSuccessful) {

    // Get a multi-factor session for the user.
    val session: MultiFactorSession = task.result
    val secret: TotpSecret  = TotpMultiFactorGenerator.generateSecret(session)

    // Display this URL as a QR code for the user to scan.
    val qrCodeUrl = secret.generateQrCodeUrl()
    // Display the QR code
    // ...

    // Alternatively, you can automatically load the QR code
    // into a TOTP authenticator app with either default or
    // specified fallback URL and activity.
    // Default fallback URL and activity.
    secret.openInOtpApp(qrCodeUrl)

    // Specify a fallback URL and activity.
    // secret.openInOtpApp(qrCodeUrl, fallbackUrl, activity);
  }
}

// Ask the user for the one-time password (otp) from the TOTP authenticator app.
val multiFactorAssertion =
  TotpMultiFactorGenerator.getAssertionForEnrollment(secret, otp)

// Complete enrollment.
user.multiFactor.enroll(multiFactorAssertion, /* displayName= */ "My TOTP second factor")
  .addOnCompleteListener {
  // ...
}

Swift

let user = Auth.auth().currentUser

// Get a multi-factor session for the user
user?.multiFactor.getSessionWithCompletion({ (session, error) in
  TOTPMultiFactorGenerator.generateSecret(with: session!) {
    (secret, error) in
    let accountName = user?.email;
    let issuer = Auth.auth().app?.name;

    // Generate a QR code
    let qrCodeUrl = secret?.generateQRCodeURL(withAccountName: accountName!, issuer: issuer!)

    // Display the QR code
    // ...

    // Alternatively, you can automatically load the QR code
    // into a TOTP authenticator app with default fallback UR
    // and activity.
    secret?.openInOTPAppWithQRCodeURL(qrCodeUrl!);

    // Ask the user for the verification code after scanning
    let assertion = TOTPMultiFactorGenerator.assertionForEnrollment(with: secret, oneTimePassword: onetimePassword)

    // Complete the enrollment
    user?.multiFactor.enroll(with: assertion, displayName: accountName) { (error) in
      // ...
    }
  }
})

Objective-C

FIRUser *user = FIRAuth.auth.currentUser;

// Get a multi-factor session for the user
[user.multiFactor getSessionWithCompletion:^(FIRMultiFactorSession *_Nullable session, NSError *_Nullable error) {

  // ...

  [FIRTOTPMultiFactorGenerator generateSecretWithMultiFactorSession:session completion:^(FIRTOTPSecret *_Nullable secret, NSError *_Nullable error) {

    NSString *accountName = user.email;
    NSString *issuer = FIRAuth.auth.app.name;

    // Generate a QR code
    NSString *qrCodeUrl = [secret generateQRCodeURLWithAccountName:accountName issuer:issuer];

    // Display the QR code
    // ...

    // Alternatively, you can automatically load the QR code
    // into a TOTP authenticator app with default fallback URL
    // and activity.
    [secret openInOTPAppWithQRCodeURL:qrCodeUrl];

    // Ask the user for the verification code after scanning
    FIRTOTPMultiFactorAssertion *assertion = [FIRTOTPMultiFactorGenerator assertionForEnrollmentWithSecret:secret oneTimePassword:oneTimePassword];

    // Complete the enrollment
    [user.multiFactor enrollWithAssertion:assertion
                                    displayName:displayName
                                     completion:^(NSError *_Nullable error) {
      // ...

    }];
  }];
}];

Permite que los usuarios accedan con un segundo factor

Para que los usuarios accedan con la MFA de TOTP, usa el siguiente código:

Web

import {
    getAuth,
    getMultiFactorResolver,
    TotpMultiFactorGenerator,
    PhoneMultiFactorGenerator,
    signInWithEmailAndPassword
} from "firebase/auth";

const auth = getAuth();
signInWithEmailAndPassword(auth, email, password)
    .then(function(userCredential) {
      // The user is not enrolled with a second factor and is successfully
      // signed in.
      // ...
    })
    .catch(function(error) {
      if (error.code === 'auth/multi-factor-auth-required') {
          const resolver = getMultiFactorResolver(auth, error);

          // Ask the user which second factor to use.
          if (resolver.hints[selectedIndex].factorId ===
              TotpMultiFactorGenerator.FACTOR_ID) {

              // Ask the user for the OTP code from the TOTP app.
              const multiFactorAssertion = TotpMultiFactorGenerator.assertionForSignIn(resolver.hints[selectedIndex].uid, otp);

              // Finalize the sign-in.
              return resolver.resolveSignIn(multiFactorAssertion).then(function(userCredential) {
                // The user successfully signed in with the TOTP second factor.
              });
          } else if (resolver.hints[selectedIndex].factorId ===
              PhoneMultiFactorGenerator.FACTOR_ID) {
              // Handle the phone MFA.
          } else {
              // The second factor is unsupported.
          }
      }
      // Handle other errors, such as a wrong password.
      else if (error.code == 'auth/wrong-password') {
        //...
      }
    });

Java

FirebaseAuth.getInstance()
  .signInWithEmailAndPassword(email, password)
    .addOnCompleteListener(
        new OnCompleteListener<AuthResult>() {
          @Override
          public void onComplete(@NonNull Task<AuthResult> task) {
            if (task.isSuccessful()) {

              // The user is not enrolled with a second factor and is
              //  successfully signed in.
              // ...
              return;
            }
            if (task.getException() instanceof FirebaseAuthMultiFactorException) {

              // The user is a multi-factor user. Second factor challenge is required.
              FirebaseAuthMultiFactorException error =
                (FirebaseAuthMultiFactorException) task.getException();
              MultiFactorResolver multiFactorResolver = error.getResolver();

              // Display the list of enrolled second factors, user picks one (selectedIndex) from the list.
              MultiFactorInfo selectedHint = multiFactorResolver.getHints().get(selectedIndex);

              if (selectedHint.getFactorId().equals(TotpMultiFactorGenerator.FACTOR_ID)) {

                // Ask the user for the one-time password (otp) from the TOTP app.
                // Initialize a MultiFactorAssertion object with the one-time password and enrollment id.
                MultiFactorAssertion multiFactorAssertion =
                    TotpMultiFactorGenerator.getAssertionForSignIn(selectedHint.getUid(), otp);

                // Complete sign-in.
                multiFactorResolver
                    .resolveSignIn(multiFactorAssertion)
                    .addOnCompleteListener(
                        new OnCompleteListener<AuthResult>() {
                          @Override
                          public void onComplete(@NonNull Task<AuthResult> task) {
                            if (task.isSuccessful()) {
                              // User successfully signed in with the
                              // TOTP second factor.
                            }
                          }
                        });
              } else if (selectedHint.getFactorId().equals(PhoneMultiFactorGenerator.FACTOR_ID)) {
                // Handle Phone MFA.
              } else {
                // Unsupported second factor.
              }
            } else {
              // Handle other errors such as wrong password.
            }
          }
        });

Kotlin+KTX

FirebaseAuth.getInstance
  .signInWithEmailAndPassword(email, password)
    .addOnCompleteListener{ task ->
              if (task.isSuccessful) {
                // User is not enrolled with a second factor and is successfully
                // signed in.
                // ...
              }
              if (task.exception is FirebaseAuthMultiFactorException) {
                  // The user is a multi-factor user. Second factor challenge is
                  // required.
                  val multiFactorResolver:MultiFactorResolver =
                  (task.exception as FirebaseAuthMultiFactorException).resolver

                  // Display the list of enrolled second factors, user picks one (selectedIndex) from the list.
                  val selectedHint: MultiFactorInfo = multiFactorResolver.hints[selectedIndex]

                  if (selectedHint.factorId == TotpMultiFactorGenerator.FACTOR_ID) {
                      val multiFactorAssertion =
                          TotpMultiFactorGenerator.getAssertionForSignIn(selectedHint.uid, otp)

                      multiFactorResolver.resolveSignIn(multiFactorAssertion)
                    .addOnCompleteListener { task ->
                            if (task.isSuccessful) {
                                // User successfully signed in with the
                                // TOTP second factor.
                            }
                            // ...
                      }
                  } else if (selectedHint.factor == PhoneMultiFactorGenerator.FACTOR_ID) {
                      // Handle Phone MFA.
                  } else {
                      // Invalid MFA option.
                  }
              } else {
                  // Handle other errors, such as wrong password.
              }
          }

Swift

Auth.auth().signIn(withEmail: email, password: password) {
  (result, error) in
  if (error != nil) {
    let authError = error! as NSError
    if authError.code == AuthErrorCode.secondFactorRequired.rawValue {
      let resolver = authError.userInfo[AuthErrorUserInfoMultiFactorResolverKey] as! MultiFactorResolver
      if resolver.hints[selectedIndex].factorID == TOTPMultiFactorID {
        let assertion = TOTPMultiFactorGenerator.assertionForSignIn(withEnrollmentID: resolver.hints[selectedIndex].uid, oneTimePassword: oneTimePassword)
        resolver.resolveSignIn(with: assertion) {
          (authResult, error) in
          if (error != nil) {
            // User successfully signed in with second factor TOTP.
          }
        }

      } else if (resolver.hints[selectedIndex].factorID == PhoneMultiFactorID) {
        // User selected a phone second factor.
        // ...
      } else {
        // Unsupported second factor.
        // Note that only phone and TOTP second factors are currently supported.
        // ...
      }
    }
  }
  else {
    // The user is not enrolled with a second factor and is
    //  successfully signed in.
    // ...
  }
}

Objective-C

[FIRAuth.auth signInWithEmail:email
                   password:password
                 completion:^(FIRAuthDataResult * _Nullable authResult,
                              NSError * _Nullable error) {
  if (error == nil || error.code != FIRAuthErrorCodeSecondFactorRequired) {
      // User is not enrolled with a second factor and is successfully signed in.
      // ...
  } else {
      // The user is a multi-factor user. Second factor challenge is required.
      [self signInWithMfaWithError:error];
  }
}];

- (void)signInWithMfaWithError:(NSError * _Nullable)error{
  FIRMultiFactorResolver *resolver = error.userInfo[FIRAuthErrorUserInfoMultiFactorResolverKey];

  // Ask user which second factor to use. Then:
  FIRMultiFactorInfo *hint = (FIRMultiFactorInfo *) resolver.hints[selectedIndex];
  if (hint.factorID == FIRTOTPMultiFactorID) {
    // User selected a totp second factor.
    // Ask user for verification code.
    FIRMultiFactorAssertion *assertion = [FIRTOTPMultiFactorGenerator  assertionForSignInWithEnrollmentID:hint.UID oneTimePassword:oneTimePassword];
    [resolver resolveSignInWithAssertion:assertion
                                  completion:^(FIRAuthDataResult *_Nullable authResult,
                                              NSError *_Nullable error) {
      if (error != nil) {
        // User successfully signed in with the second factor TOTP.
      }
    }];
  } else if (hint.factorID == FIRPhoneMultiFactorID) {
      // User selected a phone second factor.
      // ...
  }
  else {
    // Unsupported second factor.
    // Note that only phone and totp second factors are currently supported.
  }
}

En el ejemplo anterior, se usa el correo electrónico y la contraseña como primer factor.

Dar de baja la MFA de TOTP

En esta sección, se describe cómo controlar la cancelación de la inscripción de un usuario a la MFA de TOTP.

Si un usuario se registró para usar varias opciones de MFA, y si da de baja la opción que se habilitó más recientemente, recibirá un auth/user-token-expired y saldrá de la cuenta. El usuario debe volver a acceder y verificar sus credenciales existentes, por ejemplo, una dirección de correo electrónico y una contraseña.

Para dar de baja al usuario, manejar el error y activar la reautenticación, usa el siguiente código:

Web

import {
    EmailAuthProvider,
    TotpMultiFactorGenerator,
    getAuth,
    multiFactor,
    reauthenticateWithCredential,
} from "firebase/auth";

try {
    // Unenroll from TOTP MFA.
    await multiFactor(currentUser).unenroll(mfaEnrollmentId);
} catch  (error) {
    if (error.code === 'auth/user-token-expired') {
        // If the user was signed out, re-authenticate them.

        // For example, if they signed in with a password, prompt them to
        // provide it again, then call `reauthenticateWithCredential()` as shown
        // below.

        const credential = EmailAuthProvider.credential(email, password);
        await reauthenticateWithCredential(
            currentUser,
            credential
        );
    }
}

Java

List<MultiFactorInfo> multiFactorInfoList = user.getMultiFactor().getEnrolledFactors();

// Select the second factor to unenroll
user
  .getMultiFactor()
  .unenroll(selectedMultiFactorInfo)
  .addOnCompleteListener(
      new OnCompleteListener<Void>() {
        @Override
        public void onComplete(@NonNull Task<Void> task) {
            if (task.isSuccessful()) {
              // User successfully unenrolled the selected second factor.
            }
            else {
              if (task.getException() instanceof FirebaseAuthInvalidUserException) {
                // Handle reauthentication
              }
            }
        }
      });

Kotlin+KTX

val multiFactorInfoList = user.multiFactor.enrolledFactors

// Select the option to unenroll
user.multiFactor.unenroll(selectedMultiFactorInfo)
  .addOnCompleteListener { task ->
    if (task.isSuccessful) {
      // User successfully unenrolled the selected second factor.
    }
    else {
      if (task.exception is FirebaseAuthInvalidUserException) {
        // Handle reauthentication
      }
    }
  }

Swift

user?.multiFactor.unenroll(with: (user?.multiFactor.enrolledFactors[selectedIndex])!,
  completion: { (error) in
  if (error.code == AuthErrorCode.userTokenExpired.rawValue) {
    // Handle reauthentication
  }
})

Objective-C

FIRMultiFactorInfo *unenrolledFactorInfo;
    for (FIRMultiFactorInfo *enrolledFactorInfo in FIRAuth.auth.currentUser.multiFactor.enrolledFactors) {
      // Pick one of the enrolled factors to delete.
    }
    [FIRAuth.auth.currentUser.multiFactor unenrollWithInfo:unenrolledFactorInfo
                                          completion:^(NSError * _Nullable error) {
      if (error.code == FIRAuthErrorCodeUserTokenExpired) {
            // Handle reauthentication
        }
    }];

¿Qué sigue?