diff --git a/app/controllers/auth/passwords_controller.rb b/app/controllers/auth/passwords_controller.rb index df00f1767..3ce603ade 100644 --- a/app/controllers/auth/passwords_controller.rb +++ b/app/controllers/auth/passwords_controller.rb @@ -73,7 +73,7 @@ protected if Devise.sign_in_after_reset_password if resource.need_two_factor_authentication?(request) resource.send_new_otp - admin_user_two_factor_authentication_path + send("#{resource_name}_two_factor_authentication_path") else after_sign_in_path_for(resource) end diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb index a2284e48b..ad65de5e0 100644 --- a/app/controllers/auth/sessions_controller.rb +++ b/app/controllers/auth/sessions_controller.rb @@ -30,7 +30,7 @@ private def after_sign_in_path_for(resource) if resource.need_two_factor_authentication?(request) - admin_user_two_factor_authentication_path + send("#{resource_name}_two_factor_authentication_path") else params.dig(resource_class_name, "start").present? ? case_logs_path : super end diff --git a/app/views/devise/two_factor_authentication/resend.html.erb b/app/views/devise/two_factor_authentication/resend.html.erb index 38f1ec222..c8bb5eec5 100644 --- a/app/views/devise/two_factor_authentication/resend.html.erb +++ b/app/views/devise/two_factor_authentication/resend.html.erb @@ -7,7 +7,7 @@ ) %> <% end %> -<%= form_with(url: resend_code_admin_user_two_factor_authentication_path, html: { method: :get }) do |f| %> +<%= form_with(url: send("resend_code_#{resource_name}_two_factor_authentication_path"), html: { method: :get }) do |f| %>
@@ -15,7 +15,7 @@ <%= content_for(:title) %> -

Text messages sometimes take a few minutes to arrive. If you do not receive the text message, you can request a new one.

+

Emails sometimes take a few minutes to arrive. If you do not receive the email, you can request a new one.

<%= f.govuk_submit "Resend security code" %>
diff --git a/app/views/devise/two_factor_authentication/show.html.erb b/app/views/devise/two_factor_authentication/show.html.erb index 8b2886f2f..5177f012e 100644 --- a/app/views/devise/two_factor_authentication/show.html.erb +++ b/app/views/devise/two_factor_authentication/show.html.erb @@ -1,6 +1,7 @@ <% content_for :title, "Check your email" %> -<%= form_with(model: resource, url: "/admin/two-factor-authentication", html: { method: :put }) do |f| %> +<% url_prefix = resource_name == :user ? "account" : "admin" %> +<%= form_with(model: resource, url: "/#{url_prefix}/two-factor-authentication", html: { method: :put }) do |f| %>
<%= f.govuk_error_summary %> @@ -24,5 +25,5 @@ <% end %>

- <%= govuk_link_to "Not received an email?", admin_two_factor_authentication_resend_path %> + <%= govuk_link_to "Not received an email?", send("#{resource_name}_two_factor_authentication_resend_path") %>

diff --git a/config/routes.rb b/config/routes.rb index ac126d996..4cb364878 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -20,7 +20,7 @@ Rails.application.routes.draw do } devise_scope :admin_user do - get "admin/two-factor-authentication/resend", to: "auth/two_factor_authentication#show_resend" + get "admin/two-factor-authentication/resend", to: "auth/two_factor_authentication#show_resend", as: "admin_user_two_factor_authentication_resend" end devise_for :users, { @@ -41,7 +41,7 @@ Rails.application.routes.draw do devise_scope :user do get "account/password/reset-confirmation", to: "auth/passwords#reset_confirmation" - get "account/two-factor-authentication/resend", to: "auth/two_factor_authentication#show_resend" + get "account/two-factor-authentication/resend", to: "auth/two_factor_authentication#show_resend", as: "user_two_factor_authentication_resend" put "account", to: "users#update" end diff --git a/spec/features/user_spec.rb b/spec/features/user_spec.rb index 3d20f51c0..a79c3755c 100644 --- a/spec/features/user_spec.rb +++ b/spec/features/user_spec.rb @@ -326,21 +326,24 @@ RSpec.describe "User Features" do end context "when the user is a customer support person" do - context "when they are logging in" do - let(:support_user) { FactoryBot.create(:user, :support, last_sign_in_at: Time.zone.now) } - let(:devise_notify_mailer) { DeviseNotifyMailer.new } - let(:notify_client) { instance_double(Notifications::Client) } - let(:mfa_template_id) { User::MFA_TEMPLATE_ID } - let(:otp) { "999111" } + let(:support_user) { FactoryBot.create(:user, :support, last_sign_in_at: Time.zone.now) } + let(:devise_notify_mailer) { DeviseNotifyMailer.new } + let(:notify_client) { instance_double(Notifications::Client) } + let(:mfa_template_id) { User::MFA_TEMPLATE_ID } + let(:otp) { "999111" } + + before do + allow(DeviseNotifyMailer).to receive(:new).and_return(devise_notify_mailer) + allow(devise_notify_mailer).to receive(:notify_client).and_return(notify_client) + allow(notify_client).to receive(:send_email).and_return(true) + visit("/logs") + fill_in("user[email]", with: support_user.email) + fill_in("user[password]", with: "pAssword1") + end + context "when they are logging in" do before do - allow(DeviseNotifyMailer).to receive(:new).and_return(devise_notify_mailer) - allow(devise_notify_mailer).to receive(:notify_client).and_return(notify_client) - allow(notify_client).to receive(:send_email).and_return(true) allow(SecureRandom).to receive(:random_number).and_return(otp) - visit("/logs") - fill_in("user[email]", with: support_user.email) - fill_in("user[password]", with: "pAssword1") end it "shows the 2FA code screen" do @@ -360,5 +363,129 @@ RSpec.describe "User Features" do click_button("Sign in") end end + + context "with a valid 2FA code" do + before do + allow(SecureRandom).to receive(:random_number).and_return(otp) + end + + it "authenticates successfully" do + click_button("Sign in") + fill_in("code", with: otp) + click_button("Submit") + expect(page).to have_content("Logs") + expect(page).to have_content("Two factor authentication successful.") + end + + context "but it is more than 15 minutes old" do + it "does not authenticate successfully" do + click_button("Sign in") + support_user.update!(direct_otp_sent_at: 16.minutes.ago) + fill_in("code", with: otp) + click_button("Submit") + expect(page).to have_content("Check your email") + expect(page).to have_http_status(:unprocessable_entity) + expect(page).to have_title("Error") + expect(page).to have_selector("#error-summary-title") + end + end + end + + context "with an invalid 2FA code" do + it "does not authenticate successfully" do + click_button("Sign in") + fill_in("code", with: otp) + click_button("Submit") + expect(page).to have_content("Check your email") + expect(page).to have_http_status(:unprocessable_entity) + expect(page).to have_title("Error") + expect(page).to have_selector("#error-summary-title") + end + end + + context "when the 2FA code needs to be resent" do + before do + click_button("Sign in") + end + + it "displays the resend view" do + click_link("Not received an email?") + expect(page).to have_button("Resend security code") + end + + it "send a new OTP code and redirects back to the 2FA view" do + click_link("Not received an email?") + expect { click_button("Resend security code") }.to(change { support_user.reload.direct_otp }) + expect(page).to have_current_path("/account/two-factor-authentication") + end + end + + context "when signing in and out again" do + before do + allow(SecureRandom).to receive(:random_number).and_return(otp) + end + + it "requires the 2FA code on each login" do + click_button("Sign in") + fill_in("code", with: otp) + click_button("Submit") + click_link("Sign out") + visit("/logs") + fill_in("user[email]", with: support_user.email) + fill_in("user[password]", with: "pAssword1") + click_button("Sign in") + expect(page).to have_content("Check your email") + end + end + + context "when they have forgotten their password" do + let(:reset_password_token) { "MCDH5y6Km-U7CFPgAMVS" } + + before do + allow(Devise.token_generator).to receive(:generate).and_return(reset_password_token) + allow(DeviseNotifyMailer).to receive(:new).and_return(devise_notify_mailer) + allow(devise_notify_mailer).to receive(:notify_client).and_return(notify_client) + allow(notify_client).to receive(:send_email).and_return(true) + end + + it " is redirected to the reset password page when they click the reset password link" do + visit("/account/sign-in") + click_link("reset your password") + expect(page).to have_current_path("/account/password/new") + end + + it " is shown an error message if they submit without entering an email address" do + visit("/account/password/new") + click_button("Send email") + expect(page).to have_selector("#error-summary-title") + expect(page).to have_selector("#user-email-field-error") + expect(page).to have_title("Error") + end + + it " is redirected to login page after reset email is sent" do + visit("/account/password/new") + fill_in("user[email]", with: support_user.email) + click_button("Send email") + expect(page).to have_content("Check your email") + end + + it " is sent a reset password email via Notify" do + expect(notify_client).to receive(:send_email).with( + { + email_address: support_user.email, + template_id: support_user.reset_password_notify_template, + personalisation: { + name: support_user.name, + email: support_user.email, + organisation: support_user.organisation.name, + link: "http://localhost:3000/account/password/edit?reset_password_token=#{reset_password_token}", + }, + }, + ) + visit("/account/password/new") + fill_in("user[email]", with: support_user.email) + click_button("Send email") + end + end end end diff --git a/spec/requests/auth/passwords_controller_spec.rb b/spec/requests/auth/passwords_controller_spec.rb index e10e9f2e3..1ba617598 100644 --- a/spec/requests/auth/passwords_controller_spec.rb +++ b/spec/requests/auth/passwords_controller_spec.rb @@ -144,4 +144,76 @@ RSpec.describe Auth::PasswordsController, type: :request do end end end + + context "when a customer support user" do + let(:support_user) { FactoryBot.create(:user, :support) } + + describe "reset password" do + let(:new_value) { "new-password" } + + before do + allow(DeviseNotifyMailer).to receive(:notify_client).and_return(notify_client) + allow(notify_client).to receive(:send_email).and_return(true) + end + + it "renders the user edit password view" do + _raw, enc = Devise.token_generator.generate(User, :reset_password_token) + get "/account/password/edit?reset_password_token=#{enc}" + expect(page).to have_css("h1", text: "Reset your password") + end + + context "when passwords entered don't match" do + let(:raw) { support_user.send_reset_password_instructions } + let(:params) do + { + id: support_user.id, + user: { + password: new_value, + password_confirmation: "something_else", + reset_password_token: raw, + }, + } + end + + it "shows an error" do + put "/account/password", headers: headers, params: params + expect(response).to have_http_status(:unprocessable_entity) + expect(page).to have_content("doesn't match Password") + end + end + + context "when passwords is reset" do + let(:raw) { support_user.send_reset_password_instructions } + let(:params) do + { + id: support_user.id, + user: { + password: new_value, + password_confirmation: new_value, + reset_password_token: raw, + }, + } + end + + it "updates the password" do + expect { + put "/account/password", headers: headers, params: params + support_user.reload + }.to change(support_user, :encrypted_password) + end + + it "sends you to the 2FA page and does not allow bypassing 2FA code" do + put "/account/password", headers: headers, params: params + expect(response).to redirect_to("/account/two-factor-authentication") + get "/logs", headers: headers + expect(response).to redirect_to("/account/two-factor-authentication") + end + + it "triggers an email" do + expect(notify_client).to receive(:send_email) + put "/account/password", headers: headers, params: params + end + end + end + end end