11 changed files with 156 additions and 2 deletions
@ -1,3 +1,5 @@
|
||||
DB_USERNAME=postgres-user |
||||
DB_PASSWORD=postgres-password |
||||
GOVUK_NOTIFY_API_KEY=<notify-key-here-if-testing-emails> |
||||
|
||||
GOVUK_NOTIFY_API_KEY=<notify-key-here-if-testing-emails-or-admin-users> |
||||
OTP_SECRET_ENCRYPTION_KEY="<Generate this using bundle exec rake secret>" |
||||
|
@ -1,5 +1,16 @@
|
||||
class AdminUser < ApplicationRecord |
||||
# Include default devise modules. Others available are: |
||||
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable |
||||
devise :database_authenticatable, :recoverable, :rememberable, :validatable |
||||
devise :two_factor_authenticatable, :database_authenticatable, :recoverable, |
||||
:rememberable, :validatable |
||||
|
||||
has_one_time_password(encrypted: true) |
||||
|
||||
MFA_SMS_TEMPLATE_ID = "bf309d93-804e-4f95-b1f4-bd513c48ecb0".freeze |
||||
|
||||
def send_two_factor_authentication_code(code) |
||||
template_id = MFA_SMS_TEMPLATE_ID |
||||
personalisation = { otp: code } |
||||
Sms.send(phone, template_id, personalisation) |
||||
end |
||||
end |
||||
|
@ -0,0 +1,15 @@
|
||||
require "notifications/client" |
||||
|
||||
class Sms |
||||
def self.notify_client |
||||
Notifications::Client.new(ENV["GOVUK_NOTIFY_API_KEY"]) |
||||
end |
||||
|
||||
def self.send(phone_number, template_id, args) |
||||
notify_client.send_sms( |
||||
phone_number: phone_number, |
||||
template_id: template_id, |
||||
personalisation: args, |
||||
) |
||||
end |
||||
end |
@ -0,0 +1,26 @@
|
||||
<% content_for :title, "Check your phone" %> |
||||
|
||||
<%= form_with(url: "/admin/two_factor_authentication", html: { method: :put }) do |f| %> |
||||
<div class="govuk-grid-row"> |
||||
<div class="govuk-grid-column-two-thirds"> |
||||
|
||||
<h1 class="govuk-heading-l"> |
||||
<%= content_for(:title) %> |
||||
</h1> |
||||
|
||||
<p class="govuk-body">We’ve sent you a text message with a security code.</p> |
||||
|
||||
<%= f.govuk_number_field :code, |
||||
label: { text: "Security code" }, |
||||
width: 5, |
||||
autofocus: true |
||||
%> |
||||
|
||||
<%= f.govuk_submit "Submit" %> |
||||
</div> |
||||
</div> |
||||
<% end %> |
||||
|
||||
<p class="govuk-body"> |
||||
<%= govuk_link_to "Not received a text message?", "#" %> |
||||
</p> |
@ -0,0 +1,16 @@
|
||||
class TwoFactorAuthenticationAddToAdminUsers < ActiveRecord::Migration[6.1] |
||||
def change |
||||
change_table :admin_users, bulk: true do |t| |
||||
t.column :second_factor_attempts_count, :integer, default: 0 |
||||
t.column :encrypted_otp_secret_key, :string |
||||
t.column :encrypted_otp_secret_key_iv, :string |
||||
t.column :encrypted_otp_secret_key_salt, :string |
||||
t.column :direct_otp, :string |
||||
t.column :direct_otp_sent_at, :datetime |
||||
t.column :totp_timestamp, :timestamp |
||||
t.column :phone, :string |
||||
|
||||
t.index :encrypted_otp_secret_key, unique: true |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,45 @@
|
||||
require "rails_helper" |
||||
|
||||
RSpec.describe "Admin Panel" do |
||||
let!(:admin) { FactoryBot.create(:admin_user) } |
||||
let(:notify_client) { instance_double(Notifications::Client) } |
||||
let(:mfa_template_id) { AdminUser::MFA_SMS_TEMPLATE_ID } |
||||
let(:otp) { "999111" } |
||||
|
||||
before do |
||||
allow(Sms).to receive(:notify_client).and_return(notify_client) |
||||
allow(notify_client).to receive(:send_sms).and_return(true) |
||||
end |
||||
|
||||
context "with a valid 2FA code" do |
||||
before do |
||||
allow(SecureRandom).to receive(:random_number).and_return(otp) |
||||
end |
||||
|
||||
it "authenticates successfully" do |
||||
expect(notify_client).to receive(:send_sms).with( |
||||
hash_including(phone_number: admin.phone, template_id: mfa_template_id), |
||||
) |
||||
visit("/admin") |
||||
fill_in("admin_user[email]", with: admin.email) |
||||
fill_in("admin_user[password]", with: admin.password) |
||||
click_button("Login") |
||||
fill_in("code", with: otp) |
||||
click_button("Submit") |
||||
expect(page).to have_content("Dashboard") |
||||
expect(page).to have_content("Two factor authentication successful.") |
||||
end |
||||
end |
||||
|
||||
context "with an invalid 2FA code" do |
||||
it "does not authenticate successfully" do |
||||
visit("/admin") |
||||
fill_in("admin_user[email]", with: admin.email) |
||||
fill_in("admin_user[password]", with: admin.password) |
||||
click_button("Login") |
||||
fill_in("code", with: otp) |
||||
click_button("Submit") |
||||
expect(page).to have_content("Check your phone") |
||||
end |
||||
end |
||||
end |
Loading…
Reference in new issue