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

Merged
merged 23 commits into from
Nov 25, 2024
Merged

OAuth Provider #4749

merged 23 commits into from
Nov 25, 2024

Conversation

victormours
Copy link
Contributor

@victormours victormours commented Oct 28, 2024

Contexte : l'intégration à Démarches Simplifiées

Dans le cadre de notre financement par le FTAP, on a prévu de faire une intégration avec Démarches Simplifiées pour permettre à l'instructeur d'un dossier sur DS de proposer un rendez-vous à l'usager qui a envoyé le dossier.
Cette intégration nécessite que Démarches Simplifiées appelle notre api, par exemple vérifier l'existence de plages d'ouverture, ou pour envoyer l'invitation (une fois qu'on aura un endpoint qui permettra de faire cette action).

Voir la maquette de l'intégration : https://www.figma.com/board/Vuy7UCLaLWpoY1u30AEaKG/Piste-d'int%C3%A9gration-RDV-%26-DS?node-id=0-1&node-type=canvas&t=eQKAZwYgYYkh8ERT-0
Piste d'intégration RDV & DS.pdf

Le problème, c'est que l'authentification à notre api actuelle ne fonctionne pas très bien pour ce cas d'usage. On a deux modes d'authentification :

  • Par email/mot de passe via la gem devise_token_auth : l'endpoint /api/v1/auth/sign_in permet d'envoyer un email/mot de passe et d'obtenir un token d'api en échange. Si on utilisait ce mécanisme pour l'intégration à DS, il faudrait demander aux agents d'entrer leur email/mot de passe de RDV Service Public sur le front de DS, ce qui n'est pas acceptable (c'est la pire dé-sensibilitation au risque de phishing qu'on puisse imaginer). Cela ne permet pas non plus d'authentifier les agents qui utilisent ProConnect)
  • Par "shared secret" : l'application cliente fait un hash de l'id, le nom, prénom et email de l'agent avec une clé d'api (voir app/controllers/api/v1/agent_auth_base_controller.rb:95), et ajoute ce hash en header des requêtes. C'est ce qu'utilise RDV Insertion pour les agents qui utilisent ProConnect. Le problème de cette approche est qu'il n'y a pas vraiment de vérification que l'agent utilisé a bien donné la permission à ce qu'on utilise l'api en son nom, et ça permet à l'application cliente de lire et modifier des données pour tous les agents de l'application. C'est déjà discutable pour RDV Insertion, et pas généralisable à d'autres applications (impossible à justifier d'un point de vue RGPD).

On a donc besoin d'un autre mode d'authentification des agents à notre api, et on a la chance d'être dans un cas très classique pour lequel un standard a émergé : l'OAuth.

La place de l'oauth dans cette intégration

En proposant aux agents qui utilisent Démarches Simplifiées et RDV Service Public de connecter les deux applications par Oauth, on permet à la fois de fournir un mode d'authentification viable, et d'expliciter l'intégration aux agents.
Le fait de rendre notre application visible à l'agent permet aussi d'éviter les demandes qu'on a eu de la part de certains partenaires potentiel de cacher toute notre application derrière leur front et de recoder toute notre application en api.

Le parcours d'OAuth permettra à Démarches Simplifiées de récupérer un token d'api au nom de l'agent.

Le plan d'ouverture de l'oauth

C'est un gros projet, donc on va le découper :

MVP : Mise en place du parcours d'OAuth pour RDV Insertion

Pour cette première étape, on ne change pas l'authentification à l'api, mais on met en place le parcours d'oauth pour les agents.

Ça nous permet directement de remplacer la manière dont certains agents se connectent à RDV Insertion en entrant leur email/mot de passe RDV Solidarités sur le front de RDV Insertion. Cette PR chez RDV Insertion permettra de faire le changement une fois qu'on aura déployé l'oauth sur RDV Service Public gip-inclusion/rdv-insertion#2426

Critère d'acceptation (aka "definition of done") :

  • les agents arrêtent d'entrer leur mot de passe RDV Solidarités sur RDV Insertion

V1 : Authentification à l'api par token récupérés pendant l'Oauth par RDV Insertion

Une fois que les agents seront authentifiés par Oauth, on pourra commencer à utiliser les token pour les appels faits par RDV Insertion, et à voir comment on gère les refresh_token.

A partir de ce moment, on pourra aussi désactiver le bouton ProConnect sur RDV Insertion, puisque la connexion à ProConnect pourra se faire directement sur RDV Solidarités.

Réutilisation pour Démarches Simplifiées

Une fois que ça marchera pour RDV Insertion, ça devrait être réutilisable à l'identique pour commencer à travailler sur l'intégration avec DS

Le périmètre de cette PR dans ce plan

Le but de cette PR est de permettre des premiers tests utilisateurs du parcours d'OAuth avec RDV Insertion. Elle se concentre plutôt sur les aspects techniques, et le but est donc de valider les différents choix qui sont faits ici.

NB : Pour faire les strong migrations correctement, on ne peut pas merger directement cette PR. On va d'abord extraire la migration de création des tables dans une autre PR avant de merger celle-ci

La mise en prod du code de cette PR

Plan de mise en ligne :

  • PR pour créer les tables
  • PR pour valider les clés étrangères et ajouter les commentaires
  • Mettre en ligne le code avec doorkeeper
  • Mettre en ligne côté RDV Insertion mais sans activer les variables d'env
  • Créer l'app Oauth chez RDV Solidarités, ajouter les variables d'env sur RDV Insertion
  • Tester en interne sur la prod avec le paramètre force dans l'url
  • Faire du user test en visio avec des agents
  • Ajuster l'interface en fonction de leur retours
  • Tester à nouveau
  • Ajuster à nouveau
  • Ouvrir

Solution :

Rappel sur l'oauth https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow

Choix d'implémentation majeurs

Doorkeeper

On utilise la gem doorkeeper pour l'implémentation. Ça semble être un standard, et gitlab s'en sert aussi. On a essayé d'en faire l'usage le plus simple possible.

gem omniauth dans lib

endpoint de /me

De manière similaire à ce que fait Github, on propose un endpoint /api/v1/agents/me pour les infos de l'agent connecté. C'est la manière la plus standard que j'ai trouvé pour faire ça. L'introspection de tokens ne fonctionne pas parce que le token qui authentifie la requête doit être différent de celui qui est introspecté)

Choix de configuration de doorkeeper

  • chiffrer tout ce qui peut l'être
  • ferme le back-office de crud sur les applications, et proposer uniquement un script pour créer une appli

Choix d'implémentation mineurs

  • garder la colonne confidential : c'est pas forcément plus clair de la monkeypatcher, et si jamais le cas d'usage apparaît, on pourra s'en servir
  • garder les routes des authorized applications mais sans
  • garder les templates html qui n'ont pas été customisés pour faciliter la future customization

Tester

En local, on peut directement tester avec RDV Insertion en étant sur la bonne branche et en relançant les seeds.

Scénarios à tester dans le navigateur

  • connexion avec email/mdp par oauth puis déconnexion des deux services
  • connexion avec proconnect puis déconnexion des trois services ?

Prochaines étapes après cette PR

Tests utilisateurs et amélioration de la page pour accepter l'autorisation

  • layout de la page pour accepter l'autorisation : est-ce qu'on ancre tout à gauche et on met juste les cta à droite ?
  • layout de la page pour accepter l'autorisation : est-ce qu'on affiche tout le menu des préférences agent ?
  • est-ce qu'on supprime complètement l'écran d'autorisation ? voir l'option skip_authorization dans l'initializer

Hors scope

  • affiner les scopes permis aux tokens
  • afficher la liste des applis oauth dans un back office : on préfère ne pas ouvrir ces routes
  • gérer le login_hint
  • gérer le force_login

@victormours victormours marked this pull request as draft October 28, 2024 11:30
Copy link

gitguardian bot commented Nov 4, 2024

️✅ There are no secrets present in this pull request anymore.

If these secrets were true positive and are still valid, we highly recommend you to revoke them.
Once a secret has been leaked into a git repository, you should consider it compromised, even if it was deleted immediately.
Find here more information about risks.


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@victormours victormours force-pushed the oauth-provider branch 3 times, most recently from 4457543 to 5d084e2 Compare November 13, 2024 16:46
@victormours victormours marked this pull request as ready for review November 13, 2024 16:58
@victormours victormours reopened this Nov 13, 2024
Copy link
Contributor

@adipasquale adipasquale left a comment

Choose a reason for hiding this comment

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

commentaire edité car posté trop vite, woups!

félicitations pour cette PR monstre @victormours 👍

la description est limpide et c’est parfait de pouvoir se projeter aussi loin que tu l’as déjà fait dans les étapes à venir.
Je n’ai pas formellement accepté la PR puisqu’elle n’est pas vouée à être mergée mais plutôt qu’on va en extraire des PRs successives.
Je n’ai aucun retour bloquant.

Je n’ai pas testé en local, je n’ai pas cherché à le faire je me dis que ça pourrait être plus efficace de le faire tous les deux en synchrone et éventuellement le documenter.

Dans la description de la PR je n’ai pas compris cette phrase :

L'introspection de tokens ne fonctionne pas parce que le token qui authentifie la requête doit être différent de celui qui est introspecté)

config/routes.rb Show resolved Hide resolved
spec/support/capybara_config.rb Show resolved Hide resolved
db/migrate/20241104135353_comment_doorkeeper_tables.rb Outdated Show resolved Hide resolved
docs/interconnexions/oauth.md Outdated Show resolved Hide resolved

Ci-dessous le processus d'autorisation par lequel un agent de demarches-simplifiees.fr lie son compte à RDV-SP :

```mermaid
Copy link
Contributor

Choose a reason for hiding this comment

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

super clair ❤️ merci

Copy link
Contributor Author

Choose a reason for hiding this comment

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

un grand merci à @francois-ferrandis qui a rédigé cette doc 😍

Copy link
Contributor

Choose a reason for hiding this comment

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

C'était rigolo et je suis fier d'avoir mon nom au générique. 🍿

config/initializers/doorkeeper.rb Outdated Show resolved Hide resolved
app/views/layouts/application_agent_config.html.slim Outdated Show resolved Hide resolved
Copy link
Contributor

@francois-ferrandis francois-ferrandis left a comment

Choose a reason for hiding this comment

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

Superbe boulot, ça a l'air prêt à partir en prod. 🤞 🚀

# Use DeviseTokenAuth
authenticate_api_v1_agent_with_token_auth!
else
Copy link
Contributor

Choose a reason for hiding this comment

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

Dommage qu'on ait pas de moyen de déterminer quand il faut tenter une auth Doorkeep, ça signifie qu'on va tenter cette auth par défaut, pour toutes les requêtes qui ne passent pas dans les if / elsif. C'est pas forcément un problème, je vérifier aussi que j'ai bien compris.

J'ai initialement été surpris que le if doorkeeper_token n'ait pas de else, mais en fait le else est explicite : si on a pas de doorkeeper_token, c'est que le doorkeeper_authorize! a échoué et a déjà appelé un render d'erreur, c'est ça ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

en gros oui

@@ -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
Copy link
Contributor

Choose a reason for hiding this comment

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

On dévoile l'attribut inclusion_connect_open_id_sub de l'agent, c'est OK (je pense que oui) ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

je crois que oui (et on le fait déjà par ailleurs)

Comment on lines -17 to +19
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
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

app/controllers/search_controller.rb Outdated Show resolved Hide resolved
class FakeOauthClient < Sinatra::Base
use OmniAuth::Builder do
provider :rdv_service_public, "fake_app_id", "fake_app_secret",
scope: "write", base_url: Capybara.app_host
Copy link
Contributor

Choose a reason for hiding this comment

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

La cerise sur le gâteau (dans une autre PR) serait de pouvoir utiliser cette app Sinatra en dev, mais j'y suis pas arrivé en moins de 15 minutes (la route http://127.0.0.1:4567/omniauth/rdvservicepublic n'est pas définie, je ne sais pas pourquoi).

Copy link
Contributor Author

@victormours victormours Nov 25, 2024

Choose a reason for hiding this comment

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

oui, j'ai eu des difficultés à faire ça moi aussi. J'en suis arrivé à la conclusion que le préfixe omniauth n'est pas exactement le même (la route devient http://127.0.0.1:4567/auth/rdvservicepublic. Je suis pas sûr exactement de l'explication, et omniauth n'aide vraiment pas à débugger ça.

Copy link
Collaborator

@aminedhobb aminedhobb left a comment

Choose a reason for hiding this comment

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

Trop cool @victormours 🙏 🚀 !
J'ai testé le flow avec rdv-i ça marche nickel :).
Je t'ai laissé des mini remarques, je te laisserai juger si elles sont pertinentes ou non vis à vis de vos pratiques.

spec/features/agents/oauth_provider_spec.rb Outdated Show resolved Hide resolved
' 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|
Copy link
Collaborator

Choose a reason for hiding this comment

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

Pareil ici peut-être que de la documentation ou un lien quelque part vers ce qu'est @pre_auth (sur la classe par exemple) peut être utile

@victormours victormours merged commit aa64b77 into production Nov 25, 2024
15 checks passed
@victormours victormours deleted the oauth-provider branch November 25, 2024 17:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: ✅ Done
Development

Successfully merging this pull request may close these issues.

4 participants