Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OAuth Provider #4749

Open
wants to merge 7 commits into
base: production
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ gem "omniauth-github"
gem "omniauth-microsoft_graph"
# OpenID Connect Strategy for OmniAuth
gem "omniauth_openid_connect"
# Oauth provider
gem "doorkeeper"
# Translations for Doorkeeper
gem "doorkeeper-i18n"
# Provides CSRF protection on OmniAuth request endpoint on Rails application.
gem "omniauth-rails_csrf_protection"
# OO authorization for Rails
Expand Down Expand Up @@ -264,4 +268,7 @@ group :test do

# Dépendence indirecte de axe-core-api
gem "axiom-types", git: "https://github.com/rdv-solidarites/axiom-types.git", ref: "b9b204c"

gem "sinatra"
gem "omniauth-rdv-service-public", path: "lib/omniauth-rdv-service-public"
end
23 changes: 23 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ PATH
anonymizer (0.1.0)
activerecord (>= 7.0)

PATH
remote: lib/omniauth-rdv-service-public
specs:
omniauth-rdv-service-public (0.1.0)
omniauth (~> 2.0)
omniauth-oauth2 (~> 1.8)

GEM
remote: https://rubygems.org/
specs:
Expand Down Expand Up @@ -217,6 +224,10 @@ GEM
devise (> 3.5.2, < 5)
rails (>= 4.2.0, < 7.3)
diff-lcs (1.5.0)
doorkeeper (5.7.1)
railties (>= 5)
doorkeeper-i18n (5.2.7)
doorkeeper (>= 5.2)
dotenv (2.8.1)
dotenv-rails (2.8.1)
dotenv (= 2.8.1)
Expand Down Expand Up @@ -340,6 +351,8 @@ GEM
activesupport (>= 5.2, < 8)
msgpack (1.7.2)
multi_xml (0.6.0)
mustermann (3.0.3)
ruby2_keywords (~> 0.0.1)
mutex_m (0.2.0)
net-http (0.4.1)
uri
Expand Down Expand Up @@ -577,6 +590,7 @@ GEM
rexml
ruby-ole (1.2.12.2)
ruby-progressbar (1.13.0)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
sassc (2.4.0)
ffi (~> 1.9)
Expand All @@ -600,6 +614,11 @@ GEM
simple_form (5.1.0)
actionpack (>= 5.2)
activemodel (>= 5.2)
sinatra (3.2.0)
mustermann (~> 3.0)
rack (~> 2.2, >= 2.2.4)
rack-protection (= 3.2.0)
tilt (~> 2.0)
skylight (6.0.1)
activesupport (>= 5.2.0)
slim (4.1.0)
Expand Down Expand Up @@ -716,6 +735,8 @@ DEPENDENCIES
devise-async
devise_invitable
devise_token_auth (= 1.2.4)
doorkeeper
doorkeeper-i18n
dotenv-rails
drb
dsfr-view-components
Expand All @@ -739,6 +760,7 @@ DEPENDENCIES
omniauth-github
omniauth-microsoft_graph
omniauth-rails_csrf_protection
omniauth-rdv-service-public!
omniauth_openid_connect
ostruct
paper_trail
Expand Down Expand Up @@ -773,6 +795,7 @@ DEPENDENCIES
sentry-rails
sentry-ruby
simple_form (~> 5.0)
sinatra
skylight
slim
slim_lint
Expand Down
21 changes: 17 additions & 4 deletions app/controllers/agents/sessions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,27 @@ def create
end

def destroy
if session[:agent_connect_id_token]
sign_out(:agent) && set_flash_message!(:notice, :signed_out)
if params[:oauth_client_app_id].present? && params[:oauth_client_app_id].in?(session[:oauth_app_ids])
oauth_app = Doorkeeper::Application.find_by(uid: params[:oauth_client_app_id])
@oauth_client_app_post_logout_redirect_url = oauth_app.post_logout_redirect_uri
end

agent_connect_id_token = session.delete(:agent_connect_id_token)

sign_out(:agent)

if @oauth_client_app_post_logout_redirect_url
session[:post_logout_redirect_url] = @oauth_client_app_post_logout_redirect_url
else
set_flash_message!(:notice, :signed_out)
end

agent_connect_client = AgentConnectOpenIdClient::Logout.new(session.delete(:agent_connect_id_token))
if agent_connect_id_token
agent_connect_client = AgentConnectOpenIdClient::Logout.new(agent_connect_id_token)

redirect_to agent_connect_client.agent_connect_logout_url(root_url), allow_other_host: true
else
super
redirect_to after_sign_out_path_for(:agent)
end
end

Expand Down
7 changes: 6 additions & 1 deletion app/controllers/api/v1/agent_auth_base_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,14 @@ def authenticate_agent
if request.headers.include?("X-Agent-Auth-Signature")
# Bypass DeviseTokenAuth
authenticate_agent_with_shared_secret
else
elsif request.headers["HTTP_ACCESS_TOKEN"] && request.headers["HTTP_UID"]
# Use DeviseTokenAuth
authenticate_api_v1_agent_with_token_auth!
else
doorkeeper_authorize!
if doorkeeper_token
@current_agent = Agent.find(doorkeeper_token.resource_owner_id)
end
end
end

Expand Down
4 changes: 4 additions & 0 deletions app/controllers/api/v1/agents_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@ def index
agents = agents.joins(:organisations).where(organisations: { id: current_organisation.id }) if current_organisation.present?
render_collection(agents.order(:created_at))
end

def me
render_record current_agent
end
end
5 changes: 3 additions & 2 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ def after_sign_in_path_for(resource)
end

def after_sign_out_path_for(resource)
return "https://#{ENV['FRANCECONNECT_HOST']}/api/v1/logout" \
if @connected_with_franceconnect
if @connected_with_franceconnect
return "https://#{ENV['FRANCECONNECT_HOST']}/api/v1/logout"
end

super
end
Expand Down
30 changes: 22 additions & 8 deletions app/controllers/search_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,32 @@ class SearchController < ApplicationController
# utilisé par le Pas-de-Calais pour prendre rdv depuis leur site : https://www.pasdecalais.fr/Solidarite-Sante/Enfance-et-famille/La-Protection-Maternelle-et-Infantile/Prendre-rendez-vous-en-ligne-en-MDS-PMI-ou-service-social
after_action :allow_iframe

def home
post_logout_redirect_url = session.delete(:post_logout_redirect_url)

if post_logout_redirect_url
redirect_to post_logout_redirect_url, allow_other_host: true
return
end

if current_domain == Domain::RDV_MAIRIE
render "dsfr/rdv_mairie/homepage"
else
search_rdv
end
end

def search_rdv
# TODO : public_link_organisation_id has to work if agent is logged in ?
if current_agent && params[:prescripteur] == Prescripteur::INTERNE && session[:agent_prescripteur_organisation_id]
redirect_to search_creneau_admin_organisation_prescription_path(session[:agent_prescripteur_organisation_id], agent_search_params)
end
@context = if invitation?
WebInvitationSearchContext.new(user: current_user, query_params: query_params)
else
WebSearchContext.new(user: current_user, query_params: query_params)
end
if current_domain == Domain::RDV_MAIRIE && request.path == "/"
render "dsfr/rdv_mairie/homepage"
else
@context = if invitation?
WebInvitationSearchContext.new(user: current_user, query_params: query_params)
else
WebSearchContext.new(user: current_user, query_params: query_params)
end
render :search_rdv
end
end

Expand Down
6 changes: 6 additions & 0 deletions app/helpers/doorkeeper_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module DoorkeeperHelper
# TODO: éviter de dupliquer cette méthode
def current_domain
@current_domain ||= Domain.find_matching(URI.parse(request.url).host)
end
end
1 change: 1 addition & 0 deletions app/javascript/stylesheets/application_agent_config.scss
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
@import "./components/rdv_solidarites_instance_name";
@import "./components/alert";
@import "./components/select2";
@import "./components/creneaux"; // pour le color-scheme-green

// Structure
@import "./structure/general";
Expand Down
6 changes: 6 additions & 0 deletions app/jobs/cron_job/destroy_old_oauth_objects.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class CronJob::DestroyOldOauthObjects < CronJob
def perform
Doorkeeper::AccessGrant.where("created_at < ?", 24.hours.ago).delete_all
Doorkeeper::AccessToken.where("created_at < ?", 30.days.ago).delete_all
end
end
5 changes: 5 additions & 0 deletions app/models/agent.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ def timeout_in = 14.days # Used by Devise's :timeoutable
has_many :territories_through_organisations, source: :territory, through: :organisations
has_many :webhook_endpoints, through: :organisations

# Associations pour être provider d'OAuth
# On désactive le cop pour inverse_of car les modèles sont gérés dans Doorkeeper, et on ne se sert pas de l'association inverse
has_many :access_grants, class_name: "Doorkeeper::AccessGrant", foreign_key: :resource_owner_id, dependent: :delete_all # rubocop:disable Rails/InverseOf
has_many :access_tokens, class_name: "Doorkeeper::AccessToken", foreign_key: :resource_owner_id, dependent: :delete_all # rubocop:disable Rails/InverseOf

attr_accessor :allow_blank_name

# Validation
Expand Down
9 changes: 9 additions & 0 deletions app/views/doorkeeper/authorizations/error.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<div class="border-bottom mb-4">
<h1><%= t('doorkeeper.authorizations.error.title') %></h1>
</div>

<main role="main">
<pre>
<%= (local_assigns[:error_response] ? error_response : @pre_auth.error_response).body[:error_description] %>
</pre>
</main>
15 changes: 15 additions & 0 deletions app/views/doorkeeper/authorizations/form_post.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<header class="page-header">
<h1><%= t('.title') %></h1>
</header>

<%= form_tag @pre_auth.redirect_uri, method: :post, name: :redirect_form, authenticity_token: false do %>
<% auth.body.compact.each do |key, value| %>
<%= hidden_field_tag key, value %>
<% end %>
<% end %>

<script>
window.onload = function () {
document.forms['redirect_form'].submit();
};
</script>
45 changes: 45 additions & 0 deletions app/views/doorkeeper/authorizations/new.html.slim
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
h1.rdv-text-align-center.mt-0.font-weight-bold.mb-4 Validation de permissions

.card
.card-body
.d-flex.justify-content-center
= image_tag(@pre_auth.client.application.logo_base64, width: 200, alt: "Le logo de #{@pre_auth.client.application.name}")

= image_tag(current_domain.dark_logo_path, width: 200, alt: "Le logo de #{current_domain.name}")

p.rdv-text-align-center
i.fa-solid.fa-circle-check.color-scheme-green.mr-1
| #{@pre_auth.client.name} est une application partenaire de #{current_domain.name}.

p.rdv-text-align-center
' En continuant, vous allez permettre à <b>#{@pre_auth.client.application.name}</b> d'accéder à votre compte <b>#{current_domain.name}</b>
' lié à l'adresse #{current_agent.email}

- @pre_auth.scopes.each do |scope|
/ Quand on affinera les scopes, il faudra ajouter des textes d'explication pour les permissions données ici
/= t(scope, scope: [:doorkeeper, :scopes])

.row.rdv-text-align-center
.col-md-6
= form_tag oauth_authorization_path, method: :delete do
= hidden_field_tag :client_id, @pre_auth.client.uid, id: nil
= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri, id: nil
= hidden_field_tag :state, @pre_auth.state, id: nil
= hidden_field_tag :response_type, @pre_auth.response_type, id: nil
= hidden_field_tag :response_mode, @pre_auth.response_mode, id: nil
= hidden_field_tag :scope, @pre_auth.scope, id: nil
= hidden_field_tag :code_challenge, @pre_auth.code_challenge, id: nil
= hidden_field_tag :code_challenge_method, @pre_auth.code_challenge_method, id: nil
= submit_tag "Annuler", class: "btn btn-outline-primary"

.col-md-6
= form_tag oauth_authorization_path, method: :post do
= hidden_field_tag :client_id, @pre_auth.client.uid, id: nil
= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri, id: nil
= hidden_field_tag :state, @pre_auth.state, id: nil
= hidden_field_tag :response_type, @pre_auth.response_type, id: nil
= hidden_field_tag :response_mode, @pre_auth.response_mode, id: nil
= hidden_field_tag :scope, @pre_auth.scope, id: nil
= hidden_field_tag :code_challenge, @pre_auth.code_challenge, id: nil
= hidden_field_tag :code_challenge_method, @pre_auth.code_challenge_method, id: nil
= submit_tag "Continuer", class: "btn btn-primary"
7 changes: 7 additions & 0 deletions app/views/doorkeeper/authorizations/show.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<header class="page-header">
<h1><%= t('.title') %>:</h1>
</header>

<main role="main">
<code id="authorization_code"><%= params[:code] %></code>
</main>
9 changes: 9 additions & 0 deletions app/views/doorkeeper/authorized_applications/index.html.slim
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
- content_for :title, "Applications connectées"

- @applications.each do |application|
.card
.card-header
h3= application.name
.card-body.flex
= form_tag oauth_authorized_application_path(application), method: :delete, class: "mt-2" do
= submit_tag "Déconnecter", data: { confirm: "Déconnecter"}, class: "btn btn-outline-danger"
10 changes: 4 additions & 6 deletions app/views/layouts/application_agent_config.html.slim
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,20 @@ html lang="fr"
.auth-fluid-left.text-center.col-xs-12.col-md-4 class="#{"auth-fluid-left--agent-preferences" if current_agent}"
.p-2
= link_logo
- if current_agent.present?
- if current_agent
= link_to(root_path, class: "pt-1 pb-4 text-white") do
i.fa.fa-arrow-left
= " Retour à l'accueil"
.align-items-center.d-flex.mt-2.mt-lg-4
.p-1.p-lg-3.flex-grow-1.mt-lg-4
- if agent_path? && current_agent.nil?
p.lead.mb-3 Terminé l'agenda papier, moins de temps perdu.
- elsif agent_path? && current_agent.present?
- if current_agent
= render "agents/preferences_menu"
- else
h4.mb-3 Prenez RDV en ligne avec votre département !
p.lead.mb-3 Terminé l'agenda papier, moins de temps perdu.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ce n’est pas très important, mais je ne suis pas sûr de comprendre ce changement dans cette PR ? ça vient peut-être d’un merge raté ?


.auth-fluid-form-box.col-xs-12.col-md-8
/ Permet de centrer les formulaire de login, et d'aligner en haut de l'écran les formulaires de préférences
div class="#{"align-items-center d-flex h-100" if current_agent.blank?}"
div class="#{"align-items-center d-flex h-100" unless current_agent}"
.p-3.flex-grow-1
= render "layouts/flash"
.row
Expand Down
1 change: 0 additions & 1 deletion app/views/search/_address_selection.html.slim

This file was deleted.

2 changes: 1 addition & 1 deletion app/views/search/search_rdv.html.slim
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

/ Adress selection partials have multiple sections
- if @context.current_step == :address_selection
= render @context, context: @context
= render(current_domain.address_selection_template_name, context: @context)
- else
section.rdv-background-color-alt-grey.py-4
= render @context, context: @context
Loading
Loading