From 2a0ebd062af7275bec553117a366c4cd99c35f18 Mon Sep 17 00:00:00 2001 From: Christophe Robillard Date: Wed, 4 Nov 2020 16:35:15 +0100 Subject: [PATCH] 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. --- Gemfile | 1 + Gemfile.lock | 5 ++++ .../administrations/sessions_controller.rb | 12 +------- app/controllers/administrations_controller.rb | 28 +++++++++++++++++++ app/controllers/application_controller.rb | 5 ++++ .../manager/application_controller.rb | 4 ++- app/models/administration.rb | 18 ++++++++++++ app/views/administrations/edit_otp.html.haml | 6 ++++ .../administrations/enable_otp.html.haml | 12 ++++++++ .../administrations/sessions/new.html.haml | 17 ++++++++++- config/routes.rb | 6 ++-- .../administrateurs_controller_spec.rb | 18 +++++++++--- spec/factories/administration.rb | 2 +- spec/models/administration_spec.rb | 27 ++++++++++++++++++ 14 files changed, 141 insertions(+), 20 deletions(-) create mode 100644 app/controllers/administrations_controller.rb create mode 100644 app/views/administrations/edit_otp.html.haml create mode 100644 app/views/administrations/enable_otp.html.haml diff --git a/Gemfile b/Gemfile index 1bf9c8101..8285c33c4 100644 --- a/Gemfile +++ b/Gemfile @@ -64,6 +64,7 @@ gem 'rails-i18n' # Locales par défaut gem 'rake-progressbar', require: false gem 'react-rails' gem 'rgeo-geojson' +gem 'rqrcode' gem 'sanitize-url' gem 'sassc-rails' # Use SCSS for stylesheets gem 'sentry-raven' diff --git a/Gemfile.lock b/Gemfile.lock index 871dca500..b9688a6b2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -571,6 +571,10 @@ GEM rotp (4.1.0) addressable (~> 2.5) 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-core (~> 3.9.0) rspec-expectations (~> 3.9.0) @@ -849,6 +853,7 @@ DEPENDENCIES rake-progressbar react-rails rgeo-geojson + rqrcode rspec-rails rspec_junit_formatter rubocop diff --git a/app/controllers/administrations/sessions_controller.rb b/app/controllers/administrations/sessions_controller.rb index 90ed081b1..0f08b1db4 100644 --- a/app/controllers/administrations/sessions_controller.rb +++ b/app/controllers/administrations/sessions_controller.rb @@ -1,12 +1,2 @@ -class Administrations::SessionsController < ApplicationController - def new - end - - def destroy - if administration_signed_in? - sign_out :administration - end - - redirect_to root_path - end +class Administrations::SessionsController < Devise::SessionsController end diff --git a/app/controllers/administrations_controller.rb b/app/controllers/administrations_controller.rb new file mode 100644 index 000000000..ab61abf2c --- /dev/null +++ b/app/controllers/administrations_controller.rb @@ -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 diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 4c795d27c..795cd500c 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -13,6 +13,7 @@ class ApplicationController < ActionController::Base before_action :set_raven_context before_action :redirect_if_untrusted before_action :reject, if: -> { feature_enabled?(:maintenance_mode) } + before_action :configure_permitted_parameters, if: :devise_controller? before_action :staging_authenticate before_action :set_active_storage_host @@ -105,6 +106,10 @@ class ApplicationController < ActionController::Base stored_location_for(:user) || super end + def configure_permitted_parameters + devise_parameter_sanitizer.permit(:sign_in, keys: [:otp_attempt]) + end + private def set_current_roles diff --git a/app/controllers/manager/application_controller.rb b/app/controllers/manager/application_controller.rb index 8f41d7a2e..4d8153b15 100644 --- a/app/controllers/manager/application_controller.rb +++ b/app/controllers/manager/application_controller.rb @@ -13,8 +13,10 @@ module Manager protected def authenticate_administration! - if administration_signed_in? + if administration_signed_in? && current_administration.otp_required_for_login? super + elsif administration_signed_in? + redirect_to edit_administration_otp_path else redirect_to manager_sign_in_path end diff --git a/app/models/administration.rb b/app/models/administration.rb index d90d9b7a2..d1610e8df 100644 --- a/app/models/administration.rb +++ b/app/models/administration.rb @@ -28,6 +28,24 @@ class Administration < ApplicationRecord devise :rememberable, :trackable, :validatable, :lockable, :async, :recoverable, :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) user = User.create_or_promote_to_administrateur(email, SecureRandom.hex) diff --git a/app/views/administrations/edit_otp.html.haml b/app/views/administrations/edit_otp.html.haml new file mode 100644 index 000000000..8f6f088fd --- /dev/null +++ b/app/views/administrations/edit_otp.html.haml @@ -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' diff --git a/app/views/administrations/enable_otp.html.haml b/app/views/administrations/enable_otp.html.haml new file mode 100644 index 000000000..fb7dab1af --- /dev/null +++ b/app/views/administrations/enable_otp.html.haml @@ -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 diff --git a/app/views/administrations/sessions/new.html.haml b/app/views/administrations/sessions/new.html.haml index de5a7a4c9..f08597e61 100644 --- a/app/views/administrations/sessions/new.html.haml +++ b/app/views/administrations/sessions/new.html.haml @@ -1,3 +1,18 @@ .super-admin.flex.justify-center %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" diff --git a/config/routes.rb b/config/routes.rb index 53cf86820..8acc97dda 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -77,8 +77,10 @@ Rails.application.routes.draw do # Authentication # - devise_for :administrations, - skip: [:registrations] + devise_for :administrations, 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: { sessions: 'users/sessions', diff --git a/spec/controllers/manager/administrateurs_controller_spec.rb b/spec/controllers/manager/administrateurs_controller_spec.rb index 18ae8a852..f7d7a36be 100644 --- a/spec/controllers/manager/administrateurs_controller_spec.rb +++ b/spec/controllers/manager/administrateurs_controller_spec.rb @@ -7,13 +7,23 @@ describe Manager::AdministrateursController, type: :controller do end describe '#show' do - render_views + let(:subject) { get :show, params: { id: administrateur.id } } - before do - get :show, params: { id: administrateur.id } + context 'with 2FA not enabled' do + let(:administration) { create(:administration, otp_required_for_login: false) } + it { expect(subject).to redirect_to(edit_administration_otp_path) } 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 describe 'GET #new' do diff --git a/spec/factories/administration.rb b/spec/factories/administration.rb index fbe2ef17c..68fc430fb 100644 --- a/spec/factories/administration.rb +++ b/spec/factories/administration.rb @@ -3,6 +3,6 @@ FactoryBot.define do factory :administration do email { generate(:administration_email) } password { 'my-s3cure-p4ssword' } - otp_required_for_login { false } + otp_required_for_login { true } end end diff --git a/spec/models/administration_spec.rb b/spec/models/administration_spec.rb index a2a5e8e29..56d6998c0 100644 --- a/spec/models/administration_spec.rb +++ b/spec/models/administration_spec.rb @@ -34,4 +34,31 @@ describe Administration, type: :model do 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