Attiva TOTP MFA per la tua app

Questo documento descrive come aggiungere l'autenticazione a più fattori (MFA) basata su password unica basata su tempo (TOTP) alla tua app.

Identity Platform consente di utilizzare un TOTP come fattore aggiuntivo per l'MFA. Quando abiliti questa funzionalità, gli utenti che tentano di accedere alla tua app visualizzano una richiesta di TOTP. Per generarlo, devono utilizzare un'app di autenticazione in grado di generare codici TOTP validi, come Google Authenticator.

Prima di iniziare

  1. Abilita almeno un provider che supporti l'MFA. Tieni presente che tutti i provider, tranne i seguenti, supportano MFA:

    • Autorizzazione telefono
    • Autenticazione anonima
    • Token di autenticazione personalizzati
    • Game Center di Apple
  2. Assicurati che l'app verifichi gli indirizzi email degli utenti. L'MFA richiede la verifica email. In questo modo è possibile impedire a malintenzionati di registrarsi a un servizio con un indirizzo email che non possiede e quindi bloccare l'effettivo proprietario dell'indirizzo email aggiungendo un secondo fattore.

  3. Assicurati di avere la versione della piattaforma corretta. TOTP MFA è supportato solo nelle seguenti versioni dell'SDK:

    Piattaforma Versioni
    SDK web 9.19.1 e versioni successive
    SDK Android 22.1.0+
    SDK iOS 10.12.0+

Attiva MFA TOTP a livello di progetto

Per abilitare TOTP come secondo fattore, utilizza l'SDK Admin o chiama l'endpoint REST di configurazione del progetto.

Per utilizzare SDK Admin:

  1. Se non l'hai ancora fatto, installa l'SDK Node.js di Firebase Admin.

    TOTP MFA è supportato solo nell'SDK Node.js di Firebase Admin versione 11.6.0 e successive.

  2. Esegui questo comando:

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

    Sostituisci quanto segue:

    • NUM_ADJ_INTERVALS: il numero di intervalli di finestre temporali adiacenti da cui accettare i TOTP, da zero a dieci. Il valore predefinito è cinque.

      I TOTP assicurano che quando due parti (di prova e convalida) generano OTP nella stessa finestra temporale (in genere di 30 secondi), generino la stessa password. Tuttavia, per soddisfare la deviazione dell'orologio tra le parti e i tempi di risposta umani, puoi configurare il servizio TOTP in modo che accetti anche TOTP da finestre adiacenti.

Per abilitare TOTP MFA utilizzando l'API REST, esegui questo comando:

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

Sostituisci quanto segue:

  • PROJECT_ID: l'ID progetto.
  • NUM_ADJ_INTERVALS: il numero di intervalli della finestra temporale, da zero a dieci. Il valore predefinito è cinque.

    I TOTP assicurano che quando due parti (di prova e convalida) generano OTP nella stessa finestra temporale (in genere di 30 secondi), generino la stessa password. Tuttavia, per soddisfare la deviazione dell'orologio tra le parti e i tempi di risposta umani, puoi configurare il servizio TOTP in modo che accetti anche TOTP da finestre adiacenti.

Abilita MFA TOTP a livello di tenant

Per abilitare TOTP come secondo fattore per l'MFA a livello di tenant, utilizza il codice seguente:

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

Sostituisci quanto segue:

  • TENANT_ID: l'ID tenant della stringa.
  • NUM_ADJ_INTERVALS: il numero di intervalli della finestra temporale, da zero a dieci. Il valore predefinito è cinque.

Per abilitare TOTP MFA utilizzando l'API REST a livello di tenant, esegui questo comando:

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

Sostituisci quanto segue:

  • PROJECT_ID: l'ID progetto.
  • TENANT_ID: l'ID tenant.
  • NUM_ADJ_INTERVALS: il numero di intervalli della finestra temporale, da zero a dieci. Il valore predefinito è cinque.

Scegli un pattern di registrazione

Puoi scegliere se la tua app richiede l'autenticazione a più fattori e come e quando registrare gli utenti. Ecco alcuni pattern comuni:

  • Registra il secondo fattore dell'utente come parte della registrazione. Utilizza questo metodo se la tua app richiede l'autenticazione a più fattori per tutti gli utenti.

  • Offri un'opzione ignorabile per registrare un secondo fattore durante la registrazione. Se vuoi incoraggiare, ma non richiedere l'autenticazione a più fattori nella tua app, puoi utilizzare questo approccio.

  • Fornisci la possibilità di aggiungere un secondo fattore dalla pagina di gestione dell'account o del profilo dell'utente, anziché dalla schermata di registrazione. Questo riduce al minimo l'attrito durante il processo di registrazione, pur rendendo l'autenticazione a più fattori disponibile per gli utenti sensibili alla sicurezza.

  • Richiedi l'aggiunta di un secondo fattore in modo incrementale quando l'utente vuole accedere a funzionalità con requisiti di sicurezza maggiori.

Registrare utenti in MFA TOTP

Dopo aver abilitato MFA TOTP come secondo fattore per la tua app, implementa la logica lato client per registrare gli utenti in MFA 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) {
      // ...

    }];
  }];
}];

Eseguire l'accesso degli utenti con un secondo fattore

Per accedere agli utenti con MFA TOTP, utilizza il codice seguente:

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.
  }
}

L'esempio sopra utilizza email e password come primo fattore.

Annulla la registrazione a TOTP MFA

Questa sezione descrive come gestire l'annullamento della registrazione di un utente alla MFA TOTP.

Se un utente si è registrato per più opzioni MFA e annulla la registrazione all'opzione abilitata più di recente, riceverà un auth/user-token-expired e verrà disconnesso. L'utente deve accedere di nuovo e verificare le proprie credenziali esistenti, ad esempio un indirizzo email e una password.

Per annullare la registrazione dell'utente, gestire l'errore e attivare la riautenticazione, utilizza il codice seguente:

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

Passaggi successivi