Merge pull request #10154 from demarches-simplifiees/multiple_agent_connect

Tech: permet de faire marcher agent connect simultanément sur le domaine gouv et ds
This commit is contained in:
LeSim 2024-03-19 20:26:31 +00:00 committed by GitHub
commit 7178660acd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 97 additions and 56 deletions

View file

@ -20,22 +20,18 @@ class AgentConnect::AgentController < ApplicationController
def callback
user_info, id_token = AgentConnectService.user_info(params[:code], cookies.encrypted[NONCE_COOKIE_NAME])
cookies.encrypted[NONCE_COOKIE_NAME] = nil
cookies.delete NONCE_COOKIE_NAME
instructeur = Instructeur.find_by(agent_connect_id: user_info['sub'])
if instructeur.nil?
instructeur = Instructeur.find_by(users: { email: santized_email(user_info) })
end
instructeur = Instructeur.find_by(users: { email: santized_email(user_info) })
if instructeur.nil?
user = User.create_or_promote_to_instructeur(santized_email(user_info), Devise.friendly_token[0, 20])
instructeur = user.instructeur
end
instructeur.update(agent_connect_id: user_info['sub'], agent_connect_id_token: id_token)
instructeur.update(agent_connect_id_token: id_token)
aci = AgentConnectInformation.find_or_initialize_by(instructeur:)
aci = AgentConnectInformation.find_or_initialize_by(instructeur:, sub: user_info['sub'])
aci.update(user_info.slice('given_name', 'usual_name', 'email', 'sub', 'siret', 'organizational_unit', 'belonging_population', 'phone'))
sign_in(:user, instructeur.user)
@ -69,7 +65,7 @@ class AgentConnect::AgentController < ApplicationController
flash.alert = t('errors.messages.france_connect.connexion')
redirect_to(new_user_session_path)
else
cookies.encrypted[STATE_COOKIE_NAME] = nil
cookies.delete STATE_COOKIE_NAME
end
end
end

View file

@ -56,7 +56,7 @@ class RecoveriesController < ApplicationController
private
def nature_params = params[:nature]
def siret = current_instructeur.agent_connect_information.siret
def siret = current_instructeur.last_agent_connect_information.siret
def previous_email = params[:previous_email]
def procedure_ids = params[:procedure_ids].map(&:to_i)
@ -70,7 +70,7 @@ class RecoveriesController < ApplicationController
end
def ensure_agent_connect_is_used
if current_instructeur&.agent_connect_information.nil?
if current_instructeur&.last_agent_connect_information.nil?
redirect_to support_recovery_path(error: :must_use_agent_connect)
end
end

View file

@ -110,6 +110,6 @@ class Users::SessionsController < Devise::SessionsController
def build_agent_connect_logout_url(id_token)
h = { id_token_hint: id_token, post_logout_redirect_uri: logout_url }
"#{ENV['AGENT_CONNECT_BASE_URL']}/api/v2/session/end?#{h.to_query}"
"#{AGENT_CONNECT[:end_session_endpoint]}?#{h.to_query}"
end
end

View file

@ -1,9 +0,0 @@
class AgentConnectClient < OpenIDConnect::Client
def initialize(code = nil)
super(AGENT_CONNECT)
if code.present?
self.authorization_code = code
end
end
end

View file

@ -1,8 +1,10 @@
class Instructeur < ApplicationRecord
self.ignored_columns += [:agent_connect_id]
include UserFindByConcern
has_and_belongs_to_many :administrateurs
has_one :agent_connect_information, dependent: :destroy
has_many :agent_connect_information, dependent: :destroy
has_many :assign_to, dependent: :destroy
has_many :groupe_instructeurs, -> { order(:label) }, through: :assign_to
@ -296,6 +298,10 @@ class Instructeur < ApplicationRecord
.update_all(claimant_id: id)
end
def last_agent_connect_information
agent_connect_information.order(updated_at: :desc).first
end
private
def annotations_hash(demande, annotations_privees, avis, messagerie)

View file

@ -2,11 +2,11 @@ class AgentConnectService
include OpenIDConnect
def self.enabled?
ENV.fetch("AGENT_CONNECT_ENABLED", "enabled") == "enabled"
ENV['AGENT_CONNECT_BASE_URL'].present?
end
def self.authorization_uri
client = AgentConnectClient.new
client = OpenIDConnect::Client.new(conf)
state = SecureRandom.hex(16)
nonce = SecureRandom.hex(16)
@ -22,25 +22,25 @@ class AgentConnectService
end
def self.user_info(code, nonce)
client = AgentConnectClient.new(code)
client = OpenIDConnect::Client.new(conf)
client.authorization_code = code
access_token = client.access_token!(client_auth_method: :secret)
discover = find_discover
id_token = ResponseObject::IdToken.decode(access_token.id_token, discover.jwks)
id_token.verify!(
client_id: Rails.application.secrets.agent_connect[:identifier],
issuer: discover.issuer,
nonce: nonce
)
id_token = ResponseObject::IdToken.decode(access_token.id_token, conf[:jwks])
id_token.verify!(conf.merge(nonce: nonce))
[access_token.userinfo!.raw_attributes, access_token.id_token]
end
private
def self.find_discover
Discovery::Provider::Config.discover!("#{ENV.fetch('AGENT_CONNECT_BASE_URL')}/api/v2")
# TODO: remove this block when migration to new domain is done
def self.conf
if Current.host.end_with?('.gouv.fr')
AGENT_CONNECT_GOUV
else
AGENT_CONNECT
end
end
end

View file

@ -5,7 +5,7 @@
%h2 Identification du propriétaire des dossiers
%p Votre organisation est « #{@structure_name} » identifiée par le SIRET #{current_instructeur.agent_connect_information.siret}
%p Votre organisation est « #{@structure_name} » identifiée par le SIRET #{current_instructeur.last_agent_connect_information.siret}
= form_with do |f|
.fr-input-group
%label.fr-label{ for: "email" }

View file

@ -52,12 +52,6 @@ FC_PARTICULIER_ID=""
FC_PARTICULIER_SECRET=""
FC_PARTICULIER_BASE_URL=""
# External service: authentication through Agent Connect
AGENT_CONNECT_ID=""
AGENT_CONNECT_SECRET=""
AGENT_CONNECT_BASE_URL=""
AGENT_CONNECT_REDIRECT=""
# External service: integration with HelpScout (optional)
HELPSCOUT_MAILBOX_ID=""
HELPSCOUT_CLIENT_ID=""

View file

@ -19,8 +19,16 @@ DS_ENV="staging"
# France Connect usage
# FRANCE_CONNECT_ENABLED="disabled" # "enabled" by default
# Agent Connect usage
# AGENT_CONNECT_ENABLED="disabled" # "enabled" by default
# External service: authentication through Agent Connect
# AGENT_CONNECT_ID=""
# AGENT_CONNECT_SECRET=""
# AGENT_CONNECT_BASE_URL=""
# AGENT_CONNECT_REDIRECT=""
# useful when migrating to gouv domain
# AGENT_CONNECT_GOUV_ID=""
# AGENT_CONNECT_GOUV_SECRET=""
# AGENT_CONNECT_GOUV_REDIRECT=""
# Certigna usage
# CERTIGNA_ENABLED="disabled" # "enabled" by default

View file

@ -1 +1,27 @@
AGENT_CONNECT = Rails.application.secrets.agent_connect
if ENV['AGENT_CONNECT_BASE_URL'].present?
discover = OpenIDConnect::Discovery::Provider::Config.discover!("#{ENV.fetch('AGENT_CONNECT_BASE_URL')}/api/v2")
AGENT_CONNECT = {
issuer: discover.issuer,
jwks: discover.jwks,
authorization_endpoint: discover.authorization_endpoint,
token_endpoint: discover.token_endpoint,
userinfo_endpoint: discover.userinfo_endpoint,
end_session_endpoint: discover.end_session_endpoint,
client_id: ENV.fetch('AGENT_CONNECT_ID'),
identifier: ENV.fetch('AGENT_CONNECT_ID'),
secret: ENV.fetch('AGENT_CONNECT_SECRET'),
redirect_uri: ENV.fetch('AGENT_CONNECT_REDIRECT')
}
if ENV['AGENT_CONNECT_GOUV_ID'].present?
gouv_conf = AGENT_CONNECT.dup
gouv_conf[:client_id] = ENV.fetch('AGENT_CONNECT_GOUV_ID')
gouv_conf[:identifier] = ENV.fetch('AGENT_CONNECT_GOUV_ID')
gouv_conf[:secret] = ENV.fetch('AGENT_CONNECT_GOUV_SECRET')
gouv_conf[:redirect_uri] = ENV.fetch('AGENT_CONNECT_GOUV_REDIRECT')
AGENT_CONNECT_GOUV = gouv_conf
end
end

View file

@ -27,14 +27,6 @@ defaults: &defaults
token_endpoint: <%= ENV['FC_PARTICULIER_BASE_URL'] %>/api/v1/token
userinfo_endpoint: <%= ENV['FC_PARTICULIER_BASE_URL'] %>/api/v1/userinfo
logout_endpoint: <%= ENV['FC_PARTICULIER_BASE_URL'] %>/api/v1/logout
agent_connect:
identifier: <%= ENV['AGENT_CONNECT_ID'] %>
secret: <%= ENV['AGENT_CONNECT_SECRET'] %>
redirect_uri: <%= ENV['AGENT_CONNECT_REDIRECT'] %>
authorization_endpoint: <%= ENV['AGENT_CONNECT_BASE_URL'] %>/api/v2/authorize
token_endpoint: <%= ENV['AGENT_CONNECT_BASE_URL'] %>/api/v2/token
userinfo_endpoint: <%= ENV['AGENT_CONNECT_BASE_URL'] %>/api/v2/userinfo
logout_endpoint: <%= ENV['AGENT_CONNECT_BASE_URL'] %>/api/v2/session/end
dolist:
username: <%= ENV['DOLIST_USERNAME'] %>
password: <%= ENV['DOLIST_PASSWORD'] %>

View file

@ -50,7 +50,6 @@ describe AgentConnect::AgentController, type: :controller do
expect(last_user.email).to eq(email)
expect(last_user.confirmed_at).to be_present
expect(last_user.instructeur.agent_connect_id).to eq('sub')
expect(last_user.instructeur.agent_connect_id_token).to eq('id_token')
expect(response).to redirect_to(instructeur_procedures_path)
expect(state_cookie).to be_nil
@ -70,7 +69,6 @@ describe AgentConnect::AgentController, type: :controller do
instructeur.reload
expect(instructeur.agent_connect_id).to eq('sub')
expect(instructeur.agent_connect_id_token).to eq('id_token')
expect(response).to redirect_to(instructeur_procedures_path)
end
@ -88,13 +86,42 @@ describe AgentConnect::AgentController, type: :controller do
instructeur = user.reload.instructeur
expect(instructeur.agent_connect_id).to eq('sub')
expect(instructeur.agent_connect_id_token).to eq('id_token')
expect(response).to redirect_to(instructeur_procedures_path)
end
end
end
context 'when the instructeur connects two times with the same domain' do
before do
expect(AgentConnectService).to receive(:user_info).with(code, nonce).and_return([user_info, id_token]).twice
expect(controller).to receive(:sign_in).twice
end
it 'creates another agent_connect_information' do
get :callback, params: { code: code, state: state }
get :callback, params: { code: code, state: state }
expect(Instructeur.last.agent_connect_information.count).to eq(1)
end
end
context 'when the instructeur connects two times with different domains' do
before do
expect(controller).to receive(:sign_in).twice
end
it 'creates another agent_connect_information' do
expect(AgentConnectService).to receive(:user_info).with(code, nonce).and_return([user_info, id_token])
get :callback, params: { code: code, state: state }
expect(AgentConnectService).to receive(:user_info).with(code, nonce).and_return([user_info.merge('sub' => 'sub2'), id_token])
get :callback, params: { code: code, state: state }
expect(Instructeur.last.agent_connect_information.pluck(:sub)).to match_array(['sub', 'sub2'])
end
end
context 'but user_info raises and error' do
before do
expect(AgentConnectService).to receive(:user_info).and_raise(Rack::OAuth2::Client::Error.new(500, error: 'Unknown'))

View file

@ -85,6 +85,7 @@ describe Users::SessionsController, type: :controller do
let(:agent_connect_id_token) { nil }
before do
stub_const("AGENT_CONNECT", { end_session_endpoint: 'http://agent-connect/logout' })
sign_in user
delete :destroy
end