Skip to content
This repository was archived by the owner on Apr 8, 2026. It is now read-only.
Draft
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
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ gem 'listen'
gem 'logstasher', '~> 3.0'
gem 'mailjet'
gem 'mjml-rails'
gem 'doorkeeper'
gem 'omniauth-oauth2'
gem 'omniauth-rails_csrf_protection'
gem 'omniauth-proconnect'
Expand Down
3 changes: 3 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ GEM
diff-lcs (1.6.2)
docile (1.4.1)
domain_name (0.6.20240107)
doorkeeper (5.8.2)
railties (>= 5)
draper (4.0.6)
actionpack (>= 5.0)
activemodel (>= 5.0)
Expand Down Expand Up @@ -666,6 +668,7 @@ DEPENDENCIES
connection_pool (< 3.0)
csv
cuprite
doorkeeper
draper
factory_bot_rails
faker
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,14 @@ http://entreprise.api.localtest.me:5000/
http://particulier.api.localtest.me:5000/
```

## OAuth2 de démonstration (Doorkeeper + dummy DS)

- `bundle install` puis `bin/rails db:migrate` pour ajouter les tables Doorkeeper.
- `rails db:seed:replant` crée une application OAuth `Dummy DS` (`dummy-ds-client-id` / `dummy-ds-client-secret`) avec redirect URI `http://ds.api.localtest.me:5678/auth/api_entreprise/callback`.
- Lancer le serveur Rails (`./bin/local.sh`) pour exposer `/oauth/authorize` et `/oauth/token` sur `http://entreprise.api.localtest.me:5000`.
- Depuis `dummy_app/`, `bundle install` puis `bundle exec rackup -p 5678 -o 0.0.0.0`, puis ouvrir `http://ds.api.localtest.me:5678/settings` et cliquer sur "Connecter API Entreprise".
- La réponse `/oauth/token` renvoie un champ `api_tokens` contenant les jetons API Entreprise sélectionnés côté consentement.

### Avec Docker

Pour lancer le server:
Expand Down
2 changes: 2 additions & 0 deletions app/controllers/oauth.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module OAuth
end
55 changes: 55 additions & 0 deletions app/controllers/oauth/authorizations_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
class OAuth::AuthorizationsController < Doorkeeper::AuthorizationsController
layout 'api_entreprise/application'

helper ExternalUrlHelper
helper ScopeHelper
helper_method :namespace

before_action :load_available_tokens, only: %i[new create] # rubocop:disable Rails/LexicallyScopedActionFilter

def new
if pre_auth.authorizable?
if skip_authorization? || (matching_token? && !force_consent?)
auth = authorization.authorize
URI.parse(auth.redirect_uri)
session.delete(:prompt)
redirect_or_render(auth)
else
render :new
end
else
render :error
end
end

def namespace
'api_entreprise'
end

private

def force_consent?
true
end

def authorize_response
@authorize_response ||= super.tap do |response|
persist_token_selection(response)
end
end

def persist_token_selection(response)
grant = response.try(:auth)&.token
return unless grant.is_a?(Doorkeeper::AccessGrant)

grant.update(token_ids: selected_token_ids)
end

def selected_token_ids
Array(params[:token_ids]).compact_blank
end

def load_available_tokens
@available_tokens = current_user ? current_user.tokens.active_for('entreprise') : []
end
end
16 changes: 16 additions & 0 deletions app/controllers/oauth/me_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
class OAuth::MeController < Doorkeeper::ApplicationController
before_action :doorkeeper_authorize!

def show
token = doorkeeper_token
render json: {
oauth_token: {
id: token.id,
scopes: token.scopes.to_a,
expires_in: token.expires_in,
created_at: token.created_at
},
api_tokens: token.api_tokens_payload
}
end
end
33 changes: 33 additions & 0 deletions app/controllers/oauth/sessions_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
class OAuth::SessionsController < ApplicationController
layout 'api_entreprise/application'

helper_method :namespace

def new
if current_user
redirect_to session[:user_return_to] || root_path
else
@client_name = oauth_client_name
end
end

def namespace
'api_entreprise'
end

private

def oauth_client_name
return_to = session[:user_return_to]
return 'Application tierce' unless return_to

uri = URI.parse(return_to)
params = Rack::Utils.parse_query(uri.query)
client_id = params['client_id']

app = Doorkeeper::Application.find_by(uid: client_id)
app&.name || 'Application tierce'
rescue StandardError
'Application tierce'
end
end
14 changes: 14 additions & 0 deletions app/controllers/oauth/tokens_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
class OAuth::TokensController < Doorkeeper::TokensController
def create
headers.merge!(authorize_response.headers)

body = authorize_response.body
token = authorize_response.try(:token)
if token.respond_to?(:api_tokens_payload)
token.reload
body = body.merge('api_tokens' => token.api_tokens_payload)
end

render json: body, status: authorize_response.status
end
end
4 changes: 2 additions & 2 deletions app/helpers/scope_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ def build_scopes(scopes, api)
scopes_tree
end

private

def humanize_scope(scope, api)
I18n.t("api_#{api}.tokens.token.scope.#{scope}.label", default: scope.humanize)
end

private

def build_scopes_parts(scopes_tree, splitted_scope) # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
if splitted_scope.size > 2
scopes_tree[splitted_scope[0]] ||= {}
Expand Down
3 changes: 2 additions & 1 deletion app/helpers/user_sessions_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ def user_is_demandeur?(authorization_request)

def sign_in_and_redirect(user)
session[:current_user_id] = user.id
redirect_current_user_to_homepage
return_to = session.delete(:user_return_to)
redirect_to(return_to || authorization_requests_path, allow_other_host: false)
end

def redirect_current_user_to_homepage
Expand Down
15 changes: 15 additions & 0 deletions app/lib/seeds.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def create_data_for_api_particulier

def create_data_shared
create_magic_link
create_dummy_doorkeeper_application
end

def create_main_user
Expand Down Expand Up @@ -95,6 +96,20 @@ def create_magic_link
MagicLink.create!(email: @user.email)
end

def create_dummy_doorkeeper_application
Doorkeeper::Application.find_or_create_by!(uid: 'dummy-ds-client-id') do |app|
app.name = 'Dummy DS'
app.secret = 'dummy-ds-client-secret'
app.redirect_uri = [
'http://ds.api.localtest.me:5678/auth/api_entreprise/callback',
'http://localhost:5678/auth/api_entreprise/callback',
'http://127.0.0.1:5678/auth/api_entreprise/callback'
].join("\n")
app.scopes = ''
app.confidential = true
end
end

def create_api_entreprise_token_valid
create_token(
%w[open_data unites_legales_etablissements_insee attestation_sociale_urssaf attestation_fiscale_dgfip],
Expand Down
70 changes: 70 additions & 0 deletions app/views/oauth/authorizations/new.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<div class="fr-container fr-my-6w">
<div class="fr-grid-row fr-grid-row--gutters">
<div class="fr-col-12 fr-col-md-8">
<h1 class="fr-h2 fr-mb-2w">
Autoriser <%= @pre_auth.client.application.name %> à utiliser vos accès API Entreprise
</h1>
<p class="fr-text--lead fr-mb-4w">
Sélectionnez les demandes DataPass pour lesquelles vous autorisez l'éditeur à récupérer des données protégées d'entreprises.
</p>

<% if @available_tokens.any? %>
<%= form_tag oauth_authorization_path, method: :post, id: 'authorize-form', data: { turbo: false } do %>
<%= hidden_field_tag :client_id, @pre_auth.client.uid %>
<%= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri %>
<%= hidden_field_tag :state, @pre_auth.state %>
<%= hidden_field_tag :response_type, @pre_auth.response_type %>
<%= hidden_field_tag :scope, @pre_auth.scope %>

<fieldset class="fr-fieldset" aria-labelledby="demandes-legend">
<legend class="fr-fieldset__legend--regular fr-fieldset__legend" id="demandes-legend">
Vos demandes DataPass
</legend>
<% @available_tokens.each do |token| %>
<% authorization_request = token.authorization_request %>
<div class="fr-fieldset__element">
<div class="fr-checkbox-group">
<%= check_box_tag 'token_ids[]', token.id, true, id: dom_id(token, :share) %>
<%= label_tag dom_id(token, :share), class: 'fr-label' do %>
D<%= authorization_request&.external_id %> — <%= authorization_request&.intitule %>
<% if authorization_request %>
<span class="fr-hint-text">
<%= link_to 'Voir sur DataPass', datapass_authorization_request_url(authorization_request), target: '_blank', rel: 'noopener' %>
</span>
<% end %>
<% end %>
</div>
</div>
<% end %>
</fieldset>
<% end %>

<div class="fr-mt-4w" style="display: flex; gap: 1rem; justify-content: flex-end; flex-wrap: wrap;">
<button type="submit" form="authorize-form" class="fr-btn">Autoriser</button>
<%= form_tag oauth_authorization_path, method: :delete, id: 'deny-form', data: { turbo: false }, style: 'margin: 0;' do %>
<%= hidden_field_tag :client_id, @pre_auth.client.uid %>
<%= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri %>
<%= hidden_field_tag :state, @pre_auth.state %>
<%= hidden_field_tag :response_type, @pre_auth.response_type %>
<%= hidden_field_tag :scope, @pre_auth.scope %>
<% end %>
<button type="submit" form="deny-form" class="fr-btn fr-btn--secondary">Refuser</button>
</div>
<% else %>
<div class="fr-alert fr-alert--warning fr-alert--sm">
<p>Aucune demande DataPass active n'est disponible sur votre compte.</p>
</div>
<%= form_tag oauth_authorization_path, method: :delete, data: { turbo: false } do %>
<%= hidden_field_tag :client_id, @pre_auth.client.uid %>
<%= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri %>
<%= hidden_field_tag :state, @pre_auth.state %>
<%= hidden_field_tag :response_type, @pre_auth.response_type %>
<%= hidden_field_tag :scope, @pre_auth.scope %>
<div class="fr-btns-group fr-mt-4w">
<button type="submit" class="fr-btn fr-btn--secondary">Retour</button>
</div>
<% end %>
<% end %>
</div>
</div>
</div>
31 changes: 31 additions & 0 deletions app/views/oauth/sessions/new.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<div class="fr-container fr-my-6w">
<div class="fr-grid-row fr-grid-row--gutters">
<div class="fr-col-12 fr-col-md-8">
<h1 class="fr-h2 fr-mb-2w">
<%= @client_name %> souhaite accéder à vos accès API Entreprise
</h1>
<p class="fr-text--lead fr-mb-4w">
Connectez-vous via ProConnect pour sélectionner les informations que vous souhaitez partager.
</p>

<div class="fr-callout fr-mb-4w">
<p class="fr-callout__text">
En vous connectant, vous pourrez autoriser <strong><%= @client_name %></strong>
à récupérer des données protégées d'entreprises via API Entreprise, en votre nom.
</p>
</div>

<div class="fr-connect-group">
<%= form_with url: login_proconnect_entreprise_path, method: :post, data: { turbo: false }, local: true do %>
<button type="submit" class="fr-connect fr-connect-proconnect fr-m-auto">
<span class="fr-connect__login">S'identifier avec</span>
<span class="fr-connect__brand">ProConnect</span>
</button>
<p>
<%= link_to "Qu'est-ce que ProConnect ?", "https://www.proconnect.gouv.fr/", target: '_blank', rel: 'noopener', title: "Qu'est-ce que ProConnect ? - Nouvelle fenêtre" %>
</p>
<% end %>
</div>
</div>
</div>
</div>
Loading
Loading