From 9f76b68ebcf4997dd34ac176def81d7a8932c8fc Mon Sep 17 00:00:00 2001 From: EchoEkhi Date: Mon, 18 May 2026 03:54:33 +0100 Subject: [PATCH 1/6] AO3-7442 Add TOTP 2FA for users --- app/controllers/application_controller.rb | 2 +- app/controllers/users/sessions_controller.rb | 80 +++++++++++++++++- app/controllers/users/totp_controller.rb | 83 +++++++++++++++++++ app/models/user.rb | 43 +++++++++- app/views/preferences/index.html.erb | 20 +++++ app/views/users/sessions/totp.html.erb | 17 ++++ app/views/users/totp/confirm_disable.html.erb | 17 ++++ app/views/users/totp/new.html.erb | 17 ++++ .../users/totp/reauthenticate_create.erb | 38 +++++++++ .../users/totp/show_backup_codes.html.erb | 33 ++++++++ config/config.yml | 2 + config/initializers/devise.rb | 2 + config/locales/controllers/en.yml | 24 ++++++ config/locales/views/en.yml | 48 +++++++++++ config/routes.rb | 6 ++ ...17234000_add_devise_two_factor_to_users.rb | 12 +++ 16 files changed, 441 insertions(+), 3 deletions(-) create mode 100644 app/controllers/users/totp_controller.rb create mode 100644 app/views/users/sessions/totp.html.erb create mode 100644 app/views/users/totp/confirm_disable.html.erb create mode 100644 app/views/users/totp/new.html.erb create mode 100644 app/views/users/totp/reauthenticate_create.erb create mode 100644 app/views/users/totp/show_backup_codes.html.erb create mode 100644 db/migrate/20260517234000_add_devise_two_factor_to_users.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 089bf55e286..7c84063f3fe 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -218,7 +218,7 @@ def load_tos_popup end include PathCleaner - # Warning: Admin 2FA bypasses this method in SessionsController#authenticate_admin_with_otp_two_factor + # Warning: The User and Admin 2FA login flows bypass this method def after_sign_in_path_for(resource) if resource.respond_to?(:pwned?) && resource.pwned? set_flash_message! :alert, :warn_pwned diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb index 7eb3b941222..d27ac3a40de 100644 --- a/app/controllers/users/sessions_controller.rb +++ b/app/controllers/users/sessions_controller.rb @@ -1,8 +1,12 @@ class Users::SessionsController < Devise::SessionsController - layout "session" before_action :admin_logout_required + prepend_before_action :authenticate_with_totp_two_factor, + if: -> { action_name == "create" && totp_two_factor_enabled? } + + protect_from_forgery with: :exception, prepend: true, except: :destroy + # POST /users/login def create super do |resource| @@ -28,4 +32,78 @@ def destroy redirect_to relative_path(params[:return_to]) || root_path end + + # Two-Factor Authentication + def authenticate_with_totp_two_factor + user = self.resource = find_user + + if params[:totp_attempt].present? && session[:otp_user_id] + authenticate_user_with_otp_two_factor(user) + elsif user&.valid_password?(user_params[:password]) + prompt_for_otp_two_factor(user) + end + end + + private + + def valid_totp_attempt?(user) + user.validate_and_consume_otp!(params[:totp_attempt]) || + user.invalidate_otp_backup_code!(params[:totp_attempt]) + end + + def prompt_for_otp_two_factor(user) + @user = user + + session[:otp_user_id] = user.id + + session[:pwned] = user.respond_to?(:password_pwned?) && user.password_pwned?(user_params[:password]) if params[:user] && params[:user][:password] + + render "users/sessions/totp" + end + + def authenticate_user_with_otp_two_factor(user) + if valid_totp_attempt?(user) + pwned = session[:pwned] + # Remove any lingering user data from login + session.delete(:otp_user_id) + session.delete(:pwned) + + user.save! + + flash[:notice] = t("devise.sessions.signed_in") + sign_in(user, event: :authentication) + + # Set the user_credentials flag cookie + # because this login flow bypasses ensure_user_credentials#ensure_user_credentials + cookies[:user_credentials] = { value: 1, expires: 1.year.from_now } unless cookies[:user_credentials] + + if pwned + # Set the pwned flash notice + # because this login flow bypasses ApplicationController#after_sign_in_path_for + set_flash_message! :alert, :warn_pwned + redirect_to change_password_user_path(user) + else + redirect_to root_path + end + else + flash.now[:error] = t("users.sessions.invalid_totp") + prompt_for_otp_two_factor(user) + end + end + + def user_params + params.require(:user).permit(:login, :password, :remember_me) + end + + def find_user + if session[:otp_user_id] + User.find(session[:otp_user_id]) + elsif user_params[:login] + User.find_by(login: user_params[:login]) + end + end + + def totp_two_factor_enabled? + find_user&.totp_enabled? + end end diff --git a/app/controllers/users/totp_controller.rb b/app/controllers/users/totp_controller.rb new file mode 100644 index 00000000000..9121e070bfd --- /dev/null +++ b/app/controllers/users/totp_controller.rb @@ -0,0 +1,83 @@ +class Users::TotpController < ApplicationController + before_action :check_totp_disabled, only: [:new, :reauthenticate_create, :create] + before_action :check_totp_enabled, only: [:confirm_disable, :disable] + + def new + @page_subtitle = t(".page_title") + end + + def reauthenticate_create + unless current_user.valid_password?(params[:password_check]) + flash[:error] = t(".incorrect_password") + redirect_to new_user_totp_path and return + end + + current_user.generate_totp_secret_if_missing! + @page_subtitle = t(".page_title") + end + + def create + if current_user.validate_and_consume_otp!(params[:totp_attempt]) + current_user.enable_totp! + + flash[:notice] = t(".success") + redirect_to show_backup_codes_user_totp_path + else + flash.now[:error] = t(".incorrect_code") + render action: :new and return + end + end + + def show_backup_codes + unless current_user.totp_enabled? + flash[:error] = t(".not_enabled") + redirect_to new_user_totp_path and return + end + + @page_subtitle = t(".page_title") + @backup_codes = current_user.generate_otp_backup_codes! + current_user.save! + end + + def confirm_disable + @page_subtitle = t(".page_title") + end + + def disable + unless current_user.valid_password?(params[:password_check]) + flash.now[:error] = t(".incorrect_password") + render action: :confirm_disable and return + end + + if current_user.disable_totp! + flash[:notice] = t(".success") + else + flash[:error] = t(".failure") + end + + redirect_to user_preferences_path + end + + private + + def require_user_owner + return if params[:user_id] == current_user.login + + flash[:error] = t("users.totp.access.permission_denied_generic") + redirect_to root_path + end + + def check_totp_enabled + return if current_user.totp_enabled? + + flash[:error] = t("users.totp.already_disabled") + redirect_to user_preferences_path + end + + def check_totp_disabled + return unless current_user.totp_enabled? + + flash[:error] = t("users.totp.already_enabled") + redirect_to user_preferences_path + end +end diff --git a/app/models/user.rb b/app/models/user.rb index f3b74d0cb77..56db10a930c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -14,6 +14,10 @@ class User < ApplicationRecord :validatable, :lockable, :recoverable, + :two_factor_authenticatable, + :two_factor_backupable, + otp_backup_code_length: ArchiveConfig.USER_TOTP_BACKUP_CODE_LENGTH, + otp_number_of_backup_codes: ArchiveConfig.USER_TOTP_BACKUP_CODE_COUNT, reset_password_keys: [:email] devise :pwned_password unless Rails.env.test? @@ -36,7 +40,7 @@ class User < ApplicationRecord has_one :invitation, as: :invitee has_many :user_invite_requests, dependent: :destroy - attr_accessor :invitation_token + attr_accessor :invitation_token, :otp_plain_backup_codes before_create :create_default_associateds # attr_accessible :invitation_token after_create :mark_invitation_redeemed, :remove_from_queue @@ -175,6 +179,43 @@ class User < ApplicationRecord has_many :log_items, dependent: :destroy validates_associated :log_items + ##################################### + # TOTP 2FA + ##################################### + + serialize :otp_backup_codes, type: Array, coder: YAML, yaml: { permitted_classes: [String] } + + # Generate a TOTP secret it it does not already exist + def generate_totp_secret_if_missing! + return unless otp_secret.nil? + + update!(otp_secret: User.generate_otp_secret) + end + + # Ensure that the user is prompted for their TOTP when they login + def enable_totp! + update!(otp_required_for_login: true) + end + + # Disable the use of TOTP-based two-factor. + def disable_totp! + update!( + otp_required_for_login: false, + otp_secret: nil, + otp_backup_codes: nil + ) + end + + # Whether this user has TOTP enabled. + def totp_enabled? + otp_required_for_login + end + + # URI for TOTP two-factor QR code + def totp_qr_code_uri + otp_provisioning_uri(login, issuer: ArchiveConfig.APP_NAME) + end + def canonicalize_email self.canonical_email = EmailCanonicalizer.canonicalize(self.email) if self.email end diff --git a/app/views/preferences/index.html.erb b/app/views/preferences/index.html.erb index fa7cfb6e8e2..84b8301b513 100644 --- a/app/views/preferences/index.html.erb +++ b/app/views/preferences/index.html.erb @@ -12,6 +12,13 @@
  • <%= link_to t(".navigation.blocked_users"), user_blocked_users_path %>
  • <%= link_to t(".navigation.muted_users"), user_muted_users_path %>
  • <%= link_to t(".navigation.change_username"), change_username_user_path(@user) %>
  • +
  • + <% if current_user.totp_enabled? %> + <%= link_to t(".navigation.disable_two_step_verification"), confirm_disable_user_totp_path %> + <% else %> + <%= link_to t(".navigation.enable_two_step_verification"), new_user_totp_path %> + <% end %> +
  • <%= link_to t(".navigation.change_password"), change_password_user_path(@user) %>
  • <%= link_to t(".navigation.change_email"), change_email_user_path(@user) %>
  • @@ -19,6 +26,19 @@ <%= form_for(@preference, url: user_preference_path(@user, @preference), autocomplete: "off") do |f| %> +
    + <%= t(".account_security.legend") %> +

    <%= t(".account_security.heading") %>

    + +
    <%= t(".privacy.legend") %>

    <%= t(".privacy.heading") %> <%= link_to_help_modal(help_preferences_privacy_path, t(".privacy.help_title")) %>

    diff --git a/app/views/users/sessions/totp.html.erb b/app/views/users/sessions/totp.html.erb new file mode 100644 index 00000000000..b20334bdfa4 --- /dev/null +++ b/app/views/users/sessions/totp.html.erb @@ -0,0 +1,17 @@ +<%= form_with(url: session_path(resource), method: :post, class: "totp simple confirm") do |form| %> +
    + + +

    <%= t(".page_heading") %>

    +

    <%= t(".enter_totp") %>

    + + + +

    + <%= form.label :totp_attempt, t(".totp_label") %> + <%= form.text_field :totp_attempt %> + <%= submit_tag t(".submit") %> +

    + +
    +<% end %> \ No newline at end of file diff --git a/app/views/users/totp/confirm_disable.html.erb b/app/views/users/totp/confirm_disable.html.erb new file mode 100644 index 00000000000..71282bbbb8b --- /dev/null +++ b/app/views/users/totp/confirm_disable.html.erb @@ -0,0 +1,17 @@ +<%= form_with(url: disable_user_totp_path, method: :post, class: "password simple confirm") do |form| %> +
    + + +

    <%= t(".page_heading") %>

    +

    <%= t(".required_before_disabling") %>

    + + + +

    + <%= form.label :password_check, t(".password_label") %> + <%= form.password_field :password_check %> + <%= form.submit t(".submit") %> +

    + +
    +<% end %> diff --git a/app/views/users/totp/new.html.erb b/app/views/users/totp/new.html.erb new file mode 100644 index 00000000000..7b3f850148e --- /dev/null +++ b/app/views/users/totp/new.html.erb @@ -0,0 +1,17 @@ +<%= form_with(url: reauthenticate_create_user_totp_path, method: :post, class: "password simple confirm") do |form| %> +
    + + +

    <%= t(".page_heading") %>

    +

    <%= t(".required_before_enabling") %>

    + + + +

    + <%= form.label :password_check, t(".password_label") %> + <%= form.password_field :password_check %> + <%= form.submit t(".submit") %> +

    + +
    +<% end %> \ No newline at end of file diff --git a/app/views/users/totp/reauthenticate_create.erb b/app/views/users/totp/reauthenticate_create.erb new file mode 100644 index 00000000000..bd1cf191316 --- /dev/null +++ b/app/views/users/totp/reauthenticate_create.erb @@ -0,0 +1,38 @@ + +

    <%= t(".page_heading") %>

    + +
    +

    <%= t(".about.heading") %>

    +

    <%= t(".about.general_info") %>

    +

    <%= t(".about.effects_of_enabling", app_name: ArchiveConfig.APP_SHORT_NAME) %>

    +
    + + + +
    +

    <%= t(".app_setup.heading") %>

    +

    <%= t(".app_setup.instructions") %>

    +
    +

    <%= qr_code_as_svg(current_user.totp_qr_code_uri) %>

    +
    +

    <%= t(".app_setup.key.heading") %>

    + <%= current_user.otp_secret.scan(/.{1,4}/).join(" ") %> +

    <%= t(".app_setup.key.format_note") %>

    +

    Setup Link

    + Click to import settings into your authenticator app +
    +
    +
    + +
    +

    <%= t(".enter_code.heading") %>

    +

    <%= t(".enter_code.instructions") %>

    + <%= form_with(url: user_totp_path, method: :post, class: "totp simple confirm") do |form| %> +

    + <%= form.label :totp_attempt, t(".enter_code.label") %> + <%= form.text_field :totp_attempt %> + <%= form.submit t(".enter_code.submit"), data: { disable_with: t(".enter_code.please_wait") } %> +

    + <% end %> +
    + diff --git a/app/views/users/totp/show_backup_codes.html.erb b/app/views/users/totp/show_backup_codes.html.erb new file mode 100644 index 00000000000..521ad003d5b --- /dev/null +++ b/app/views/users/totp/show_backup_codes.html.erb @@ -0,0 +1,33 @@ + +

    <%= t(".page_heading") %>

    + +

    <%= t(".write_codes_down") %>

    + + + + + +

    + + <%= link_to t(".finish"), user_preferences_path %> +

    + + +<% content_for :footer_js do %> + +<% end %> diff --git a/config/config.yml b/config/config.yml index dba41f3485b..a94be58a481 100644 --- a/config/config.yml +++ b/config/config.yml @@ -124,6 +124,8 @@ ADMIN_PASSWORD_LENGTH_MIN: 10 ADMIN_PASSWORD_LENGTH_MAX: 72 ADMIN_TOTP_BACKUP_CODE_LENGTH: 16 ADMIN_TOTP_BACKUP_CODE_COUNT: 10 +USER_TOTP_BACKUP_CODE_LENGTH: 16 +USER_TOTP_BACKUP_CODE_COUNT: 10 # The maximum number of tags you can add to a collection COLLECTION_TAGS_MAX: 10 diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index b46e58a708e..afd72bd694a 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -4,6 +4,8 @@ config.warden do |manager| manager.default_strategies(scope: :admin).unshift :two_factor_authenticatable manager.default_strategies(scope: :admin).unshift :two_factor_backupable + manager.default_strategies(scope: :user).unshift :two_factor_authenticatable + manager.default_strategies(scope: :user).unshift :two_factor_backupable end # The secret key used by Devise. Devise uses this key to generate diff --git a/config/locales/controllers/en.yml b/config/locales/controllers/en.yml index 880ebc12684..bc32510ad58 100644 --- a/config/locales/controllers/en.yml +++ b/config/locales/controllers/en.yml @@ -384,9 +384,33 @@ en: page_title: Account Created new: page_title: Create Account + sessions: + invalid_totp: Incorrect two-step verification code. If you no longer have access to your authenticator app, you can enter one of your backup codes instead. status: ban_notice_html: Your account has been banned. You are not permitted to post or edit content on AO3. Please check your email or %{contact_abuse_link} for more information. suspension_notice_html: Your account has been suspended until %{suspended_until}. You cannot post, edit, or delete content until your suspension has ended. Please check your email or %{contact_abuse_link} for more information. + totp: + access: + permission_denied_generic: Sorry, you don't have permission to access the page you were trying to reach. + already_disabled: TOTP two-step verification is already disabled. + already_enabled: TOTP two-step verification is already enabled. + confirm_disable: + page_title: Disable Two-Step Verification + create: + incorrect_code: Incorrect verification code. Your code may have expired, or you may need to set up your authenticator app again. + success: Successfully enabled two-step verification; please make note of your backup codes. + disable: + failure: Could not disable two-step verification. + incorrect_password: Your password was incorrect. Please try again or, if you've forgotten your password, log out and reset your password via the link on the login form. + success: Successfully disabled two-step verification. + new: + page_title: Confirm Password + reauthenticate_create: + incorrect_password: Your password was incorrect. Please try again or, if you've forgotten your password, log out and reset your password via the link on the login form. + page_title: Enable Two-Step Verification + show_backup_codes: + not_enabled: Please enable two-step verification first. + page_title: Two-Step Verification Backup Codes works: create: draft_notice_html: Draft was successfully created. It will be %{scheduled_for_deletion_bold} on %{deletion_date}. diff --git a/config/locales/views/en.yml b/config/locales/views/en.yml index 630a3291f93..f94b1ff22f9 100644 --- a/config/locales/views/en.yml +++ b/config/locales/views/en.yml @@ -2587,6 +2587,12 @@ en: page_heading: Matching for %{collection_title} preferences: index: + account_security: + change_password: Change password + disable_two_step_verification: Disable two-step verification + enable_two_step_verification: Enable two-step verification + heading: Account Security + legend: Account Security browser_page_title_format: Browser page title format collections_challenges_gifts: allow_collection_invitation: Allow others to invite my works to collections. @@ -2628,7 +2634,9 @@ en: change_email: Change Email change_password: Change Password change_username: Change Username + disable_two_step_verification: Disable Two-Step Verification edit_my_profile: Edit My Profile + enable_two_step_verification: Enable Two-Step Verification landmark: Navigation manage_my_pseuds: Manage My Pseuds muted_users: Muted Users @@ -3189,6 +3197,11 @@ en: password: 'Password:' remember_me: Remember Me username_or_email: 'Username or email:' + totp: + enter_totp: Enter the verification code from your authenticator app to finish logging in to your account. + page_heading: Two-Step Verification + submit: Log In + totp_label: 6-digit verification code show: login_banner: contact_abuse: contact our Policy & Abuse team @@ -3230,6 +3243,41 @@ en: co_creator_requests: Co-Creator Requests (%{count}) related_works: Related Works (%{related_works_number}) sign_ups: Sign-ups (%{signup_number}) + totp: + confirm_disable: + page_heading: Disable Two-Step Verification + password_label: Password + required_before_disabling: Before disabling two-step verification, you need to verify your password. + submit: Disable Two-Step Verification + new: + page_heading: Confirm Password + password_label: Password + required_before_enabling: Before enabling two-step verification, you need to verify your password. + submit: Continue + reauthenticate_create: + about: + effects_of_enabling: When two-step verification is enabled, logging in to your %{app_name} account will require both your password and a verification code from your authenticator app. + general_info: Two-step verification, also called two-factor authentication, helps prevent other people from accessing your account even if they know your password. All you need to set it up is an authenticator app such as 1Password, Google Authenticator, or Twilio Authy. + heading: About Two-Step Verification + app_setup: + heading: 'Step 1: Set up your authenticator app' + instructions: Use your authenticator app to scan the QR code or enter the manual setup key. If you are using the mobile device the app is already installed on, click the setup link. The app will give you a 6-digit code, which you'll use in the next step. + key: + format_note: Spaces and capitalization don't matter + heading: Setup Key + enter_code: + heading: 'Step 2: Enter your 6-digit code' + instructions: Enter the 6-digit code from your app to enable two-step verification. + label: 6-digit code + please_wait: Please wait... + submit: Enable Two-Step Verification + page_heading: Set Up Two-Step Verification + show_backup_codes: + backup_codes_copied: Backup Codes Copied! + copy_to_clipboard: Copy Backup Codes to Clipboard + finish: Finish + page_heading: Two-Step Verification Backup Codes + write_codes_down: 'Keep these backup codes in a safe and secure place in case you lose access to your authenticator app:' works: adult: caution: This work could have adult content. If you continue, you have agreed that you are willing to see such content. diff --git a/config/routes.rb b/config/routes.rb index 5c5c951551f..73cf43a3362 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -310,6 +310,12 @@ end resources :nominations, controller: "tag_set_nominations", only: [:index] resources :preferences, only: [:index, :update] + resource :totp, controller: "users/totp", only: [:create, :new] do + get :show_backup_codes + get :confirm_disable + post :disable + post :reauthenticate_create + end resource :profile, only: [:show, :edit, :update], controller: "profile" do collection do get :pseuds diff --git a/db/migrate/20260517234000_add_devise_two_factor_to_users.rb b/db/migrate/20260517234000_add_devise_two_factor_to_users.rb new file mode 100644 index 00000000000..10fd0bcf44f --- /dev/null +++ b/db/migrate/20260517234000_add_devise_two_factor_to_users.rb @@ -0,0 +1,12 @@ +class AddDeviseTwoFactorToUsers < ActiveRecord::Migration[8.1] + uses_departure! if Rails.env.staging? || Rails.env.production? + + def change + change_table :users, bulk: true do |t| + t.column :otp_secret, :string + t.column :consumed_timestep, :integer + t.column :otp_required_for_login, :boolean + t.column :otp_backup_codes, :text + end + end +end From 1414ed7067b08bcc777f0e55330239a6d487c451 Mon Sep 17 00:00:00 2001 From: EchoEkhi Date: Mon, 18 May 2026 04:06:10 +0100 Subject: [PATCH 2/6] Add copy key button --- .../users/totp/reauthenticate_create.erb | 24 ++++++++++++++++--- config/locales/views/en.yml | 4 +++- .../site/2.0/22-system-messages.css | 6 +++++ 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/app/views/users/totp/reauthenticate_create.erb b/app/views/users/totp/reauthenticate_create.erb index bd1cf191316..a56905743c7 100644 --- a/app/views/users/totp/reauthenticate_create.erb +++ b/app/views/users/totp/reauthenticate_create.erb @@ -16,10 +16,17 @@

    <%= qr_code_as_svg(current_user.totp_qr_code_uri) %>

    <%= t(".app_setup.key.heading") %>

    - <%= current_user.otp_secret.scan(/.{1,4}/).join(" ") %> + <%= setup_key = current_user.otp_secret.scan(/.{1,4}/).join(" ") %>

    <%= t(".app_setup.key.format_note") %>

    -

    Setup Link

    - Click to import settings into your authenticator app +

    + +

    +

    Setup on This Device

    +

    + Import Settings +

    @@ -36,3 +43,14 @@ <% end %> + +<% content_for :footer_js do %> + +<% end %> \ No newline at end of file diff --git a/config/locales/views/en.yml b/config/locales/views/en.yml index f94b1ff22f9..e684384efc7 100644 --- a/config/locales/views/en.yml +++ b/config/locales/views/en.yml @@ -3261,8 +3261,10 @@ en: heading: About Two-Step Verification app_setup: heading: 'Step 1: Set up your authenticator app' - instructions: Use your authenticator app to scan the QR code or enter the manual setup key. If you are using the mobile device the app is already installed on, click the setup link. The app will give you a 6-digit code, which you'll use in the next step. + instructions: Use your authenticator app to scan the QR code or enter the manual setup key. If you are using the mobile device the app is already installed on, click the import settings button. The app will give you a 6-digit code, which you'll use in the next step. key: + copied: Copied! + copy_to_clipboard: Copy format_note: Spaces and capitalization don't matter heading: Setup Key enter_code: diff --git a/public/stylesheets/site/2.0/22-system-messages.css b/public/stylesheets/site/2.0/22-system-messages.css index 6c0247f6d7a..1498d2b877f 100644 --- a/public/stylesheets/site/2.0/22-system-messages.css +++ b/public/stylesheets/site/2.0/22-system-messages.css @@ -123,6 +123,12 @@ from the main flow. */ float: none; } +.annotation .actions { + float: none; + margin-block-end: 0; + padding-block-end: 0; +} + /* contexts */ .actions + .footnote { From 97f69cce2df0d26d0c6407f3a0eba5b553a8a848 Mon Sep 17 00:00:00 2001 From: EchoEkhi Date: Mon, 18 May 2026 05:59:27 +0100 Subject: [PATCH 3/6] Add tests --- app/controllers/users/totp_controller.rb | 7 + features/step_definitions/user_steps.rb | 19 ++ .../authenticate_users_with_totp.feature | 170 +++++++++++ .../controllers/users/totp_controller_spec.rb | 272 ++++++++++++++++++ 4 files changed, 468 insertions(+) create mode 100644 features/users/authenticate_users_with_totp.feature create mode 100644 spec/controllers/users/totp_controller_spec.rb diff --git a/app/controllers/users/totp_controller.rb b/app/controllers/users/totp_controller.rb index 9121e070bfd..04b9c0ed14d 100644 --- a/app/controllers/users/totp_controller.rb +++ b/app/controllers/users/totp_controller.rb @@ -1,7 +1,14 @@ class Users::TotpController < ApplicationController + before_action :load_user + before_action :check_ownership_or_admin before_action :check_totp_disabled, only: [:new, :reauthenticate_create, :create] before_action :check_totp_enabled, only: [:confirm_disable, :disable] + def load_user + @user = User.find_by!(login: params[:user_id]) + @check_ownership_of = @user + end + def new @page_subtitle = t(".page_title") end diff --git a/features/step_definitions/user_steps.rb b/features/step_definitions/user_steps.rb index 595cd2472e8..9240ccb456e 100644 --- a/features/step_definitions/user_steps.rb +++ b/features/step_definitions/user_steps.rb @@ -412,3 +412,22 @@ def get_series_name(age, classname, name) invitation_id = User.find_by(login: login).invitation.id step %{I should see "Invitation: #{invitation_id}"} end + +Given "user {string} has TOTP 2FA enabled" do |login| + user = User.find_by(login: login) || FactoryBot.create(:user, login: login) + user.generate_totp_secret_if_missing! + user.enable_totp! +end + +When "I fill in a valid TOTP two-step verification code for user {string}" do |login| + user = User.find_by(login: login) + fill_in "totp_attempt", with: user.current_otp +end + +When "I fill in a valid TOTP recovery code for user {string}" do |login| + user = User.find_by(login: login) + codes = user.generate_otp_backup_codes! + user.save! + fill_in "totp_attempt", with: codes.first + @used_totp_recovery_code = codes.first +end diff --git a/features/users/authenticate_users_with_totp.feature b/features/users/authenticate_users_with_totp.feature new file mode 100644 index 00000000000..1b6fc4975ff --- /dev/null +++ b/features/users/authenticate_users_with_totp.feature @@ -0,0 +1,170 @@ +@admin +Feature: Authenticate Users With TOTP 2FA + Scenario: Users can enable TOTP 2FA + Given the following activated users exist + | login | password | + | user | testpassword | + And I am logged in as "user" with password "testpassword" + When I follow "My Preferences" + Then I should see "Enable Two-Step Verification" + When I follow "Enable Two-Step Verification" + Then I should see "Confirm Password" + When I fill in "Password" with "testpassword" + And I press "Continue" + Then I should see "Set Up Two-Step Verification" + When I fill in a valid TOTP two-step verification code for user "user" + And I press "Enable Two-Step Verification" + Then I should see "Successfully enabled two-step verification; please make note of your backup codes." + And I should see "Finish" + When I follow "Finish" + Then I should see "Set My Preferences" + + Scenario: Users cannot enable TOTP 2FA if they provide a wrong password + Given the following activated users exist + | login | password | + | user | testpassword | + And I am logged in as "user" with password "testpassword" + When I follow "My Preferences" + Then I should see "Enable Two-Step Verification" + When I follow "Enable Two-Step Verification" + Then I should see "Confirm Password" + When I fill in "Password" with "wrongpassword" + And I press "Continue" + Then I should see "Your password was incorrect." + And I should not see "Successfully enabled two-step verification, please make note of your backup codes." + And I should not see "Finish" + + Scenario: Users cannot enable TOTP 2FA if they provide a wrong code + Given the following activated users exist + | login | password | + | user | testpassword | + And I am logged in as "user" with password "testpassword" + When I follow "My Preferences" + Then I should see "Enable Two-Step Verification" + When I follow "Enable Two-Step Verification" + Then I should see "Confirm Password" + When I fill in "Password" with "testpassword" + And I press "Continue" + Then I should see "Set Up Two-Step Verification" + When I fill in "6-digit code" with "000000" + And I press "Enable Two-Step Verification" + Then I should see "Incorrect verification code. Your code may have expired, or you may need to set up your authenticator app again." + And I should not see "Successfully enabled two-step verification, please make note of your backup codes." + And I should not see "Finish" + + Scenario: Users can disable TOTP 2FA + Given the following activated users exist + | login | password | + | user | testpassword | + And I am logged in as "user" with password "testpassword" + And user "user" has TOTP 2FA enabled + When I follow "My Preferences" + And I follow "Disable two-step verification" + Then I should see "Disable Two-Step Verification" + When I fill in "Password" with "testpassword" + And I press "Disable" + Then I should see "Successfully disabled two-step verification." + + Scenario: Admins cannot disable TOTP 2FA if they provide a wrong password + Given the following activated users exist + | login | password | + | user | testpassword | + And I am logged in as "user" with password "testpassword" + And user "user" has TOTP 2FA enabled + When I follow "My Preferences" + And I follow "Disable two-step verification" + Then I should see "Disable Two-Step Verification" + When I fill in "Password" with "wrongpassword" + And I press "Disable" + Then I should see "Your password was incorrect." + And I should not see "Successfully disabled two-step verification." + + Scenario: Users can regenerate TOTP backup codes + Given the following activated users exist + | login | password | + | user | testpassword | + And I am logged in as "user" with password "testpassword" + And user "user" has TOTP 2FA enabled + When I follow "My Preferences" + And I follow "Re-generate two-step verification backup codes" + Then I should see "Two-Step Verification Backup Codes" + When I follow "Finish" + Then I should see "Set My Preferences" + + Scenario: Users with TOTP 2FA enabled can log in after providing their code + Given the following activated users exist + | login | password | + | user | testpassword | + And user "user" has TOTP 2FA enabled + When I go to the login page + And I fill in "Username or email" with "user" + And I fill in "Password" with "testpassword" + And I press "Log In" + Then I should see "Enter the verification code from your authenticator app" + When I fill in a valid TOTP two-step verification code for user "user" + And I press "Log In" within "div#main" + Then I should see "Successfully logged in" + + Scenario: Users with TOTP 2FA enabled can log in after providing their recovery code + Given the following activated users exist + | login | password | + | user | testpassword | + And user "user" has TOTP 2FA enabled + When I go to the admin login page + And I fill in "Username or email" with "user" + And I fill in "Password" with "testpassword" + And I press "Log In" + Then I should see "Enter the verification code from your authenticator app" + When I fill in a valid TOTP recovery code for user "user" + And I press "Log In" within "div#main" + Then I should see "Successfully logged in" + + Scenario: Users with TOTP 2FA enabled cannot log in with a used recovery code + Given the following activated users exist + | login | password | + | user | testpassword | + And user "user" has TOTP 2FA enabled + When I go to the login page + And I fill in "Username or email" with "user" + And I fill in "Password" with "testpassword" + And I press "Log In" + Then I should see "Enter the verification code from your authenticator app" + When I fill in a valid TOTP recovery code for user "user" + And I press "Log In" within "div#main" + Then I should see "Successfully logged in" + When I log out + And I go to the admin login page + And I fill in "Username or email" with "user" + And I fill in "Password" with "testpassword" + And I press "Log In" + And I fill in a used TOTP recovery code + And I press "Log In" within "div#main" + Then I should see "Incorrect two-step verification code." + And I should not see "Successfully logged in" + + Scenario: Users with TOTP 2FA enabled should not be prompted for their code if they enter invalid credentials + Given the following activated users exist + | login | password | + | user | testpassword | + And user "user" has TOTP 2FA enabled + When I go to the login page + And I fill in "Username or email" with "user" + And I fill in "Password" with "wrongpassword" + And I press "Log In" + Then I should not see "Enter the verification code from your authenticator app" + And I should see "The password or username you entered doesn't match our records." + + Scenario: Users with TOTP 2FA enabled cannot log in after providing a wrong code + Given the following activated users exist + | login | password | + | user | testpassword | + And user "user" has TOTP 2FA enabled + When I go to the login page + And I fill in "Username or email" with "user" + And I fill in "Password" with "testpassword" + And I press "Log In" + Then I should see "Enter the verification code from your authenticator app" + When I fill in "6-digit verification code" with "000000" + And I press "Log In" within "div#main" + Then I should see "Incorrect two-step verification code." + And I should not see "Successfully logged in" diff --git a/spec/controllers/users/totp_controller_spec.rb b/spec/controllers/users/totp_controller_spec.rb new file mode 100644 index 00000000000..c629d83b870 --- /dev/null +++ b/spec/controllers/users/totp_controller_spec.rb @@ -0,0 +1,272 @@ +require "spec_helper" + +describe Users::TotpController do + include LoginMacros + include RedirectExpectationHelper + + describe "GET #new" do + let(:user) { create(:user) } + let(:other_user) { create(:user, otp_required_for_login: true) } + + it "denies access to guest users" do + get :new, params: { user_id: user.login } + it_redirects_to_with_error(user_path(user), "Sorry, you don't have permission to access the page you were trying to reach. Please log in.") + end + + it "denies access to other users" do + fake_login_known_user(other_user) + get :new, params: { user_id: user.login } + it_redirects_to_with_error(user_path(user), "Sorry, you don't have permission to access the page you were trying to reach.") + end + + context "when logged in as user" do + it "allows access to their own page when TOTP is disabled" do + fake_login_known_user(user) + get :new, params: { user_id: user.login } + expect(response).to have_http_status(:success) + end + + it "denies access to their own page when TOTP is enabled" do + fake_login_known_user(other_user) + get :new, params: { user_id: other_user.login } + it_redirects_to_with_error(user_preferences_path(other_user), "TOTP two-step verification is already enabled.") + end + + it "denies access to other's pages" do + fake_login_known_user(user) + get :new, params: { user_id: other_user.login } + it_redirects_to_with_error(user_path(other_user), "Sorry, you don't have permission to access the page you were trying to reach.") + end + end + end + + describe "POST #reauthenticate_create" do + let(:user) { create(:user, password: "correct_password") } + let(:other_user) { create(:user, otp_required_for_login: true) } + + it "denies access to guest users" do + post :reauthenticate_create, params: { user_id: user.login } + it_redirects_to_with_error(user_path(user), "Sorry, you don't have permission to access the page you were trying to reach. Please log in.") + end + + it "denies access to other users" do + fake_login_known_user(other_user) + post :reauthenticate_create, params: { user_id: user.login } + it_redirects_to_with_error(user_path(user), "Sorry, you don't have permission to access the page you were trying to reach.") + end + + context "when logged in as user" do + before do + user.generate_totp_secret_if_missing! + end + + it "continues to enabling TOTP form when the password is correct" do + fake_login_known_user(user) + post :reauthenticate_create, params: { user_id: user.login, password_check: "correct_password" } + expect(flash).to be_empty + expect(response).to render_template(:reauthenticate_create) + end + + it "denies access when password is wrong" do + fake_login_known_user(user) + post :reauthenticate_create, params: { user_id: user.login, password_check: "wrong_password" } + expect(user.reload.totp_enabled?).to be_falsey + expect(flash[:error]).to eq("Your password was incorrect. Please try again or, if you've forgotten your password, log out and reset your password via the link on the login form.") + end + + it "denies access when TOTP is enabled" do + fake_login_known_user(other_user) + post :reauthenticate_create, params: { user_id: other_user.login } + it_redirects_to_with_error(user_preferences_path(other_user), "TOTP two-step verification is already enabled.") + end + + it "denies access to other's pages" do + fake_login_known_user(user) + post :reauthenticate_create, params: { user_id: other_user.login } + it_redirects_to_with_error(user_path(other_user), "Sorry, you don't have permission to access the page you were trying to reach.") + end + end + end + + describe "POST #create" do + let(:user) { create(:user) } + let(:other_user) { create(:user, otp_required_for_login: true) } + + it "denies access to guest users" do + post :create, params: { user_id: user.login } + it_redirects_to_with_error(user_path(user), "Sorry, you don't have permission to access the page you were trying to reach. Please log in.") + end + + it "denies access to other users" do + fake_login + post :create, params: { user_id: user.login } + it_redirects_to_with_error(user_path(user), "Sorry, you don't have permission to access the page you were trying to reach.") + end + + context "when logged in as user" do + before do + user.generate_totp_secret_if_missing! + end + + it "allows enabling TOTP" do + fake_login_known_user(user) + post :create, params: { user_id: user.login, totp_attempt: user.current_otp } + expect(user.reload.totp_enabled?).to be_truthy + it_redirects_to_with_notice(show_backup_codes_user_totp_path, "Successfully enabled two-step verification; please make note of your backup codes.") + end + + it "denies access when TOTP code is wrong" do + fake_login_known_user(user) + post :create, params: { user_id: user.login, totp_attempt: "000000" } + expect(user.reload.totp_enabled?).to be_falsey + expect(flash[:error]).to eq("Incorrect verification code. Your code may have expired, or you may need to set up your authenticator app again.") + end + + it "denies access when TOTP is enabled" do + fake_login_known_user(other_user) + post :create, params: { user_id: other_user.login } + it_redirects_to_with_error(user_preferences_path(other_user), "TOTP two-step verification is already enabled.") + end + + it "denies access to other's pages" do + fake_login_known_user(user) + post :create, params: { user_id: other_user.login } + it_redirects_to_with_error(user_path(other_user), "Sorry, you don't have permission to access the page you were trying to reach.") + end + end + end + + describe "GET #show_backup_codes" do + let(:user) { create(:user, otp_required_for_login: true) } + let(:other_user) { create(:user) } + + it "denies access to guest users" do + get :show_backup_codes, params: { user_id: user.login } + it_redirects_to_with_error(user_path(user), "Sorry, you don't have permission to access the page you were trying to reach. Please log in.") + end + + it "denies access to other users" do + fake_login + get :show_backup_codes, params: { user_id: user.login } + it_redirects_to_with_error(user_path(user), "Sorry, you don't have permission to access the page you were trying to reach.") + end + + context "when logged in as user" do + before do + user.generate_totp_secret_if_missing! + end + + it "shows the backup codes once" do + fake_login_known_user(user) + get :show_backup_codes, params: { user_id: user.login } + expect(response).to have_http_status(:success) + end + + it "denies access to other's pages" do + fake_login_known_user(user) + get :show_backup_codes, params: { user_id: other_user.login } + it_redirects_to_with_error(user_path(other_user), "Sorry, you don't have permission to access the page you were trying to reach.") + end + + it "denies access when TOTP is disabled" do + fake_login_known_user(other_user) + get :show_backup_codes, params: { user_id: other_user.login } + it_redirects_to_with_error(new_user_totp_path, "Please enable two-step verification first.") + end + end + end + + describe "GET #confirm_disable" do + let(:user) { create(:user, otp_required_for_login: true) } + let(:other_user) { create(:user) } + + it "denies access to guest users" do + get :confirm_disable, params: { user_id: user.login } + it_redirects_to_with_error(user_path(user), "Sorry, you don't have permission to access the page you were trying to reach. Please log in.") + end + + it "denies access to other users" do + fake_login + get :confirm_disable, params: { user_id: user.login } + it_redirects_to_with_error(user_path(user), "Sorry, you don't have permission to access the page you were trying to reach.") + end + + context "when logged in as user" do + before do + user.generate_totp_secret_if_missing! + user.generate_otp_backup_codes! + user.save! + end + + it "allows access to their own page when TOTP is enabled" do + fake_login_known_user(user) + get :confirm_disable, params: { user_id: user.login } + expect(response).to have_http_status(:success) + end + + it "denies access when TOTP is disabled" do + fake_login_known_user(other_user) + get :confirm_disable, params: { user_id: other_user.login } + it_redirects_to_with_error(user_preferences_path(other_user), + "TOTP two-step verification is already disabled.") + end + + it "denies access to other's pages" do + fake_login_known_user(user) + get :confirm_disable, params: { user_id: other_user.login } + it_redirects_to_with_error(user_path(other_user), "Sorry, you don't have permission to access the page you were trying to reach.") + end + end + end + + describe "POST #disable_totp" do + let(:user) { create(:user, password: "correct_password", otp_required_for_login: true) } + let(:other_user) { create(:user) } + + it "denies access to guest users" do + post :disable, params: { user_id: user.login } + it_redirects_to_with_error(user_path(user), "Sorry, you don't have permission to access the page you were trying to reach. Please log in.") + end + + it "denies access to other users" do + fake_login + post :disable, params: { user_id: user.login } + it_redirects_to_with_error(user_path(user), "Sorry, you don't have permission to access the page you were trying to reach.") + end + + context "when logged in as user" do + before do + user.generate_totp_secret_if_missing! + user.generate_otp_backup_codes! + user.save! + end + + it "allows disabling TOTP" do + fake_login_known_user(user) + post :disable, params: { user_id: user.login, password_check: "correct_password" } + expect(user.reload.totp_enabled?).to be_falsey + it_redirects_to_with_notice(user_preferences_path(user), "Successfully disabled two-step verification.") + end + + it "denies access when password is wrong" do + fake_login_known_user(user) + post :disable, params: { user_id: user.login, password_check: "wrong_password" } + expect(user.reload.totp_enabled?).to be_truthy + expect(flash[:error]).to eq("Your password was incorrect. Please try again or, if you've forgotten your password, log out and reset your password via the link on the login form.") + end + + it "denies access when TOTP is disabled" do + fake_login_known_user(other_user) + post :disable, params: { user_id: other_user.login } + it_redirects_to_with_error(user_preferences_path(other_user), + "TOTP two-step verification is already disabled.") + end + + it "denies access to other's pages" do + fake_login_known_user(user) + post :disable, params: { user_id: other_user.login } + it_redirects_to_with_error(user_path(other_user), "Sorry, you don't have permission to access the page you were trying to reach.") + end + end + end +end From acf66e3c774465653f410b0e99762ace40f214bb Mon Sep 17 00:00:00 2001 From: EchoEkhi Date: Mon, 18 May 2026 06:11:42 +0100 Subject: [PATCH 4/6] typos --- app/controllers/users/sessions_controller.rb | 4 ++-- app/views/preferences/index.html.erb | 2 +- app/views/users/totp/new.html.erb | 2 +- app/views/users/totp/reauthenticate_create.erb | 2 +- config/locales/views/en.yml | 1 + 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb index d27ac3a40de..dae22c0b9f8 100644 --- a/app/controllers/users/sessions_controller.rb +++ b/app/controllers/users/sessions_controller.rb @@ -37,7 +37,7 @@ def destroy def authenticate_with_totp_two_factor user = self.resource = find_user - if params[:totp_attempt].present? && session[:otp_user_id] + if params.key?(:totp_attempt) && session[:otp_user_id] authenticate_user_with_otp_two_factor(user) elsif user&.valid_password?(user_params[:password]) prompt_for_otp_two_factor(user) @@ -74,7 +74,7 @@ def authenticate_user_with_otp_two_factor(user) sign_in(user, event: :authentication) # Set the user_credentials flag cookie - # because this login flow bypasses ensure_user_credentials#ensure_user_credentials + # because this login flow bypasses ApplicationController#ensure_user_credentials cookies[:user_credentials] = { value: 1, expires: 1.year.from_now } unless cookies[:user_credentials] if pwned diff --git a/app/views/preferences/index.html.erb b/app/views/preferences/index.html.erb index 84b8301b513..b66ae5dc6df 100644 --- a/app/views/preferences/index.html.erb +++ b/app/views/preferences/index.html.erb @@ -32,7 +32,7 @@
      <% if current_user.totp_enabled? %>
    • <%= link_to t(".account_security.disable_two_step_verification"), confirm_disable_user_totp_path %>
    • -
    • <%= link_to ts("Re-generate two-step verification backup codes"), show_backup_codes_user_totp_path %>
    • +
    • <%= link_to t(".account_security.regenerate_backup_codes"), show_backup_codes_user_totp_path %>
    • <% else %>
    • <%= link_to t(".account_security.enable_two_step_verification"), new_user_totp_path %>
    • <% end %> diff --git a/app/views/users/totp/new.html.erb b/app/views/users/totp/new.html.erb index 7b3f850148e..1cbd48df241 100644 --- a/app/views/users/totp/new.html.erb +++ b/app/views/users/totp/new.html.erb @@ -14,4 +14,4 @@

    -<% end %> \ No newline at end of file +<% end %> diff --git a/app/views/users/totp/reauthenticate_create.erb b/app/views/users/totp/reauthenticate_create.erb index a56905743c7..c5c64ac62b3 100644 --- a/app/views/users/totp/reauthenticate_create.erb +++ b/app/views/users/totp/reauthenticate_create.erb @@ -53,4 +53,4 @@ document.getElementById("copy-totp-setup-key").textContent = "<%= t(".app_setup.key.copied") %>"; } -<% end %> \ No newline at end of file +<% end %> diff --git a/config/locales/views/en.yml b/config/locales/views/en.yml index e684384efc7..0a75a24a4c4 100644 --- a/config/locales/views/en.yml +++ b/config/locales/views/en.yml @@ -2593,6 +2593,7 @@ en: enable_two_step_verification: Enable two-step verification heading: Account Security legend: Account Security + regenerate_backup_codes: Re-generate two-step verification backup codes browser_page_title_format: Browser page title format collections_challenges_gifts: allow_collection_invitation: Allow others to invite my works to collections. From e196565a88f995dc7d89f887acb138e9cc69429b Mon Sep 17 00:00:00 2001 From: EchoEkhi Date: Mon, 18 May 2026 06:23:06 +0100 Subject: [PATCH 5/6] Fix test --- app/views/preferences/index.html.erb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/preferences/index.html.erb b/app/views/preferences/index.html.erb index b66ae5dc6df..16a8ccfd569 100644 --- a/app/views/preferences/index.html.erb +++ b/app/views/preferences/index.html.erb @@ -13,7 +13,7 @@
  • <%= link_to t(".navigation.muted_users"), user_muted_users_path %>
  • <%= link_to t(".navigation.change_username"), change_username_user_path(@user) %>
  • - <% if current_user.totp_enabled? %> + <% if @user.totp_enabled? %> <%= link_to t(".navigation.disable_two_step_verification"), confirm_disable_user_totp_path %> <% else %> <%= link_to t(".navigation.enable_two_step_verification"), new_user_totp_path %> @@ -30,7 +30,7 @@ <%= t(".account_security.legend") %>

    <%= t(".account_security.heading") %>

      - <% if current_user.totp_enabled? %> + <% if @user.totp_enabled? %>
    • <%= link_to t(".account_security.disable_two_step_verification"), confirm_disable_user_totp_path %>
    • <%= link_to t(".account_security.regenerate_backup_codes"), show_backup_codes_user_totp_path %>
    • <% else %> From f98be92dcef87f10200f65e3d0b2f262304b1153 Mon Sep 17 00:00:00 2001 From: EchoEkhi Date: Mon, 18 May 2026 06:50:46 +0100 Subject: [PATCH 6/6] rubocop --- app/views/users/sessions/totp.html.erb | 2 +- features/users/authenticate_users_with_totp.feature | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/views/users/sessions/totp.html.erb b/app/views/users/sessions/totp.html.erb index b20334bdfa4..50e86734ee4 100644 --- a/app/views/users/sessions/totp.html.erb +++ b/app/views/users/sessions/totp.html.erb @@ -14,4 +14,4 @@

      -<% end %> \ No newline at end of file +<% end %> diff --git a/features/users/authenticate_users_with_totp.feature b/features/users/authenticate_users_with_totp.feature index 1b6fc4975ff..dc634d22414 100644 --- a/features/users/authenticate_users_with_totp.feature +++ b/features/users/authenticate_users_with_totp.feature @@ -1,4 +1,4 @@ -@admin +@users Feature: Authenticate Users With TOTP 2FA Scenario: Users can enable TOTP 2FA Given the following activated users exist @@ -65,7 +65,7 @@ Feature: Authenticate Users With TOTP 2FA And I press "Disable" Then I should see "Successfully disabled two-step verification." - Scenario: Admins cannot disable TOTP 2FA if they provide a wrong password + Scenario: Users cannot disable TOTP 2FA if they provide a wrong password Given the following activated users exist | login | password | | user | testpassword | @@ -110,7 +110,7 @@ Feature: Authenticate Users With TOTP 2FA | login | password | | user | testpassword | And user "user" has TOTP 2FA enabled - When I go to the admin login page + When I go to the login page And I fill in "Username or email" with "user" And I fill in "Password" with "testpassword" And I press "Log In" @@ -133,7 +133,7 @@ Feature: Authenticate Users With TOTP 2FA And I press "Log In" within "div#main" Then I should see "Successfully logged in" When I log out - And I go to the admin login page + And I go to the login page And I fill in "Username or email" with "user" And I fill in "Password" with "testpassword" And I press "Log In"