enable 2FA for manager

when trying to access manager, if superadmin did'nt enable otp, he/she is redirected to a page to enable 2FA. When superadmin is enabling 2FA, he has to to scan a qrcode with the 2FA application client. And afterwards, the superadmin has to log in with email, password and OTP code.
This commit is contained in:
Christophe Robillard 2020-11-04 16:35:15 +01:00
parent 3fdb045356
commit 2a0ebd062a
14 changed files with 141 additions and 20 deletions

View file

@ -64,6 +64,7 @@ gem 'rails-i18n' # Locales par défaut
gem 'rake-progressbar', require: false gem 'rake-progressbar', require: false
gem 'react-rails' gem 'react-rails'
gem 'rgeo-geojson' gem 'rgeo-geojson'
gem 'rqrcode'
gem 'sanitize-url' gem 'sanitize-url'
gem 'sassc-rails' # Use SCSS for stylesheets gem 'sassc-rails' # Use SCSS for stylesheets
gem 'sentry-raven' gem 'sentry-raven'

View file

@ -571,6 +571,10 @@ GEM
rotp (4.1.0) rotp (4.1.0)
addressable (~> 2.5) addressable (~> 2.5)
rouge (3.17.0) rouge (3.17.0)
rqrcode (1.1.2)
chunky_png (~> 1.0)
rqrcode_core (~> 0.1)
rqrcode_core (0.1.2)
rspec (3.9.0) rspec (3.9.0)
rspec-core (~> 3.9.0) rspec-core (~> 3.9.0)
rspec-expectations (~> 3.9.0) rspec-expectations (~> 3.9.0)
@ -849,6 +853,7 @@ DEPENDENCIES
rake-progressbar rake-progressbar
react-rails react-rails
rgeo-geojson rgeo-geojson
rqrcode
rspec-rails rspec-rails
rspec_junit_formatter rspec_junit_formatter
rubocop rubocop

View file

@ -1,12 +1,2 @@
class Administrations::SessionsController < ApplicationController class Administrations::SessionsController < Devise::SessionsController
def new
end
def destroy
if administration_signed_in?
sign_out :administration
end
redirect_to root_path
end
end end

View file

@ -0,0 +1,28 @@
class AdministrationsController < ApplicationController
before_action :authenticate_administration!
def edit_otp
end
def enable_otp
current_administration.enable_otp!
@qrcode = generate_qr_code
sign_out :administration
end
protected
def authenticate_administration!
if !administration_signed_in?
redirect_to root_path
end
end
private
def generate_qr_code
issuer = 'DSManager'
label = "#{issuer}:#{current_administration.email}"
RQRCode::QRCode.new(current_administration.otp_provisioning_uri(label, issuer: issuer))
end
end

View file

@ -13,6 +13,7 @@ class ApplicationController < ActionController::Base
before_action :set_raven_context before_action :set_raven_context
before_action :redirect_if_untrusted before_action :redirect_if_untrusted
before_action :reject, if: -> { feature_enabled?(:maintenance_mode) } before_action :reject, if: -> { feature_enabled?(:maintenance_mode) }
before_action :configure_permitted_parameters, if: :devise_controller?
before_action :staging_authenticate before_action :staging_authenticate
before_action :set_active_storage_host before_action :set_active_storage_host
@ -105,6 +106,10 @@ class ApplicationController < ActionController::Base
stored_location_for(:user) || super stored_location_for(:user) || super
end end
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_in, keys: [:otp_attempt])
end
private private
def set_current_roles def set_current_roles

View file

@ -13,8 +13,10 @@ module Manager
protected protected
def authenticate_administration! def authenticate_administration!
if administration_signed_in? if administration_signed_in? && current_administration.otp_required_for_login?
super super
elsif administration_signed_in?
redirect_to edit_administration_otp_path
else else
redirect_to manager_sign_in_path redirect_to manager_sign_in_path
end end

View file

@ -28,6 +28,24 @@ class Administration < ApplicationRecord
devise :rememberable, :trackable, :validatable, :lockable, :async, :recoverable, devise :rememberable, :trackable, :validatable, :lockable, :async, :recoverable,
:two_factor_authenticatable, :otp_secret_encryption_key => ENV['OTP_SECRET_KEY'] :two_factor_authenticatable, :otp_secret_encryption_key => ENV['OTP_SECRET_KEY']
def enable_otp!
self.otp_secret = Administration.generate_otp_secret
self.otp_required_for_login = true
save!
end
def disable_otp!
self.assign_attributes(
{
encrypted_otp_secret: nil,
encrypted_otp_secret_iv: nil,
encrypted_otp_secret_salt: nil,
consumed_timestep: nil,
otp_required_for_login: false
}
)
save!
end
def invite_admin(email) def invite_admin(email)
user = User.create_or_promote_to_administrateur(email, SecureRandom.hex) user = User.create_or_promote_to_administrateur(email, SecureRandom.hex)

View file

@ -0,0 +1,6 @@
.super-admin.flex.justify-center
%div
%h2.huge-title Espace Manager
%p Munissez-vous de votre téléphone sur lequel vous avez installé une application cliente 2FA (Google Authenticator, Authy, AndOTP, ...)
%br
%p= link_to "Activer l'authentification double-facteur", enable_administration_otp_path, method: :put, class: 'button primary'

View file

@ -0,0 +1,12 @@
.container
%p
%strong Si vous n'effectuez pas cette étape maintenant, vous ne pourrez plus vous connecter au manager !
%p Depuis votre téléphone, lancez votre application cliente 2FA et scannez ce QRCode afin d'ajouter votre compte DSManager. Votre application vous fournira ensuite à chaque connexion au manager le code otp à saisir.
%br
= raw @qrcode.as_svg(module_size: 6)
%br
%p
Après avoir scanné le QRCode ci-dessus, nous vous invitons à
= link_to 'accéder au Manager' , manager_root_path

View file

@ -1,3 +1,18 @@
.super-admin.flex.justify-center .super-admin.flex.justify-center
%div %div
%h2 Espace Admin %h2.huge-title Espace Manager
.auth-form.sign-in-form
= form_for Administration.new, url: administration_session_path, html: { class: "form" } do |f|
%h1 Connectez-vous
= f.label :email, "Email (nom@site.com)"
= f.text_field :email, type: :email, autocomplete: 'username', autofocus: true
= f.label :password, "Mot de passe (#{PASSWORD_MIN_LENGTH} caractères minimum)"
= f.password_field :password, autocomplete: 'current-password'
= f.label :otp_attempt, 'Code OTP (uniquement si vous avez déjà activé 2FA)'
= f.text_field :otp_attempt
= f.submit "Se connecter", class: "button large primary expand"

View file

@ -77,8 +77,10 @@ Rails.application.routes.draw do
# Authentication # Authentication
# #
devise_for :administrations, devise_for :administrations, skip: [:registrations]
skip: [:registrations]
get 'administrations/edit_otp', to: 'administrations#edit_otp', as: 'edit_administration_otp'
put 'administrations/enable_otp', to: 'administrations#enable_otp', as: 'enable_administration_otp'
devise_for :users, controllers: { devise_for :users, controllers: {
sessions: 'users/sessions', sessions: 'users/sessions',

View file

@ -7,13 +7,23 @@ describe Manager::AdministrateursController, type: :controller do
end end
describe '#show' do describe '#show' do
render_views let(:subject) { get :show, params: { id: administrateur.id } }
before do context 'with 2FA not enabled' do
get :show, params: { id: administrateur.id } let(:administration) { create(:administration, otp_required_for_login: false) }
it { expect(subject).to redirect_to(edit_administration_otp_path) }
end end
it { expect(response.body).to include(administrateur.email) } context 'with 2FA enabled' do
render_views
let(:administration) { create(:administration, otp_required_for_login: true) }
before do
subject
end
it { expect(response.body).to include(administrateur.email) }
end
end end
describe 'GET #new' do describe 'GET #new' do

View file

@ -3,6 +3,6 @@ FactoryBot.define do
factory :administration do factory :administration do
email { generate(:administration_email) } email { generate(:administration_email) }
password { 'my-s3cure-p4ssword' } password { 'my-s3cure-p4ssword' }
otp_required_for_login { false } otp_required_for_login { true }
end end
end end

View file

@ -34,4 +34,31 @@ describe Administration, type: :model do
end end
end end
end end
describe 'enable_otp!' do
let(:administration) { create(:administration, otp_required_for_login: false) }
let(:subject) { administration.enable_otp! }
it 'updates otp_required_for_login' do
expect { subject }.to change { administration.otp_required_for_login? }.from(false).to(true)
end
it 'updates otp_secret' do
expect { subject }.to change { administration.otp_secret }
end
end
describe 'disable_otp!' do
let(:administration) { create(:administration, otp_required_for_login: true) }
let(:subject) { administration.disable_otp! }
it 'updates otp_required_for_login' do
expect { subject }.to change { administration.otp_required_for_login? }.from(true).to(false)
end
it 'nullifies otp_secret' do
administration.enable_otp!
expect { subject }.to change { administration.reload.otp_secret }.to(nil)
end
end
end end