Consolidate "email OTP required?" logic

The following discussion from !210998 (merged) should be addressed:

User

Just checks if the FF is enabled and the date is in the past

  def email_based_otp_required?
    Feature.enabled?(:email_based_mfa, self) &&
      !!email_otp_required_after&.past?
  end

VerifiesWithEmail Controller Check

Allows future-dated, for graceful rollout. Rejects when hasn't signed in at least once.

  # Checks whether email-based OTP is required for the current sign-in
  # attempt.
  #
  # This feature uses a two-part rollout mechanism:
  #   - Feature Flag acts as a kill switch that can be quickly disabled
  #     via ChatOps
  #   - User enrollment is controlled by setting the attribute
  #     email_otp_required_after
  #
  # This allows us to halt or revert the rollout immediately while
  # preserving per-user enrollment dates.
  #
  # Later, the Feature Flag will be changed to an ApplicationSetting so
  # that self-managed administrators can turn this feature on after
  # validating that they have mail delivery correctly configured.
  def require_email_based_otp?(user)
    return false unless Feature.enabled?(:email_based_mfa, user)

    password_based_login? &&
      # Skip on first log in (which occurs for most during account
      # creation), to avoid double email verification with
      # Devise::Confirmable
      user.last_sign_in_at.present? &&
      user.email_otp_required_after.present? &&
      (user.email_otp_required_after <= Time.zone.now || in_email_otp_grace_period?(user))
  end

Sessions Helper

Requires it to be in the past too, plus checks "not locked".

  def fallback_to_email_otp_permitted?(user)
    Feature.enabled?(:email_based_mfa, user) &&
      user.email_otp_required_after&.past? &&
      !treat_as_locked?(user)
  end

Proposal

  • Consolidate the common logic on the User model, and re-use it