From 0d8d2de5a68786030c5d5bd02a2911e61bcbe06b Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Tue, 30 Oct 2018 18:24:29 +0100 Subject: [PATCH] Session: add trusted_device cookie --- .../administrateurs/activate_controller.rb | 7 +++- .../gestionnaires/activate_controller.rb | 7 +++- app/controllers/users/sessions_controller.rb | 21 +++++++--- app/models/concerns/trusted_device_concern.rb | 25 +++++++++++ .../activate_controller_spec.rb | 20 +++++++++ .../gestionnaires/activate_controller_spec.rb | 20 +++++++++ .../users/sessions_controller_spec.rb | 41 +++++++++++++++++++ 7 files changed, 133 insertions(+), 8 deletions(-) create mode 100644 app/models/concerns/trusted_device_concern.rb create mode 100644 spec/controllers/administrateur/activate_controller_spec.rb create mode 100644 spec/controllers/gestionnaires/activate_controller_spec.rb diff --git a/app/controllers/administrateurs/activate_controller.rb b/app/controllers/administrateurs/activate_controller.rb index d354c0375..5d2654587 100644 --- a/app/controllers/administrateurs/activate_controller.rb +++ b/app/controllers/administrateurs/activate_controller.rb @@ -1,12 +1,17 @@ require 'zxcvbn' class Administrateurs::ActivateController < ApplicationController + include TrustedDeviceConcern + layout "new_application" def new @administrateur = Administrateur.find_inactive_by_token(params[:token]) - if !@administrateur + if @administrateur + # the administrateur activates its account from an email + trust_device + else flash.alert = "Le lien de validation d'administrateur a expiré, #{helpers.contact_link('contactez-nous', tags: 'lien expiré')} pour obtenir un nouveau lien." redirect_to root_path end diff --git a/app/controllers/gestionnaires/activate_controller.rb b/app/controllers/gestionnaires/activate_controller.rb index 873757f47..12fdae36a 100644 --- a/app/controllers/gestionnaires/activate_controller.rb +++ b/app/controllers/gestionnaires/activate_controller.rb @@ -1,10 +1,15 @@ class Gestionnaires::ActivateController < ApplicationController + include TrustedDeviceConcern + layout "new_application" def new @gestionnaire = Gestionnaire.with_reset_password_token(params[:token]) - if !@gestionnaire + if @gestionnaire + # the gestionnaire activates its account from an email + trust_device + else flash.alert = "Le lien de validation du compte instructeur a expiré, #{helpers.contact_link('contactez-nous', tags: 'lien expiré')} pour obtenir un nouveau lien." redirect_to root_path end diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb index 000d6a7d8..9db5a4310 100644 --- a/app/controllers/users/sessions_controller.rb +++ b/app/controllers/users/sessions_controller.rb @@ -1,4 +1,7 @@ class Users::SessionsController < Sessions::SessionsController + include TrustedDeviceConcern + include ActionView::Helpers::DateHelper + layout "new_application" # GET /resource/sign_in @@ -24,14 +27,17 @@ class Users::SessionsController < Sessions::SessionsController end if gestionnaire_signed_in? - gestionnaire = current_gestionnaire + if trusted_device? + redirect_to gestionnaire_procedures_path + else + gestionnaire = current_gestionnaire + login_token = gestionnaire.login_token! + GestionnaireMailer.send_login_token(gestionnaire, login_token).deliver_later - login_token = gestionnaire.login_token! - GestionnaireMailer.send_login_token(gestionnaire, login_token).deliver_later + [:user, :gestionnaire, :administrateur].each { |role| sign_out(role) } - [:user, :gestionnaire, :administrateur].each { |role| sign_out(role) } - - redirect_to link_sent_path(email: gestionnaire.email) + redirect_to link_sent_path(email: gestionnaire.email) + end elsif user_signed_in? redirect_to after_sign_in_path_for(:user) else @@ -79,6 +85,9 @@ class Users::SessionsController < Sessions::SessionsController def sign_in_by_link gestionnaire = Gestionnaire.find(params[:id]) if gestionnaire&.login_token_valid?(params[:jeton]) + trust_device + flash.notice = "Merci d’avoir confirmé votre connexion. Votre navigateur est maintenant authentifié pour #{TRUSTED_DEVICE_PERIOD.to_i / ActiveSupport::Duration::SECONDS_PER_DAY} jours." + user = User.find_by(email: gestionnaire.email) administrateur = Administrateur.find_by(email: gestionnaire.email) [user, gestionnaire, administrateur].compact.each { |resource| sign_in(resource) } diff --git a/app/models/concerns/trusted_device_concern.rb b/app/models/concerns/trusted_device_concern.rb new file mode 100644 index 000000000..5aff22882 --- /dev/null +++ b/app/models/concerns/trusted_device_concern.rb @@ -0,0 +1,25 @@ +module TrustedDeviceConcern + extend ActiveSupport::Concern + + TRUSTED_DEVICE_COOKIE_NAME = :trusted_device + TRUSTED_DEVICE_PERIOD = 1.month + + def trust_device + cookies.encrypted[TRUSTED_DEVICE_COOKIE_NAME] = { + value: JSON.generate({ created_at: Time.zone.now }), + expires: TRUSTED_DEVICE_PERIOD, + httponly: true + } + end + + def trusted_device? + trusted_device_cookie.present? && + Time.zone.now - TRUSTED_DEVICE_PERIOD < JSON.parse(trusted_device_cookie)['created_at'] + end + + private + + def trusted_device_cookie + cookies.encrypted[TRUSTED_DEVICE_COOKIE_NAME] + end +end diff --git a/spec/controllers/administrateur/activate_controller_spec.rb b/spec/controllers/administrateur/activate_controller_spec.rb new file mode 100644 index 000000000..a6e99f851 --- /dev/null +++ b/spec/controllers/administrateur/activate_controller_spec.rb @@ -0,0 +1,20 @@ +describe Administrateurs::ActivateController, type: :controller do + describe '#new' do + let(:admin) { create(:administrateur) } + let(:token) { admin.send(:set_reset_password_token) } + + before { allow(controller).to receive(:trust_device) } + + context 'when the token is ok' do + before { get :new, params: { token: token } } + + it { expect(controller).to have_received(:trust_device) } + end + + context 'when the token is bad' do + before { get :new, params: { token: 'bad' } } + + it { expect(controller).not_to have_received(:trust_device) } + end + end +end diff --git a/spec/controllers/gestionnaires/activate_controller_spec.rb b/spec/controllers/gestionnaires/activate_controller_spec.rb new file mode 100644 index 000000000..fa7ce7857 --- /dev/null +++ b/spec/controllers/gestionnaires/activate_controller_spec.rb @@ -0,0 +1,20 @@ +describe Gestionnaires::ActivateController, type: :controller do + describe '#new' do + let(:gestionnaire) { create(:gestionnaire) } + let(:token) { gestionnaire.send(:set_reset_password_token) } + + before { allow(controller).to receive(:trust_device) } + + context 'when the token is ok' do + before { get :new, params: { token: token } } + + it { expect(controller).to have_received(:trust_device) } + end + + context 'when the token is bad' do + before { get :new, params: { token: 'bad' } } + + it { expect(controller).not_to have_received(:trust_device) } + end + end +end diff --git a/spec/controllers/users/sessions_controller_spec.rb b/spec/controllers/users/sessions_controller_spec.rb index 0e8eb02ea..620bc7968 100644 --- a/spec/controllers/users/sessions_controller_spec.rb +++ b/spec/controllers/users/sessions_controller_spec.rb @@ -48,6 +48,20 @@ describe Users::SessionsController, type: :controller do expect(subject.current_administrateur).to be(nil) end + context 'when the device is trusted' do + before do + allow(controller).to receive(:trusted_device?).and_return(true) + post :create, params: { user: { email: gestionnaire.email, password: gestionnaire.password } } + end + + it 'directly log the gestionnaire' do + expect(subject).to redirect_to gestionnaire_procedures_path + expect(subject.current_user).to be(nil) + expect(subject.current_gestionnaire).to eq(gestionnaire) + expect(subject.current_administrateur).to be(nil) + end + end + context 'signs administrateur in' do # an admin has always an gestionnaire role before { gestionnaire } @@ -237,6 +251,7 @@ describe Users::SessionsController, type: :controller do context 'when the gestionnaire has non other account' do let(:gestionnaire) { create(:gestionnaire) } before do + allow(controller).to receive(:trust_device) post :sign_in_by_link, params: { id: gestionnaire.id, login_token: login_token } end @@ -245,6 +260,7 @@ describe Users::SessionsController, type: :controller do it { is_expected.to redirect_to gestionnaire_procedures_path } it { expect(controller.current_gestionnaire).to eq(gestionnaire) } + it { expect(controller).to have_received(:trust_device) } end context 'when the token is invalid' do @@ -252,6 +268,7 @@ describe Users::SessionsController, type: :controller do it { is_expected.to redirect_to new_user_session_path } it { expect(controller.current_gestionnaire).to be_nil } + it { expect(controller).not_to have_received(:trust_device) } end end @@ -276,4 +293,28 @@ describe Users::SessionsController, type: :controller do end end end + + describe '#trust_device and #trusted_device?' do + subject { controller.trusted_device? } + + context 'when the trusted cookie is not present' do + it { is_expected.to be false } + end + + context 'when the cookie is outdated' do + before do + Timecop.freeze(Time.zone.now - TrustedDeviceConcern::TRUSTED_DEVICE_PERIOD - 1.minute) + controller.trust_device + Timecop.return + end + + it { is_expected.to be false } + end + + context 'when the cookie is ok' do + before { controller.trust_device } + + it { is_expected.to be true } + end + end end