Repository: logto-io/logto
File: packages/core/src/routes/experience/classes/experience-interaction.ts (TypeScript)

Code before:

    const user = await this.getIdentifiedUser();
    const mfaSettings = await this.signInExperienceValidator.getMfaSettings();
    const adaptiveMfaResult = await this.adaptiveMfaValidator.getResult(user);
    const mfaValidator = new MfaValidator(mfaSettings, user, adaptiveMfaResult);

    if (adaptiveMfaResult) {
      log?.append({ adaptiveMfaResult });
    }

    if (
      adaptiveMfaResult?.requiresMfa &&
      mfaValidator.availableUserMfaVerificationTypes.length === 0
    ) {
      const { primaryEmail, primaryPhone } = this.profile.data;
      const hasPendingImplicitFactors =
        (mfaSettings.factors.includes(MfaFactor.EmailVerificationCode) && Boolean(primaryEmail)) ||
        (mfaSettings.factors.includes(MfaFactor.PhoneVerificationCode) && Boolean(primaryPhone));
      const hasPendingBindingFactors =
        this.mfa.bindMfaFactorsArray.length > 0 || hasPendingImplicitFactors;

      // If the user has already bound a factor in this interaction, do not block
      // with `user.missing_mfa` again. The new factor will be persisted on submit.
      if (hasPendingBindingFactors) {
        return;
      }

      // Adaptive MFA triggered but user has no usable factors.
      // Force binding flow and attach available factors from SIE settings.
      // Avoid throwing `session.mfa.require_mfa_verification` with empty options.
      const availableFactors =
        await this.signInExperienceValidator.getAvailableMfaFactorsForBinding();

      // If no factors are enabled in SIE, surface a config error instead of sending
      // users into a binding flow with an empty list.
      assertThat(
        availableFactors.length > 0,
        new RequestError({ code: 'session.mfa.mfa_factor_not_enabled', status: 400 })
      );

      const preBindVerificationFactors =
        await this.signInExperienceValidator.getPreBindMfaVerificationFactors(user);
      const hasVerifiedPreBindVerification = this.verificationRecordsArray.some(
        (record) =>
          (record.type === VerificationType.MfaEmailVerificationCode ||
            record.type === VerificationType.MfaPhoneVerificationCode) &&
          record.isVerified
      );

      if (preBindVerificationFactors.length > 0 && !hasVerifiedPreBindVerification) {
        // If user has primary email/phone and a corresponding connector, require pre-bind

Review comment (suggestion):
This method should only guard the validation status of the current user if the user has any existing enabled mfa factors. 
If MFA is required (either adaptively or mandatory by policy),  but use user enabled mfa factors is empty ,the logic should be handled in the `mfa.assertUserMandatoryMfaFulfilled.` method


Code after:
      return;
    }

    const user = await this.getIdentifiedUser();
    const mfaSettings = await this.signInExperienceValidator.getMfaSettings();
    const adaptiveMfaResult = await this.adaptiveMfaValidator.getResult();
    const mfaValidator = new MfaValidator(mfaSettings, user, adaptiveMfaResult);

    if (adaptiveMfaResult) {
      log?.append({ adaptiveMfaResult });
    }

    if (!mfaValidator.isMfaRequired) {
      return;
    }

    if (EnvSet.values.isDevFeaturesEnabled && adaptiveMfaResult?.requiresMfa) {
      this.ctx.assignInteractionHookResult({
        event: InteractionHookEvent.PostSignInAdaptiveMfaTriggered,
        payload: { adaptiveMfaResult },
        userId: user.id,
      });
    }

    const { primaryEmail, primaryPhone } = user;
    const maskedIdentifiers: Record<string, string> = {
      ...(mfaValidator.availableUserMfaVerificationTypes.includes(
        MfaFactor.EmailVerificationCode
      ) && primaryEmail
        ? { [MfaFactor.EmailVerificationCode]: maskEmail(primaryEmail) }
        : {}),
      ...(mfaValidator.availableUserMfaVerificationTypes.includes(
        MfaFactor.PhoneVerificationCode
      ) && primaryPhone
        ? { [MfaFactor.PhoneVerificationCode]: maskPhone(primaryPhone) }
        : {}),
    };

    assertThat(
      mfaValidator.isMfaVerified(this.verificationRecordsArray),
      new RequestError(
        { code: 'session.mfa.require_mfa_verification', status: 403 },
        {
          availableFactors: mfaValidator.availableUserMfaVerificationTypes,
          maskedIdentifiers,
        }
      )
    );
  }

  /**