Ativar o TOTP MFA para seu app

Este documento descreve como adicionar a autenticação multifator (MFA) baseada em tempo (TOTP, na sigla em inglês) ao seu app.

O Identity Platform permite usar um TOTP como um fator adicional para a MFA. Quando você ativa esse recurso, os usuários que tentam fazer login no app visualizam uma solicitação de um TOTP. Para gerá-lo, é necessário usar um app autenticador capaz de gerar códigos TOTP válidos, como o Google Authenticator.

Antes de começar

  1. Ative pelo menos um provedor compatível com a autenticação multifator (MFA). Todos os provedores, exceto os seguintes MFA de suporte:

    • Autenticação por telefone
    • Autenticação anônima
    • Tokens de autenticação personalizados
    • Apple Game Center
  2. Verifique se o app verifica os endereços de e-mail dos usuários. A autenticação multifator (MFA) requer verificação de e-mail. Isso impede que agentes mal-intencionados se registrem em um serviço com um endereço de e-mail que não tenham e bloqueiem o proprietário real do endereço de e-mail adicionando um segundo fator.

  3. Verifique se você tem a versão correta da plataforma. O TOTP MFA só é compatível com as seguintes versões do SDK:

    Plataforma Versões
    SDK da Web v9.19.1 ou superior
    SDK Android 22.1.0+
    SDK do iOS 10.12.0+

Ative o TOTP MFA no nível do projeto

Para ativar o TOTP como segundo fator, use o SDK Admin ou chame o endpoint REST da configuração do projeto.

Para usar o SDK Admin, faça o seguinte:

  1. Instale o SDK Admin para Node.js do Firebase, caso ainda não tenha feito isso.

    O TOTP MFA só é compatível com o SDK Admin para Node.js do Firebase versão 11.6.0 e mais recentes.

  2. Execute o comando a seguir:

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

    Substitua:

    • NUM_ADJ_INTERVALS: o número de intervalos de janela de tempo adjacentes a partir dos quais aceitar TOTPs, de zero a dez. O padrão é cinco.

      Os TOTPs garantem que, quando duas partes (o responsável e o validador) gerarem OTPs na mesma janela de tempo (normalmente 30 segundos), elas gerem a mesma senha. No entanto, para acomodar o deslocamento do relógio entre as partes e o tempo de resposta humano, você pode configurar o serviço TOTP para aceitar também TOTPs de janelas adjacentes.

Para ativar a MTP TOTP usando a API REST, execute o seguinte:

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
            }
          }]
       }
    }'

Substitua:

  • PROJECT_ID: o ID do projeto.
  • NUM_ADJ_INTERVALS: o número de intervalos de janela de tempo, de zero a dez. O padrão é cinco.

    Os TOTPs garantem que, quando duas partes (o responsável e o validador) gerarem OTPs na mesma janela de tempo (normalmente 30 segundos), elas gerem a mesma senha. No entanto, para acomodar o deslocamento do relógio entre as partes e o tempo de resposta humano, você pode configurar o serviço TOTP para aceitar também TOTPs de janelas adjacentes.

Ativar o TOTP MFA no nível do locatário

Para ativar o TOTP como um segundo fator para MFA no nível do locatário, use o seguinte código:

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

Substitua:

  • TENANT_ID: o ID do locatário da string.
  • NUM_ADJ_INTERVALS: o número de intervalos de janela de tempo, de zero a dez. O padrão é cinco.

Para ativar o TOTP MFA usando a API REST no nível do locatário, execute o seguinte:

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": [{
            "totpProviderConfig": {
              "adjacentIntervals": NUM_ADJ_INTERVALS
            }
          }]
       }
    }'

Substitua:

  • PROJECT_ID: o ID do projeto.
  • TENANT_ID: o ID do locatário.
  • NUM_ADJ_INTERVALS: o número de intervalos de janela de tempo, de zero a dez. O padrão é cinco.

Escolha um padrão de inscrição

É possível escolher se o app requer autenticação multifator e como e quando registrar os usuários. Alguns padrões comuns incluem:

  • Registrar o segundo fator do usuário como parte do processo. Use esse método se o app exigir autenticação multifator para todos os usuários.

  • Oferecer uma opção pulável para registrar um segundo fator durante o processo. Se você quiser incentivar, mas não exigir, a autenticação multifator no seu app, use essa abordagem.

  • Permitir a adição de um segundo fator na página de gerenciamento da conta ou no perfil do usuário, em vez da tela de inscrição. Isso minimiza o atrito durante o processo de registro, ao mesmo tempo que disponibiliza a autenticação multifator para usuários que se preocupam com a segurança.

  • Exigir a adição de um segundo fator de maneira incremental quando o usuário quiser acessar recursos com requisitos de segurança aprimorados.

Inscrever usuários na TOTP MFA

Depois de ativar a TOFA MFA como o segundo fator do app, implemente a lógica do cliente para registrar usuários na TOTP MFA:

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) {
      // ...

    }];
  }];
}];

Fazer login de usuários com um segundo fator

Para fazer login de usuários com a MFA TOTP, use o seguinte 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.
  }
}

O exemplo acima usa e-mail e senha como o primeiro fator.

Cancelar inscrição na TOTP MFA

Esta seção descreve como lidar com um usuário que cancela a inscrição na TOFA MFA.

Se um usuário se inscrever em várias opções de MFA e se cancelar a inscrição na opção ativada mais recentemente, ele receberá uma auth/user-token-expired e será desconectado. O usuário precisará fazer login novamente e verificar as credenciais existentes, por exemplo, um endereço de e-mail e uma senha.

Para cancelar a inscrição do usuário, lidar com o erro e acionar a reautenticação, use o seguinte 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
        }
    }];

A seguir