rubygems/rubygems.org

View on GitHub
app/controllers/passwords_controller.rb

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
class PasswordsController < ApplicationController
  include MfaExpiryMethods
  include WebauthnVerifiable
  include SessionVerifiable

  before_action :ensure_email_present, only: %i[create]

  before_action :validate_confirmation_token, only: %i[edit otp_edit webauthn_edit]
  before_action :session_expired_failure, only: %i[otp_edit webauthn_edit], unless: :mfa_session_active?
  before_action :webauthn_failure, only: %i[webauthn_edit], unless: :webauthn_credential_verified?
  before_action :otp_failure, only: %i[otp_edit], unless: :otp_edit_conditions_met?
  after_action :delete_mfa_expiry_session, only: %i[otp_edit webauthn_edit]

  verify_session_before only: %i[update]

  def new
  end

  def edit
    if @user.mfa_enabled?
      @otp_verification_url = otp_edit_password_url(token: @user.confirmation_token)
      setup_webauthn_authentication(form_url: webauthn_edit_password_url(token: @user.confirmation_token))

      create_new_mfa_expiry

      render template: "multifactor_auths/prompt"
    else
      # When user doesn't have mfa, a valid token is a full "magic link" sign in.
      verified_sign_in
      render :edit
    end
  end

  def create
    user = User.find_by_normalized_email(@email)

    if user
      user.forgot_password!
      ::PasswordMailer.change_password(user).deliver_later
    end

    render :create, status: :accepted
  end

  def update
    if current_user.update_password reset_params[:password]
      current_user.reset_api_key! if reset_params[:reset_api_key] == "true" # singular
      current_user.api_keys.expire_all! if reset_params[:reset_api_keys] == "true" # plural
      redirect_to dashboard_path
      session[:password_reset_token] = nil
    else
      flash.now[:alert] = t(".failure")
      render :edit
    end
  end

  def otp_edit
    verified_sign_in
    render :edit
  end

  def webauthn_edit
    verified_sign_in
    render :edit
  end

  private

  def verified_sign_in
    sign_in @user
    session_verified
    @user.update!(confirmation_token: nil)
    StatsD.increment "login.success"
  end

  def reset_params
    params.fetch(:password_reset, {}).permit(:password, :reset_api_key, :reset_api_keys)
  end

  def ensure_email_present
    @email = params.dig(:password, :email)
    return if @email.present?

    flash.now[:alert] = t(".failure_on_missing_email")
    render template: "passwords/new", status: :unprocessable_entity
  end

  def validate_confirmation_token
    @user = User.find_by(confirmation_token: params[:token].to_s)
    redirect_to root_path, alert: t("passwords.edit.token_failure") unless @user&.valid_confirmation_token?
  end

  def otp_edit_conditions_met? = @user.mfa_enabled? && @user.ui_mfa_verified?(params[:otp]) && mfa_session_active?

  def session_expired_failure = login_failure(t("multifactor_auths.session_expired"))
  def webauthn_failure = login_failure(@webauthn_error)
  def otp_failure = login_failure(t("multifactor_auths.incorrect_otp"))

  def login_failure(message)
    flash.now.alert = message
    render template: "multifactor_auths/prompt", status: :unauthorized
  end

  def redirect_to_verify
    session[:redirect_uri] = verify_session_redirect_path
    redirect_to verify_session_path, alert: t("verification_expired")
  end
end