feature(agent_connect_2fa): add intermediate pages to improve UX

This commit is contained in:
simon lehericey 2024-09-16 12:14:46 +02:00
parent 6f5135a6b2
commit cd2d772cd0
No known key found for this signature in database
GPG key ID: CDE670D827C7B3C5
8 changed files with 120 additions and 5 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -10,6 +10,9 @@ class AgentConnect::AgentController < ApplicationController
STATE_COOKIE_NAME = :agentConnect_state STATE_COOKIE_NAME = :agentConnect_state
NONCE_COOKIE_NAME = :agentConnect_nonce NONCE_COOKIE_NAME = :agentConnect_nonce
AC_ID_TOKEN_COOKIE_NAME = :agentConnect_id_token
REDIRECT_TO_AC_LOGIN_COOKIE_NAME = :redirect_to_ac_login
def index def index
end end
@ -27,7 +30,11 @@ class AgentConnect::AgentController < ApplicationController
cookies.delete NONCE_COOKIE_NAME cookies.delete NONCE_COOKIE_NAME
if user_info['idp_id'] == MON_COMPTE_PRO_IDP_ID && !amr.include?('mfa') if user_info['idp_id'] == MON_COMPTE_PRO_IDP_ID && !amr.include?('mfa')
return redirect_to ENV['MON_COMPTE_PRO_2FA_NOT_CONFIGURED_URL'], allow_other_host: true # we need the id_token to disconnect the agent connect session later.
# we cannot store it in the instructeur model because the user is not yet created
# so we store it in a encrypted cookie
cookies.encrypted[AC_ID_TOKEN_COOKIE_NAME] = id_token
return redirect_to agent_connect_explanation_2fa_path
end end
instructeur = Instructeur.find_by(users: { email: santized_email(user_info) }) instructeur = Instructeur.find_by(users: { email: santized_email(user_info) })
@ -52,6 +59,31 @@ class AgentConnect::AgentController < ApplicationController
redirect_france_connect_error_connection redirect_france_connect_error_connection
end end
def explanation_2fa
end
# Special callback from MonComptePro juste after 2FA configuration
# then:
# - the current user is disconnected from the AgentConnect session by redirecting to the AgentConnect logout endpoint
# - the user is redirected to User::SessionsController#logout by agent connect (no choice)
# - the cookie redirect_to_ac_login is detected and the controller redirects to the relogin_after_2fa_config page
# - finally, the user clicks on the button to reconnect to the AgentConnect session
def logout_from_mcp
sign_out(:user) if user_signed_in?
id_token = cookies.encrypted[AC_ID_TOKEN_COOKIE_NAME]
cookies.delete(AC_ID_TOKEN_COOKIE_NAME)
return redirect_to root_path if id_token.blank?
cookies.encrypted[REDIRECT_TO_AC_LOGIN_COOKIE_NAME] = true
redirect_to AgentConnectService.logout_url(id_token, host_with_port: request.host_with_port), allow_other_host: true
end
def relogin_after_2fa_config
end
private private
def santized_email(user_info) def santized_email(user_info)

View file

@ -109,6 +109,12 @@ class Users::SessionsController < Devise::SessionsController
# agent connect callback # agent connect callback
def logout def logout
if cookies.encrypted[AgentConnect::AgentController::REDIRECT_TO_AC_LOGIN_COOKIE_NAME].present?
cookies.delete(AgentConnect::AgentController::REDIRECT_TO_AC_LOGIN_COOKIE_NAME)
return redirect_to agent_connect_relogin_after_2fa_config_path
end
redirect_to root_path, notice: I18n.t('devise.sessions.signed_out') redirect_to root_path, notice: I18n.t('devise.sessions.signed_out')
end end
end end

View file

@ -0,0 +1,14 @@
.fr-container
%h1.fr-h2.fr-mt-4w Une validation en 2 étapes est désormais nécessaire.
%p.fr-mb-2w
La sécurité de votre compte augmente. Nous vous demandons à présent une validation en 2 étapes pour vous connecter.
%p.fr-mb-2w
Vous allez devoir <b>configurer votre mode d'authentification</b> sur le site <b>MonComptePro</b> :
%img{ src: image_url("instructions_moncomptepro.png"), alt: "MonComptePro", loading: 'lazy' }
%button.fr-btn.fr-btn--primary.fr-mb-2w
= link_to "Configurer mon appli d'authentification sur MonComptePro", ENV['MON_COMPTE_PRO_2FA_NOT_CONFIGURED_URL']

View file

@ -0,0 +1,12 @@
.fr-container
%h1.fr-h2.fr-mt-4w Poursuivez votre connexion à #{APPLICATION_NAME}
= render Dsfr::AlertComponent.new(state: :success, extra_class_names: 'fr-mb-4w') do |c|
- c.with_body do
%p Votre application d'authentification a bien été configurée.
%p.fr-mb-4w
Vous allez maintenant pouvoir <b>vous connecter</b> à nouveau à #{APPLICATION_NAME} en effectuant la validation en 2 étapes avec votre <b>application d'authentification</b>.
%button.fr-btn.fr-btn--primary.fr-mb-2w
= link_to "Se connecter à #{APPLICATION_NAME} avec #{AgentConnect}", agent_connect_login_path

View file

@ -202,6 +202,9 @@ Rails.application.routes.draw do
get '' => 'agent#index' get '' => 'agent#index'
get 'login' => 'agent#login' get 'login' => 'agent#login'
get 'callback' => 'agent#callback' get 'callback' => 'agent#callback'
get 'explanation_2fa' => 'agent#explanation_2fa'
get 'relogin_after_2fa_config' => 'agent#relogin_after_2fa_config'
get 'logout_from_mcp' => 'agent#logout_from_mcp'
end end
namespace :champs do namespace :champs do

View file

@ -38,24 +38,24 @@ describe AgentConnect::AgentController, type: :controller do
context 'and user_info returns some info' do context 'and user_info returns some info' do
before do before do
ENV['MON_COMPTE_PRO_2FA_NOT_CONFIGURED_URL'] = 'https://moncomptepro.fr/not_configured'
expect(AgentConnectService).to receive(:user_info).with(code, nonce).and_return([user_info, id_token, amr]) expect(AgentConnectService).to receive(:user_info).with(code, nonce).and_return([user_info, id_token, amr])
end end
context 'and the instructeur use mon_compte_pro without 2FA' do context 'and the instructeur use mon_compte_pro' do
before do before do
user_info['idp_id'] = AgentConnect::AgentController::MON_COMPTE_PRO_IDP_ID user_info['idp_id'] = AgentConnect::AgentController::MON_COMPTE_PRO_IDP_ID
allow(controller).to receive(:sign_in) allow(controller).to receive(:sign_in)
end end
context 'without 2FA' do context 'without 2FA' do
it 'redirects to MON_COMPTE_PRO_2FA_NOT_CONFIGURED_URL' do it 'redirects to agent_connect_explanation_2fa_path' do
subject subject
expect(controller).not_to have_received(:sign_in) expect(controller).not_to have_received(:sign_in)
expect(response).to redirect_to(ENV['MON_COMPTE_PRO_2FA_NOT_CONFIGURED_URL']) expect(response).to redirect_to(agent_connect_explanation_2fa_path)
expect(state_cookie).to be_nil expect(state_cookie).to be_nil
expect(nonce_cookie).to be_nil expect(nonce_cookie).to be_nil
expect(cookies.encrypted[controller.class::AC_ID_TOKEN_COOKIE_NAME]).to eq(id_token)
end end
end end
@ -198,6 +198,37 @@ describe AgentConnect::AgentController, type: :controller do
end end
end end
describe '#logout_from_mcp' do
let(:id_token) { 'id_token' }
subject { get :logout_from_mcp }
before do
cookies.encrypted[controller.class::AC_ID_TOKEN_COOKIE_NAME] = id_token
end
it 'clears the id token cookie and redirects to the agent connect logout url' do
expect(AgentConnectService).to receive(:logout_url).with(id_token, host_with_port: 'test.host')
.and_return("https://agent-connect.fr/logout/#{id_token}")
subject
expect(cookies.encrypted[controller.class::AC_ID_TOKEN_COOKIE_NAME]).to be_nil
expect(cookies.encrypted[controller.class::REDIRECT_TO_AC_LOGIN_COOKIE_NAME]).to eq(true)
expect(response).to redirect_to("https://agent-connect.fr/logout/#{id_token}")
end
context 'when the id_token is blank' do
let(:id_token) { nil }
it 'clears the cookies and redirects to the root path' do
subject
expect(cookies.encrypted[controller.class::REDIRECT_TO_AC_LOGIN_COOKIE_NAME]).to be_nil
expect(response).to redirect_to(root_path)
end
end
end
def state_cookie def state_cookie
cookies.encrypted[controller.class::STATE_COOKIE_NAME] cookies.encrypted[controller.class::STATE_COOKIE_NAME]
end end

View file

@ -311,4 +311,21 @@ describe Users::SessionsController, type: :controller do
end end
end end
end end
describe '#logout' do
subject { get :logout }
it 'redirects to root_path' do
expect(subject).to redirect_to(root_path)
end
context 'when the cookie redirect_to_ac_login is present' do
before { cookies.encrypted[AgentConnect::AgentController::REDIRECT_TO_AC_LOGIN_COOKIE_NAME] = true }
it 'redirects to relogin_after_2fa_config' do
expect(subject).to redirect_to(agent_connect_relogin_after_2fa_config_path)
expect(cookies.encrypted[AgentConnect::AgentController::REDIRECT_TO_AC_LOGIN_COOKIE_NAME]).to be_nil
end
end
end
end end