Merge pull request #9904 from demarches-simplifiees/use_email_merge_token

Use email merge token
This commit is contained in:
LeSim 2024-01-11 10:45:07 +00:00 committed by GitHub
commit 5f4aa4fc4a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 91 additions and 20 deletions

View file

@ -1,6 +1,7 @@
class FranceConnect::ParticulierController < ApplicationController class FranceConnect::ParticulierController < ApplicationController
before_action :redirect_to_login_if_fc_aborted, only: [:callback] before_action :redirect_to_login_if_fc_aborted, only: [:callback]
before_action :securely_retrieve_fci, only: [:merge, :merge_with_existing_account, :merge_with_new_account, :mail_merge_with_existing_account, :resend_and_renew_merge_confirmation] before_action :securely_retrieve_fci, only: [:merge, :merge_with_existing_account, :merge_with_new_account, :resend_and_renew_merge_confirmation]
before_action :securely_retrieve_fci_from_email_merge_token, only: [:mail_merge_with_existing_account]
def login def login
if FranceConnectService.enabled? if FranceConnectService.enabled?
@ -14,7 +15,7 @@ class FranceConnect::ParticulierController < ApplicationController
fci = FranceConnectService.find_or_retrieve_france_connect_information(params[:code]) fci = FranceConnectService.find_or_retrieve_france_connect_information(params[:code])
if fci.user.nil? if fci.user.nil?
preexisting_unlinked_user = User.find_by(email: fci.email_france_connect.downcase) preexisting_unlinked_user = User.find_by(email: sanitize(fci.email_france_connect))
if preexisting_unlinked_user.nil? if preexisting_unlinked_user.nil?
fci.associate_user!(fci.email_france_connect) fci.associate_user!(fci.email_france_connect)
@ -57,6 +58,7 @@ class FranceConnect::ParticulierController < ApplicationController
else else
@fci.update(user: user) @fci.update(user: user)
@fci.delete_merge_token! @fci.delete_merge_token!
@fci.delete_email_merge_token!
flash.notice = t('france_connect.particulier.flash.connection_done', application_name: APPLICATION_NAME) flash.notice = t('france_connect.particulier.flash.connection_done', application_name: APPLICATION_NAME)
connect_france_connect_particulier(user) connect_france_connect_particulier(user)
@ -67,7 +69,7 @@ class FranceConnect::ParticulierController < ApplicationController
end end
def mail_merge_with_existing_account def mail_merge_with_existing_account
user = User.find_by(email: @fci.email_france_connect.downcase) user = User.find_by(email: sanitize(@fci.email_france_connect.downcase))
if user.can_france_connect? if user.can_france_connect?
@fci.update(user: user) @fci.update(user: user)
@fci.delete_merge_token! @fci.delete_merge_token!
@ -96,14 +98,33 @@ class FranceConnect::ParticulierController < ApplicationController
end end
def resend_and_renew_merge_confirmation def resend_and_renew_merge_confirmation
@fci.create_email_merge_token!
UserMailer.france_connect_merge_confirmation(
@fci.email_france_connect,
@fci.email_merge_token,
@fci.email_merge_token_created_at
)
.deliver_later
merge_token = @fci.create_merge_token! merge_token = @fci.create_merge_token!
UserMailer.france_connect_merge_confirmation(@fci.email_france_connect, merge_token, @fci.merge_token_created_at).deliver_later
redirect_to france_connect_particulier_merge_path(merge_token), redirect_to france_connect_particulier_merge_path(merge_token),
notice: t('france_connect.particulier.flash.confirmation_mail_sent') notice: t('france_connect.particulier.flash.confirmation_mail_sent')
end end
private private
def securely_retrieve_fci_from_email_merge_token
@fci = FranceConnectInformation.find_by(email_merge_token: email_merge_token_params)
if @fci.nil? || !@fci.valid_for_email_merge?
flash.alert = t('france_connect.particulier.flash.merger_token_expired', application_name: APPLICATION_NAME)
redirect_to root_path
else
@fci.delete_email_merge_token!
end
end
def securely_retrieve_fci def securely_retrieve_fci
@fci = FranceConnectInformation.find_by(merge_token: merge_token_params) @fci = FranceConnectInformation.find_by(merge_token: merge_token_params)
@ -141,11 +162,19 @@ class FranceConnect::ParticulierController < ApplicationController
params[:merge_token] params[:merge_token]
end end
def email_merge_token_params
params[:email_merge_token]
end
def password_params def password_params
params[:password] params[:password]
end end
def sanitized_email_params def sanitized_email_params
params[:email]&.gsub(/[[:space:]]/, ' ')&.strip&.downcase sanitize(params[:email])
end
def sanitize(string)
string&.gsub(/[[:space:]]/, ' ')&.strip&.downcase
end end
end end

View file

@ -20,9 +20,9 @@ class UserMailer < ApplicationMailer
mail(to: requested_email, subject: @subject) mail(to: requested_email, subject: @subject)
end end
def france_connect_merge_confirmation(email, merge_token, merge_token_created_at) def france_connect_merge_confirmation(email, email_merge_token, email_merge_token_created_at)
@merge_token = merge_token @email_merge_token = email_merge_token
@merge_token_created_at = merge_token_created_at @email_merge_token_created_at = email_merge_token_created_at
@subject = "Veuillez confirmer la fusion de compte" @subject = "Veuillez confirmer la fusion de compte"
mail(to: email, subject: @subject) mail(to: email, subject: @subject)

View file

@ -26,19 +26,34 @@ class FranceConnectInformation < ApplicationRecord
def create_merge_token! def create_merge_token!
merge_token = SecureRandom.uuid merge_token = SecureRandom.uuid
update(merge_token: merge_token, merge_token_created_at: Time.zone.now) update(merge_token:, merge_token_created_at: Time.zone.now)
merge_token merge_token
end end
def create_email_merge_token!
email_merge_token = SecureRandom.uuid
update(email_merge_token:, email_merge_token_created_at: Time.zone.now)
email_merge_token
end
def valid_for_merge? def valid_for_merge?
(MERGE_VALIDITY.ago < merge_token_created_at) && user_id.nil? (MERGE_VALIDITY.ago < merge_token_created_at) && user_id.nil?
end end
def valid_for_email_merge?
(MERGE_VALIDITY.ago < email_merge_token_created_at) && user_id.nil?
end
def delete_merge_token! def delete_merge_token!
update(merge_token: nil, merge_token_created_at: nil) update(merge_token: nil, merge_token_created_at: nil)
end end
def delete_email_merge_token!
update(email_merge_token: nil, email_merge_token_created_at: nil)
end
def full_name def full_name
[given_name, family_name].compact.join(" ") [given_name, family_name].compact.join(" ")
end end

View file

@ -1,16 +1,17 @@
- content_for(:title, @subject) - content_for(:title, @subject)
- merge_link = france_connect_particulier_mail_merge_with_existing_account_url(email_merge_token: @email_merge_token)
%p %p
Bonjour, Bonjour,
%p %p
Pour confirmer la fusion de votre compte, veuillez cliquer sur le lien suivant : Pour confirmer la fusion de votre compte, veuillez cliquer sur le lien suivant :
= round_button 'Je confirme', france_connect_particulier_mail_merge_with_existing_account_url(merge_token: @merge_token), :primary = round_button 'Je confirme', merge_link, :primary
%p %p
Vous pouvez aussi visiter ce lien : #{link_to france_connect_particulier_mail_merge_with_existing_account_url(merge_token: @merge_token), france_connect_particulier_mail_merge_with_existing_account_url(merge_token: @merge_token)} Vous pouvez aussi visiter ce lien : #{link_to merge_link, merge_link}
%p Ce lien est valide #{distance_of_time_in_words(FranceConnectInformation::MERGE_VALIDITY)}, jusqu'à #{@merge_token_created_at.strftime("%d-%m-%Y à %H:%M (%Z)")} %p Ce lien est valide #{distance_of_time_in_words(FranceConnectInformation::MERGE_VALIDITY)}, jusqu'à #{@email_merge_token_created_at.strftime("%d-%m-%Y à %H:%M (%Z)")}
%p %p
Si vous nêtes pas à lorigine de cette demande, vous pouvez ignorer ce message. Et si vous avez besoin dassistance, nhésitez pas à nous contacter à Si vous nêtes pas à lorigine de cette demande, vous pouvez ignorer ce message. Et si vous avez besoin dassistance, nhésitez pas à nous contacter à

View file

@ -177,7 +177,7 @@ Rails.application.routes.draw do
get 'particulier' => 'particulier#login' get 'particulier' => 'particulier#login'
get 'particulier/callback' => 'particulier#callback' get 'particulier/callback' => 'particulier#callback'
get 'particulier/merge/:merge_token' => 'particulier#merge', as: :particulier_merge get 'particulier/merge/:merge_token' => 'particulier#merge', as: :particulier_merge
get 'particulier/mail_merge_with_existing_account/:merge_token' => 'particulier#mail_merge_with_existing_account', as: :particulier_mail_merge_with_existing_account get 'particulier/mail_merge_with_existing_account/:email_merge_token' => 'particulier#mail_merge_with_existing_account', as: :particulier_mail_merge_with_existing_account
post 'particulier/resend_and_renew_merge_confirmation' => 'particulier#resend_and_renew_merge_confirmation', as: :particulier_resend_and_renew_merge_confirmation post 'particulier/resend_and_renew_merge_confirmation' => 'particulier#resend_and_renew_merge_confirmation', as: :particulier_resend_and_renew_merge_confirmation
post 'particulier/merge_with_existing_account' => 'particulier#merge_with_existing_account' post 'particulier/merge_with_existing_account' => 'particulier#merge_with_existing_account'
post 'particulier/merge_with_new_account' => 'particulier#merge_with_new_account' post 'particulier/merge_with_new_account' => 'particulier#merge_with_new_account'

View file

@ -0,0 +1,10 @@
class AddEmailMergeTokenColumnToFranceConnectInformation < ActiveRecord::Migration[7.0]
disable_ddl_transaction!
def change
add_column :france_connect_informations, :email_merge_token, :string
add_column :france_connect_informations, :email_merge_token_created_at, :datetime
add_index :france_connect_informations, :email_merge_token, algorithm: :concurrently
end
end

View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2023_12_21_142727) do ActiveRecord::Schema[7.0].define(version: 2024_01_10_113623) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto" enable_extension "pgcrypto"
enable_extension "plpgsql" enable_extension "plpgsql"
@ -626,6 +626,8 @@ ActiveRecord::Schema[7.0].define(version: 2023_12_21_142727) do
t.datetime "created_at", precision: nil, null: false t.datetime "created_at", precision: nil, null: false
t.jsonb "data" t.jsonb "data"
t.string "email_france_connect" t.string "email_france_connect"
t.string "email_merge_token"
t.datetime "email_merge_token_created_at"
t.string "family_name" t.string "family_name"
t.string "france_connect_particulier_id" t.string "france_connect_particulier_id"
t.string "gender" t.string "gender"
@ -634,6 +636,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_12_21_142727) do
t.datetime "merge_token_created_at", precision: nil t.datetime "merge_token_created_at", precision: nil
t.datetime "updated_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false
t.integer "user_id" t.integer "user_id"
t.index ["email_merge_token"], name: "index_france_connect_informations_on_email_merge_token"
t.index ["merge_token"], name: "index_france_connect_informations_on_merge_token" t.index ["merge_token"], name: "index_france_connect_informations_on_merge_token"
t.index ["user_id"], name: "index_france_connect_informations_on_user_id" t.index ["user_id"], name: "index_france_connect_informations_on_user_id"
end end

View file

@ -268,10 +268,10 @@ describe FranceConnect::ParticulierController, type: :controller do
describe '#mail_merge_with_existing_account' do describe '#mail_merge_with_existing_account' do
let(:fci) { FranceConnectInformation.create!(user_info) } let(:fci) { FranceConnectInformation.create!(user_info) }
let!(:merge_token) { fci.create_merge_token! } let!(:email_merge_token) { fci.create_email_merge_token! }
context 'when the merge_token is ok and the user is found' do context 'when the merge_token is ok and the user is found' do
subject { post :mail_merge_with_existing_account, params: { merge_token: fci.merge_token } } subject { post :mail_merge_with_existing_account, params: { email_merge_token: } }
let!(:user) { create(:user, email: email, password: 'abcdefgh') } let!(:user) { create(:user, email: email, password: 'abcdefgh') }
@ -281,6 +281,7 @@ describe FranceConnect::ParticulierController, type: :controller do
expect(fci.user).to eq(user) expect(fci.user).to eq(user)
expect(fci.merge_token).to be_nil expect(fci.merge_token).to be_nil
expect(fci.email_merge_token).to be_nil
expect(controller.current_user).to eq(user) expect(controller.current_user).to eq(user)
expect(flash[:notice]).to eq("Les comptes FranceConnect et #{APPLICATION_NAME} sont à présent fusionnés") expect(flash[:notice]).to eq("Les comptes FranceConnect et #{APPLICATION_NAME} sont à présent fusionnés")
end end
@ -298,8 +299,8 @@ describe FranceConnect::ParticulierController, type: :controller do
end end
end end
context 'when the merge_token is not ok' do context 'when the email_merge_token is not ok' do
subject { post :mail_merge_with_existing_account, params: { merge_token: 'ko' } } subject { post :mail_merge_with_existing_account, params: { email_merge_token: 'ko' } }
let!(:user) { create(:user, email: email) } let!(:user) { create(:user, email: email) }
@ -308,7 +309,7 @@ describe FranceConnect::ParticulierController, type: :controller do
fci.reload fci.reload
expect(fci.user).to be_nil expect(fci.user).to be_nil
expect(fci.merge_token).not_to be_nil expect(fci.email_merge_token).not_to be_nil
expect(controller.current_user).to be_nil expect(controller.current_user).to be_nil
expect(response).to redirect_to(root_path) expect(response).to redirect_to(root_path)
end end
@ -340,6 +341,8 @@ describe FranceConnect::ParticulierController, type: :controller do
context 'when an account with the same email exists' do context 'when an account with the same email exists' do
let!(:user) { create(:user, email: email) } let!(:user) { create(:user, email: email) }
before { allow(controller).to receive(:sign_in).and_call_original }
render_views render_views
it 'asks for the corresponding password' do it 'asks for the corresponding password' do
@ -352,6 +355,15 @@ describe FranceConnect::ParticulierController, type: :controller do
expect(response.body).to include('entrez votre mot de passe') expect(response.body).to include('entrez votre mot de passe')
end end
it 'cannot use the merge token in the email confirmation route' do
subject
fci.reload
get :mail_merge_with_existing_account, params: { email_merge_token: fci.merge_token }
expect(controller).not_to have_received(:sign_in)
expect(flash[:alert]).to be_present
end
end end
end end
@ -360,6 +372,7 @@ describe FranceConnect::ParticulierController, type: :controller do
let(:merge_token) { fci.create_merge_token! } let(:merge_token) { fci.create_merge_token! }
it 'renew token' do it 'renew token' do
expect { post :resend_and_renew_merge_confirmation, params: { merge_token: merge_token } }.to change { fci.reload.merge_token } expect { post :resend_and_renew_merge_confirmation, params: { merge_token: merge_token } }.to change { fci.reload.merge_token }
expect(fci.email_merge_token).to be_present
expect(response).to redirect_to(france_connect_particulier_merge_path(fci.reload.merge_token)) expect(response).to redirect_to(france_connect_particulier_merge_path(fci.reload.merge_token))
end end
end end

View file

@ -65,7 +65,7 @@ RSpec.describe UserMailer, type: :mailer do
subject { described_class.france_connect_merge_confirmation(email, code, 15.minutes.from_now) } subject { described_class.france_connect_merge_confirmation(email, code, 15.minutes.from_now) }
it { expect(subject.to).to eq([email]) } it { expect(subject.to).to eq([email]) }
it { expect(subject.body).to include(france_connect_particulier_mail_merge_with_existing_account_url(merge_token: code)) } it { expect(subject.body).to include(france_connect_particulier_mail_merge_with_existing_account_url(email_merge_token: code)) }
context 'without SafeMailer configured' do context 'without SafeMailer configured' do
it { expect(subject[BalancerDeliveryMethod::FORCE_DELIVERY_METHOD_HEADER]&.value).to eq(nil) } it { expect(subject[BalancerDeliveryMethod::FORCE_DELIVERY_METHOD_HEADER]&.value).to eq(nil) }