11 changed files with 156 additions and 2 deletions
@ -1,3 +1,5 @@ |
|||||||
DB_USERNAME=postgres-user |
DB_USERNAME=postgres-user |
||||||
DB_PASSWORD=postgres-password |
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 |
class AdminUser < ApplicationRecord |
||||||
# Include default devise modules. Others available are: |
# Include default devise modules. Others available are: |
||||||
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable |
# :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 |
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