diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 545d8cab..408dd5d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -84,6 +84,9 @@ jobs: - name: devise config_path: "ci/configs/devise.yml" skips: --skip-javascript + - name: devise_mfa + config_path: "ci/configs/devise_mfa.yml" + skips: --skip-javascript - name: basic_with_skips config_path: "ci/configs/basic.yml" skips: --skip-spring --skip-javascript diff --git a/ackama_rails_template.config.yml b/ackama_rails_template.config.yml index d0ac3db9..1287be00 100644 --- a/ackama_rails_template.config.yml +++ b/ackama_rails_template.config.yml @@ -17,6 +17,7 @@ use_typescript: true # Use these flags to enable features in the rails app created by this template. apply_variant_react: false apply_variant_devise: false +apply_variant_devise_mfa: false apply_variant_sidekiq: false apply_variant_bootstrap: false diff --git a/ci/configs/all-typescript.yml b/ci/configs/all-typescript.yml index 798cd595..5a191ee1 100644 --- a/ci/configs/all-typescript.yml +++ b/ci/configs/all-typescript.yml @@ -6,6 +6,7 @@ use_typescript: true apply_variant_github_actions_ci: true apply_variant_react: true apply_variant_devise: true +apply_variant_devise_mfa: true apply_variant_sidekiq: true apply_variant_bootstrap: true apply_variant_deploy_with_capistrano: true diff --git a/ci/configs/all.yml b/ci/configs/all.yml index 54d07a23..8679fa7b 100644 --- a/ci/configs/all.yml +++ b/ci/configs/all.yml @@ -6,6 +6,7 @@ use_typescript: false apply_variant_github_actions_ci: true apply_variant_react: true apply_variant_devise: true +apply_variant_devise_mfa: true apply_variant_sidekiq: true apply_variant_bootstrap: true apply_variant_deploy_with_capistrano: true diff --git a/ci/configs/basic-typescript.yml b/ci/configs/basic-typescript.yml index 59308ade..c9ae5140 100644 --- a/ci/configs/basic-typescript.yml +++ b/ci/configs/basic-typescript.yml @@ -6,6 +6,7 @@ use_typescript: true apply_variant_github_actions_ci: false apply_variant_react: false apply_variant_devise: false +apply_variant_devise_mfa: true apply_variant_sidekiq: false apply_variant_bootstrap: false apply_variant_deploy_with_capistrano: false diff --git a/ci/configs/basic.yml b/ci/configs/basic.yml index 161725b5..c9a5f27f 100644 --- a/ci/configs/basic.yml +++ b/ci/configs/basic.yml @@ -6,6 +6,7 @@ use_typescript: false apply_variant_github_actions_ci: false apply_variant_react: false apply_variant_devise: false +apply_variant_devise_mfa: false apply_variant_sidekiq: false apply_variant_bootstrap: false apply_variant_deploy_with_capistrano: false diff --git a/ci/configs/bootstrap-typescript.yml b/ci/configs/bootstrap-typescript.yml index 5e524677..3085e730 100644 --- a/ci/configs/bootstrap-typescript.yml +++ b/ci/configs/bootstrap-typescript.yml @@ -6,6 +6,7 @@ use_typescript: true apply_variant_github_actions_ci: false apply_variant_react: false apply_variant_devise: false +apply_variant_devise_mfa: true apply_variant_sidekiq: false apply_variant_bootstrap: true apply_variant_deploy_with_capistrano: false diff --git a/ci/configs/bootstrap.yml b/ci/configs/bootstrap.yml index f3326bf2..40e70761 100644 --- a/ci/configs/bootstrap.yml +++ b/ci/configs/bootstrap.yml @@ -6,6 +6,7 @@ use_typescript: false apply_variant_github_actions_ci: false apply_variant_react: false apply_variant_devise: false +apply_variant_devise_mfa: true apply_variant_sidekiq: false apply_variant_bootstrap: true apply_variant_deploy_with_capistrano: false diff --git a/ci/configs/deploy_with_ackama_ec2_capistrano.yml b/ci/configs/deploy_with_ackama_ec2_capistrano.yml index a9fd37dc..62fbd3b3 100644 --- a/ci/configs/deploy_with_ackama_ec2_capistrano.yml +++ b/ci/configs/deploy_with_ackama_ec2_capistrano.yml @@ -5,6 +5,7 @@ git_repo_url: "" use_typescript: false apply_variant_react: false apply_variant_devise: false +apply_variant_devise_mfa: true apply_variant_github_actions_ci: false apply_variant_sidekiq: false apply_variant_typescript: false diff --git a/ci/configs/deploy_with_capistrano.yml b/ci/configs/deploy_with_capistrano.yml index efc7ac2c..d1d4137a 100644 --- a/ci/configs/deploy_with_capistrano.yml +++ b/ci/configs/deploy_with_capistrano.yml @@ -6,6 +6,7 @@ use_typescript: false apply_variant_github_actions_ci: false apply_variant_react: false apply_variant_devise: false +apply_variant_devise_mfa: true apply_variant_sidekiq: false apply_variant_bootstrap: false apply_variant_deploy_with_capistrano: true diff --git a/ci/configs/devise.yml b/ci/configs/devise.yml index 61e93148..c8343a08 100644 --- a/ci/configs/devise.yml +++ b/ci/configs/devise.yml @@ -6,6 +6,7 @@ apply_variant_github_actions_ci: false use_typescript: false apply_variant_react: false apply_variant_devise: true +apply_variant_devise_mfa: true apply_variant_sidekiq: false apply_variant_bootstrap: false apply_variant_deploy_with_capistrano: false diff --git a/ci/configs/devise_mfa.yml b/ci/configs/devise_mfa.yml new file mode 100644 index 00000000..c8343a08 --- /dev/null +++ b/ci/configs/devise_mfa.yml @@ -0,0 +1,13 @@ +--- +staging_hostname: "staging.example.com" +production_hostname: "www.example.com" +git_repo_url: "" +apply_variant_github_actions_ci: false +use_typescript: false +apply_variant_react: false +apply_variant_devise: true +apply_variant_devise_mfa: true +apply_variant_sidekiq: false +apply_variant_bootstrap: false +apply_variant_deploy_with_capistrano: false +apply_variant_deploy_with_ackama_ec2_capistrano: false diff --git a/ci/configs/github_actions.yml b/ci/configs/github_actions.yml index e5ed645b..5cfe0483 100644 --- a/ci/configs/github_actions.yml +++ b/ci/configs/github_actions.yml @@ -6,6 +6,7 @@ use_typescript: false apply_variant_github_actions_ci: true apply_variant_react: false apply_variant_devise: false +apply_variant_devise_mfa: true apply_variant_sidekiq: false apply_variant_bootstrap: false apply_variant_deploy_with_capistrano: false diff --git a/ci/configs/react-typescript.yml b/ci/configs/react-typescript.yml index bb0daa7b..80acc821 100644 --- a/ci/configs/react-typescript.yml +++ b/ci/configs/react-typescript.yml @@ -6,6 +6,7 @@ use_typescript: true apply_variant_github_actions_ci: false apply_variant_react: true apply_variant_devise: false +apply_variant_devise_mfa: true apply_variant_sidekiq: false apply_variant_bootstrap: false apply_variant_deploy_with_capistrano: false diff --git a/ci/configs/react.yml b/ci/configs/react.yml index 1dfd4f3b..1dbfac12 100644 --- a/ci/configs/react.yml +++ b/ci/configs/react.yml @@ -6,6 +6,7 @@ use_typescript: false apply_variant_github_actions_ci: false apply_variant_react: true apply_variant_devise: false +apply_variant_devise_mfa: false apply_variant_sidekiq: false apply_variant_bootstrap: false apply_variant_deploy_with_capistrano: false diff --git a/ci/configs/sidekiq.yml b/ci/configs/sidekiq.yml index 6522ac53..d43c69d5 100644 --- a/ci/configs/sidekiq.yml +++ b/ci/configs/sidekiq.yml @@ -6,6 +6,7 @@ use_typescript: false apply_variant_github_actions_ci: false apply_variant_react: false apply_variant_devise: false +apply_variant_devise_mfa: false apply_variant_sidekiq: true apply_variant_bootstrap: false apply_variant_deploy_with_capistrano: false diff --git a/template.rb b/template.rb index 2dae5df8..7efc711d 100644 --- a/template.rb +++ b/template.rb @@ -45,6 +45,10 @@ def apply_variant_devise? @yaml_config.fetch("apply_variant_devise") end + def apply_variant_devise_mfa? + @yaml_config.fetch("apply_variant_devise") + end + def apply_variant_sidekiq? @yaml_config.fetch("apply_variant_sidekiq") end @@ -180,7 +184,10 @@ def apply_template! # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Met # we deliberately place this after the initial git commit because it # contains a lot of changes and adds its own git commit - apply "variants/devise/template.rb" if TEMPLATE_CONFIG.apply_variant_devise? + if TEMPLATE_CONFIG.apply_variant_devise? + apply "variants/devise/template.rb" + apply "variants/devise-mfa/template.rb" if TEMPLATE_CONFIG.apply_variant_devise_mfa? + end # We apply code annotation **after** all the other variants which might # generate routes and models diff --git a/variants/devise-mfa/app/controllers/dashboards_controller.rb b/variants/devise-mfa/app/controllers/dashboards_controller.rb new file mode 100644 index 00000000..fc506074 --- /dev/null +++ b/variants/devise-mfa/app/controllers/dashboards_controller.rb @@ -0,0 +1,11 @@ +## +# This is an example controller which you should remove from your application. +# It demonstrates how to create a controller which requires users to be +# authenticated before running any of its actions. +# +class DashboardsController < ApplicationController + # Only authenticated users can see the dashboard + def show + authorize :dashboard + end +end diff --git a/variants/devise-mfa/app/controllers/users/devise_controller.rb b/variants/devise-mfa/app/controllers/users/devise_controller.rb new file mode 100644 index 00000000..0b481540 --- /dev/null +++ b/variants/devise-mfa/app/controllers/users/devise_controller.rb @@ -0,0 +1,14 @@ +module Users + class DeviseController < ApplicationController + # class DeviseController < Devise::DeviseController + # TODO: this doesn't really need to inherit from Application controller anymore + # Is it more or less surprising to + before_action :configure_permitted_parameters, if: :devise_controller? + + protected + + def configure_permitted_parameters + devise_parameter_sanitizer.permit(:sign_in, keys: [:otp_attempt]) + end + end +end diff --git a/variants/devise-mfa/app/controllers/users/multi_factor_authentications_controller.rb b/variants/devise-mfa/app/controllers/users/multi_factor_authentications_controller.rb new file mode 100644 index 00000000..b8f122fe --- /dev/null +++ b/variants/devise-mfa/app/controllers/users/multi_factor_authentications_controller.rb @@ -0,0 +1,86 @@ +## +# This controller allows users to manage their MFA credential(s). This +# controller is not involved in signing in or out. +# +# Conceptually each user has one MFA resource which they can +# manage because devise-two-factor only supports one mfa code per user +# +module Users + class MultiFactorAuthenticationsController < ApplicationController + before_action { authorize :mfa } + + # Add the environment name to the two-factor authentication string so that + # you can more easily tell the MFA codes for different environments + # apart in your MFA app. + # + # Keep this name short. Some TOTP management applications (e.g. Google + # Authenticator) do not let users edit this name and do not show many + # characters on screen. + # + ISSUER = if Rails.env.production? + I18n.t("application.name").freeze + else + "#{I18n.t("application.name")} #{Rails.env}".freeze + end + + # Allow the template to access issuer + helper_method :issuer + + ## + # #show is the entry point for a user managing their two-factor + # authentication. It displays a summary of the current state of their + # two-factor authentication setup and buttons/links to take actions on + # it. + # + def show + end + + ## + # #new starts the process of setting up two-factor authentication for + # the user + # + def new + @otp_secret = User.generate_otp_secret + end + + ## + # #create accepts the form submission from #new and checks that the TOTP + # code supplied by the user is valid. If the user gives us a valid TOTP code + # then we can we be confident they have correctly setup OTP so we can + # require it at next sign in. + def create + if current_user.validate_and_consume_otp!(otp_param, otp_secret: otp_secret_param) + current_user.update!(otp_secret: otp_secret_param, otp_required_for_login: true) + redirect_to users_multi_factor_authentication_path, notice: t(".success") + else + @otp_secret = otp_secret_param + flash.now[:alert] = t(".invalid_code") + render :new + end + end + + def create_backup_codes + @backup_codes = current_user.generate_otp_backup_codes! + current_user.save! + end + + def destroy_backup_codes + current_user.update!(otp_backup_codes: nil) + redirect_to users_multi_factor_authentication_path, notice: t(".success") + end + + private + + def otp_param + params.require(:otp_attempt).gsub(/\A[^\d+]\z/, "") + end + + def otp_secret_param + params.require(:otp_secret) + end + + def issuer + ISSUER + end + end +end diff --git a/variants/devise-mfa/app/controllers/users/sessions_controller.rb b/variants/devise-mfa/app/controllers/users/sessions_controller.rb new file mode 100644 index 00000000..185fe0ca --- /dev/null +++ b/variants/devise-mfa/app/controllers/users/sessions_controller.rb @@ -0,0 +1,139 @@ +module Users + ## + # This controller contains overrides the standard Devise SessionController to + # support multi-factor authentication. + # + # Each action has a comment describing it's function. Also see + # doc/multi_factor_authentication.md for a more verbose description of the + # sequence of events for MFA. + # + class SessionsController < Devise::SessionsController + RESOURCE_OTP_TOKEN_EXPIRY = 15.minutes # Must sign in within this time + RESOURCE_OTP_TOKEN_PURPOSE = "mfa_attempt".freeze # Used to scope generated signed IDs + + ## + # We do not want Warden to try and authenticate the user in middleware, + # since middleware runs before any controller actions, resulting in the user + # being signed in (AND stored, which we definitely do not want), + # before our customised create action is run. Inside our create action, we + # enable this setting again to allow Devise::Strategies::Authenticatable to + # run. + # + # NOTE: This should be fixed in devise 5.x + # https://github.com/heartcombo/devise/issues/5013 + # https://github.com/heartcombo/devise/pull/5032 + # https://github.com/heartcombo/devise/blob/6d32d2447cc0f3739d9732246b5a5bde98d9e032/lib/devise/strategies/authenticatable.rb#L103 + # + skip_before_action :allow_params_authentication!, only: :create + + ## + # We need to intercept the sign-in attempt if the user has OTP enabled, and + # present them with UI to enter their MFA code. To do this, we check if the + # user requires MFA to sign in. If it does not, we proceed as-is. If it + # does, we render a response to request their MFA code, which then submits + # to the #validate_otp action. + # + def create + # Once we are inside the controller action, we _do_ want to allow params + # authentication, otherwise Devise won't authenticate the user from params. + allow_params_authentication! + + # Tell Devise not to track the user after any call to set_user. We set + # this because we don't want to track a successful sign in until after MFA + # verification. + disable_user_tracking! + + # NOTE: store: false is IMPORTANT - we want to authenticate the user, but not + # actually log them in. + self.resource = warden.authenticate!(**auth_options, store: false) + + return after_successful_sign_in unless resource.otp_required_for_login? + + store_otp_identifier(resource_to_otp_identifier) + render :mfa_prompt, status: :forbidden + end + + ## Validates the OTP code before signing in the user. This requires a + # valid, non-expired signed ID representing the user to exist in the + # session. This is set by successfully authenticating to #create. We do this + # to avoid leaking the signed ID to the user. Even though it has a short + # expiry, it could be used in a social phishing attack, and without + # requiring it in the session, would allow for remote sign in without + # authenticating the user beforehand. + def validate_otp + self.resource = resource_from_otp_identifier(retrieve_otp_identifer) + return after_successful_sign_in if otp_validated + + flash[:alert] = t("devise.failure.invalid", authentication_keys: User.human_attribute_name(:email)) + redirect_to new_user_session_path + end + + ## + # We want to make sure that when a user logs out (i.e. destroys their + # session) then the session cookie they had cannot be used again. We + # achieve this by overriding the built-in devise implementation of + # `Devise::SessionsController#destroy` action to invalidate all existing + # user sessions and then call `super` to run the built-in devise + # implementation of the method. + # + # References + # * https://github.com/plataformatec/devise/issues/3031 + # * http://maverickblogging.com/logout-is-broken-by-default-ruby-on-rails-web-applications/ + # * https://makandracards.com/makandra/53562-devise-invalidating-all-sessions-for-a-user + # + def destroy + current_user.invalidate_all_sessions! + super + end + + private + + ## + # Validates that a given token is eiher: + # 1. A valid OTP code + # 2. A valid OTP backup code + # + def otp_validated + code = otp_params[:otp_attempt].presence + return false unless resource && code + + resource.validate_and_consume_otp!(code) || + (resource.invalidate_otp_backup_code!(code) && resource.save!) + end + + def after_successful_sign_in + set_flash_message!(:notice, :signed_in) + enable_user_tracking! + sign_in(resource, scope: resource_name, force: true) + respond_with resource, location: after_sign_in_path_for(resource) + end + + def resource_to_otp_identifier + resource.to_signed_global_id(expires_in: RESOURCE_OTP_TOKEN_EXPIRY, for: RESOURCE_OTP_TOKEN_PURPOSE) + end + + def resource_from_otp_identifier(otp_identifier) + GlobalID::Locator.locate_signed(otp_identifier, for: RESOURCE_OTP_TOKEN_PURPOSE, only: User) + end + + def store_otp_identifier(otp_identifier) + flash[:otp_identifier] = otp_identifier&.to_s + end + + def retrieve_otp_identifer + flash[:otp_identifier].presence + end + + def otp_params + params.require(resource_name).permit(:otp_attempt, :resource_identifier) + end + + def disable_user_tracking! + request.env["devise.skip_trackable"] = true + end + + def enable_user_tracking! + request.env["devise.skip_trackable"] = false + end + end +end diff --git a/variants/devise-mfa/app/frontend/images/mfa/apple-app-store-badge.svg b/variants/devise-mfa/app/frontend/images/mfa/apple-app-store-badge.svg new file mode 100644 index 00000000..471845ff --- /dev/null +++ b/variants/devise-mfa/app/frontend/images/mfa/apple-app-store-badge.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/variants/devise-mfa/app/frontend/images/mfa/google-play-badge.svg b/variants/devise-mfa/app/frontend/images/mfa/google-play-badge.svg new file mode 100644 index 00000000..4a519515 --- /dev/null +++ b/variants/devise-mfa/app/frontend/images/mfa/google-play-badge.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/variants/devise-mfa/app/policies/dashboard_policy.rb b/variants/devise-mfa/app/policies/dashboard_policy.rb new file mode 100644 index 00000000..48e443f3 --- /dev/null +++ b/variants/devise-mfa/app/policies/dashboard_policy.rb @@ -0,0 +1,5 @@ +class DashboardPolicy < ApplicationPolicy + def show? + true + end +end diff --git a/variants/devise-mfa/app/policies/mfa_policy.rb b/variants/devise-mfa/app/policies/mfa_policy.rb new file mode 100644 index 00000000..6a645b63 --- /dev/null +++ b/variants/devise-mfa/app/policies/mfa_policy.rb @@ -0,0 +1,13 @@ +class MfaPolicy < ApplicationPolicy + def show? + true + end + + def new? + true + end + + def create? + true + end +end diff --git a/variants/devise-mfa/app/views/application/_header.html.erb b/variants/devise-mfa/app/views/application/_header.html.erb new file mode 100644 index 00000000..9b9c009a --- /dev/null +++ b/variants/devise-mfa/app/views/application/_header.html.erb @@ -0,0 +1,21 @@ +<%# header and navigation menu %> + diff --git a/variants/devise-mfa/app/views/application/_mfa_help.html.erb b/variants/devise-mfa/app/views/application/_mfa_help.html.erb new file mode 100644 index 00000000..20c954ce --- /dev/null +++ b/variants/devise-mfa/app/views/application/_mfa_help.html.erb @@ -0,0 +1,20 @@ +
+ Two-factor authentication (2FA) is a way of adding security to your + account, and preventing people from accessing your account - even if + they have access to your email and password. +
++ You'll need an app on your phone or a similar device that can generate + a 6-digit "One Time Password" for you each time you sign in. We also + recommend generating backup codes, which can each be used once to + access your account if you aren't able to use an authenticator app. +
++ If you do think someone has access to your email and password, please + contact us immediately. +
++This is the signed in user dashboard. +Only signed in users can see this. +You are signed in as <%= current_user.email %> +
+ diff --git a/variants/devise-mfa/app/views/users/multi_factor_authentications/create_backup_codes.html.erb b/variants/devise-mfa/app/views/users/multi_factor_authentications/create_backup_codes.html.erb new file mode 100644 index 00000000..05fba3dc --- /dev/null +++ b/variants/devise-mfa/app/views/users/multi_factor_authentications/create_backup_codes.html.erb @@ -0,0 +1,24 @@ ++ If you lose access to your authentication app, you can use these codes instead + of the 6-digit One Time Passwords from your device. +
+ ++ Each code can only be used once, and we recommend you keep them in a safe, + secure place. +
+ +Use this page if you need to link your user to a new device or + authentication app.
+ <% else %> ++ As this is the first time you’re signing in, you’ll need to set up + two-factor authentication (2FA) before using APP_NAME_HERE. This helps keep data within this application secure. +
+ <% end %> + + <%= render "application/mfa_help" %> + ++ First, download a free app to generate One Time Passwords. + We recommend the Google Authenticator app for + <%= link_to "Apple iOS", "https://itunes.apple.com/au/app/google-authenticator/id388497605?mt=8", target: "_blank", class: "link-primary" %> + if you’re on an iPhone, or for + <%= link_to "Android", "https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en", target: "_blank", class: "link-primary" %> + if you have an Android phone. +
+ ++ Next, using this app, scan the below code. This will allow the app to generate + One Time Passwords, which you’ll need each time you sign in.
+ + <% + url = current_user.otp_provisioning_uri(current_user.email, issuer: issuer, otp_secret: @otp_secret) + qr = RQRCode::QRCode.new(url) + %> ++ If you’re using an app that does not support scanning a QR code, or if your camera doesn’t work, you can use the following URL: +
+ +<%= url %>+ +
+ Next enter the code the app shows on screen below. This will confirm that the + app has been set up correctly. +
+ + <%= form_tag(users_multi_factor_authentication_path, method: :post, class: "my-6") do %> + <%= hidden_field_tag :authenticity_token, form_authenticity_token %> + <%= hidden_field_tag :otp_secret, @otp_secret %> ++ You currently have backup codes generated. +
++ If you lose access to your authentication app you will require these to + regain access to APP_NAME_HERE. Please keep them safe and + secure. +
+ <% else %> + You do not currently have backup codes generated. + <% end %> ++ If you lose access to your authentication app, you can use these codes instead of the 6-digit One Time Passwords from your device. +
++ Each code can only be used once, and we recommend you keep them in a safe, secure place. +
+ + <%= button_to t(".create_backup_codes"), backup_codes_users_multi_factor_authentication_path, class: "btn btn-primary px-6 mt-6", data: { turbo: false } %> + + <%= button_to backup_codes_users_multi_factor_authentication_path, method: :delete, class: "btn text-primary ps-0 d-flex mt-2 align-items-center" do %> + <%= t(".destroy_backup_codes") %> + <% end if current_user.otp_backup_codes.present? %> + ++ If you need to set up a new device or authentication app, you can do that on this page. +
+ + <%= link_to t(".new_multi_factor_authentication"), new_users_multi_factor_authentication_path, class: "btn btn-outline-primary px-6" %> +Manage the device and codes you'll use to sign in to this application.
+ <%= link_to "Manage my two-factor authentication", users_multi_factor_authentication_path %> + <%= render "application/mfa_help" %> + + EO_FIELD +end + +###################################### +# Config +###################################### + +TERMINAL.puts_header "Adding otp_attempt to filtered params in logs" +append_to_file("config/initializers/filter_parameter_logging.rb") do + <<~EO_CONTENT + Rails.application.config.filter_parameters += %i[otp_attempt] + EO_CONTENT +end + +TERMINAL.puts_header "Tweaking config/initializers/devise.rb" + +gsub_file "config/initializers/devise.rb", + " # config.sign_in_after_reset_password = true", + <<-EO_CONFIG + # + # This must be set to false when using devise-two-factor - see + # https://github.com/tinfoil/devise-two-factor#disabling-automatic-login-after-password-resets + config.sign_in_after_reset_password = false + EO_CONFIG + +gsub_file "config/initializers/devise.rb", + " # config.parent_controller = 'DeviseController'", + ' config.parent_controller = "Users::DeviseController"' + +###################################### +# Images +###################################### + +TERMINAL.puts_header "Copying MFA logos" +directory "variants/devise-mfa/app/frontend/images/mfa", "app/frontend/images/mfa" + +###################################### +# Routes +###################################### + +TERMINAL.puts_header "Setting up routes.rb" +insert_into_file("config/routes.rb", before: /^end/) do + <<-EO_ROUTES + + namespace :users do + devise_scope :user do + post :validate_otp, to: "sessions#validate_otp" + end + + resource :multi_factor_authentication, only: %i[new show create] do + post :backup_codes, action: :create_backup_codes + delete :backup_codes, action: :destroy_backup_codes + end + end + + resource :dashboards, only: [:show] + EO_ROUTES +end + +###################################### +# Pundit +###################################### + +copy_file "variants/devise-mfa/app/policies/dashboard_policy.rb", "app/policies/dashboard_policy.rb" + +###################################### +# Locales +###################################### + +TERMINAL.puts_header "Setting up locales" +copy_file "variants/devise-mfa/config/locales/mfa.en.yml", "config/locales/mfa.en.yml" + +###################################### +# Documentation +###################################### + +TERMINAL.puts_header "Copying MFA docs" +copy_file "variants/devise-mfa/doc/multi_factor_authentication_sequence.png", "doc/multi_factor_authentication_sequence.png" +copy_file "variants/devise-mfa/doc/multi_factor_authentication.md", "doc/multi_factor_authentication.md" +remove_file "doc/.keep" + +###################################### +# Clean up +###################################### + +TERMINAL.puts_header "Running rubocop to clean up generated files" +run "bundle exec rubocop -A" + +TERMINAL.puts_header "Commiting changes to git" +git add: "-A ." +git commit: "-n -m 'Install and configure devise with MFA enabled'"