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

Migration vers ProConnect #19

Merged
merged 22 commits into from
Oct 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
738ce4a
Ajout du bouton ProConnect officiel
ikarius Oct 3, 2024
b5525a0
oidc: redirection après callback d'identification OIDC
ikarius Oct 3, 2024
3378803
oidc: effacement des données de connexion avant logout OIDC
ikarius Oct 3, 2024
a4c61d6
Ajout du bouton de déconnexion ProConnect
ikarius Oct 3, 2024
98d8ac3
models: ajout du champ `sub_pc` pour ProConnect
ikarius Oct 3, 2024
70251c6
oidc: modification de l'app
ikarius Oct 3, 2024
7c1b3e4
libs: ajout de `mozilla-django-oidc`
ikarius Oct 3, 2024
a4bff98
Vérification de la déconnexion ProConnect
ikarius Oct 7, 2024
2c047d6
fix: ajout du backend d'identification par défaut
ikarius Oct 7, 2024
33961dd
safir: récupération du code SAFIR
ikarius Oct 7, 2024
7b70fb2
fix: modification d'un log en exception
ikarius Oct 14, 2024
f74045d
Ajout d'une variable d'environnement pour identifier le backend OIDC
ikarius Oct 21, 2024
e63dcc3
revue: typos
ikarius Oct 21, 2024
fdd4455
Merge commit 'e63dcc3c158174e5fb42758757a3a456b22d09a7' into pro-conn…
ggounot Oct 23, 2024
3440064
Merge commit 'f74045df83d95a782b1464cf2a16b2cffb6e2b12' into pro-conn…
ggounot Oct 23, 2024
8e11dff
Vérification complémentaire de la validité du `sub`
ikarius Oct 24, 2024
96245d1
Ajout des assets ProConnect
ikarius Oct 24, 2024
1d73349
Modification de la page de connexion
ikarius Oct 24, 2024
a3840cd
Modification des différentes pages mentionnant Inclusion Connect
ikarius Oct 24, 2024
0d1e464
fix: récupération de la var-env OIDC_AUTH_BACKEND
ikarius Oct 24, 2024
eb2c6aa
fix: correction du `next_url` avec multiples paramètres
ikarius Oct 25, 2024
1ac6063
revue: corrections et typos
ikarius Oct 25, 2024
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
72 changes: 69 additions & 3 deletions back/config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
INSTALLED_APPS = [
"django.contrib.gis",
"django.contrib.auth",
# OIDC / ProConnect : doit être chargé après `django.contrib.auth`
"mozilla_django_oidc",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
Expand Down Expand Up @@ -64,8 +66,21 @@
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"csp.middleware.CSPMiddleware",
# Rafraichissement du token ProConnect
"mozilla_django_oidc.middleware.SessionRefresh",
]

# OIDC / ProConnect
AUTHENTICATION_BACKENDS = [
# auth par défaut pour la partie admin :
"django.contrib.auth.backends.ModelBackend",
"dora.oidc.OIDCAuthenticationBackend",
]

# Permet de garder le comportement d'identification "standard" (e-mail/password)
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_AUTHENTICATION_METHOD = "email"

ROOT_URLCONF = "config.urls"

TEMPLATES = [
Expand Down Expand Up @@ -287,7 +302,7 @@
# Modération :
MATTERMOST_HOOK_KEY = os.getenv("MATTERMOST_HOOK_KEY")

# INCLUSION-CONNECT / PRO-CONNECT
# INCLUSION-CONNECT
IC_ISSUER_ID = os.getenv("IC_ISSUER_ID")
IC_AUTH_URL = os.getenv("IC_AUTH_URL")
IC_TOKEN_URL = os.getenv("IC_TOKEN_URL")
Expand All @@ -296,7 +311,59 @@
IC_CLIENT_ID = os.getenv("IC_CLIENT_ID")
IC_CLIENT_SECRET = os.getenv("IC_CLIENT_SECRET")

# Recherches sauvagardées :
# OIDC / PROCONNECT
PC_CLIENT_ID = os.getenv("PC_CLIENT_ID")
PC_CLIENT_SECRET = os.getenv("PC_CLIENT_SECRET")
PC_DOMAIN = os.getenv("PC_DOMAIN", "fca.integ01.dev-agentconnect.fr")
PC_ISSUER = os.getenv("PC_ISSUER", f"{PC_DOMAIN}/api/v2")
PC_AUTHORIZE_PATH = os.getenv("PC_AUTHORIZE_PATH", "authorize")
PC_TOKEN_PATH = os.getenv("PC_TOKEN_PATH", "token")
PC_USERINFO_PATH = os.getenv("PC_USERINFO_PATH", "userinfo")

# ProConnect à besoin de ce setting pour le logout
FRONTEND_URL = os.getenv("FRONTEND_URL")

# mozilla_django_oidc:
OIDC_RP_CLIENT_ID = os.getenv("PC_CLIENT_ID")
OIDC_RP_CLIENT_SECRET = os.getenv("PC_CLIENT_SECRET")
OIDC_RP_SCOPES = "openid given_name usual_name email siret custom uid"

# `mozilla_django_oidc` n'utilise pas de discovery / .well-known
# on définit donc chaque endpoint
OIDC_RP_SIGN_ALGO = "RS256"
OIDC_OP_JWKS_ENDPOINT = f"https://{PC_ISSUER}/jwks"
OIDC_OP_AUTHORIZATION_ENDPOINT = f"https://{PC_ISSUER}/authorize"
OIDC_OP_TOKEN_ENDPOINT = f"https://{PC_ISSUER}/token"
OIDC_OP_USER_ENDPOINT = f"https://{PC_ISSUER}/userinfo"
OIDC_OP_LOGOUT_ENDPOINT = f"https://{PC_ISSUER}/session/end"

# Les paramètres suivants servent à adapter la configuration OIDC
# de `mozilla-django_oidc` pour pouvoir fonctionner dans le contexte
# spécifique à DORA et ProConnect.

# OIDC : intervalle de rafraichissement du token (4h)
OIDC_RENEW_ID_TOKEN_EXPIRY_SECONDS = 4 * 60 * 60

# OIDC : nécessaire pour la gestion de la fin de session coté ProConnect
OIDC_STORE_ID_TOKEN = True
ALLOW_LOGOUT_GET_METHOD = True

# obligatoire pour ProConnect: à passer en paramètre de requête supplémentaire
# lors de la première phase du flow OIDC
OIDC_AUTH_REQUEST_EXTRA_PARAMS = {"acr_values": "eidas1"}

# OIDC : redirection vers le front DORA en cas de succès de l'identification
# necessaire pour la gestion de "l'URL suivant" (`next_url`)
LOGIN_REDIRECT_URL = "/oidc/logged_in/"

# OIDC : redirection vers l'acceuil du front DORA pour la déconnexion
LOGOUT_REDIRECT_URL = FRONTEND_URL

# OIDC : permet de préciser quelle est la class/vue en charge du callback dans le flow OIDC
# (essentiellement pour la gestion du `next_url`).
OIDC_CALLBACK_CLASS = "dora.oidc.views.CustomAuthorizationCallbackView"

# Recherches sauvegardées :
INCLUDES_DI_SERVICES_IN_SAVED_SEARCH_NOTIFICATIONS = (
os.getenv("INCLUDES_DI_SERVICES_IN_SAVED_SEARCH_NOTIFICATIONS") == "true"
)
Expand Down Expand Up @@ -353,7 +420,6 @@
EMAIL_PORT = os.getenv("EMAIL_PORT")
EMAIL_USE_TLS = True
EMAIL_DOMAIN = os.getenv("EMAIL_DOMAIN")
FRONTEND_URL = os.getenv("FRONTEND_URL")
SUPPORT_EMAIL = os.getenv("SUPPORT_EMAIL")

SUPPORT_LINK = "https://aide.dora.inclusion.beta.gouv.fr"
Expand Down
4 changes: 3 additions & 1 deletion back/config/settings/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,7 @@
IC_TOKEN_URL = os.getenv("IC_TOKEN_URL", "https://whatever-oidc-token-url.com")
AWS_STORAGE_BUCKET_NAME = os.getenv("AWS_STORAGE_BUCKET_NAME", "dora")
SIB_ONBOARDING_LIST = os.getenv("SIB_ONBOARDING_LIST", "1")
SIB_ONBOARDING_PUTATIVE_MEMBER_LIST = os.getenv("SIB_ONBOARDING_PUTATIVE_MEMBER_LIST", "2")
SIB_ONBOARDING_PUTATIVE_MEMBER_LIST = os.getenv(
"SIB_ONBOARDING_PUTATIVE_MEMBER_LIST", "2"
)
SIB_ONBOARDING_MEMBER_LIST = os.getenv("SIB_ONBOARDING_MEMBER_LIST", "3")
3 changes: 3 additions & 0 deletions back/config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,10 @@
urlpatterns = [
*private_api_patterns,
*di_api_patterns,
# anciennes routes Inclusion-Connect (en attente de suppression)
*oidc_patterns,
# nouvelles routes OIDC pour ProConnect
path("oidc/", include("mozilla_django_oidc.urls")),
]

if settings.PROFILE:
Expand Down
124 changes: 124 additions & 0 deletions back/dora/oidc/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,126 @@
from logging import getLogger

import requests
from django.core.exceptions import SuspiciousOperation
from mozilla_django_oidc.auth import (
OIDCAuthenticationBackend as MozillaOIDCAuthenticationBackend,
)
from rest_framework.authtoken.models import Token

from dora.users.models import User

logger = getLogger(__name__)


class OIDCError(Exception):
"""Exception générique pour les erreurs OIDC"""


class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
def get_userinfo(self, access_token, id_token, payload):
# Surcharge de la récupération des informations utilisateur:
# le décodage JSON du contenu JWT pose problème avec ProConnect
# qui le retourne en format binaire (content-type: application/jwt)
# d'où ce petit hack.
# Inspiré de : https://github.com/numerique-gouv/people/blob/b637774179d94cecb0ef2454d4762750a6a5e8c0/src/backend/core/authentication/backends.py#L47C1-L47C57
user_response = requests.get(
self.OIDC_OP_USER_ENDPOINT,
headers={"Authorization": "Bearer {0}".format(access_token)},
verify=self.get_settings("OIDC_VERIFY_SSL", True),
timeout=self.get_settings("OIDC_TIMEOUT", None),
proxies=self.get_settings("OIDC_PROXY", None),
)
user_response.raise_for_status()

try:
# cas où le type du token JWT est `application/json`
return user_response.json()
except requests.exceptions.JSONDecodeError:
# sinon, on présume qu'il s'agit d'un token JWT au format `application/jwt` (+...)
# comme c'est le cas pour ProConnect.
return self.verify_token(user_response.text)

# Pas nécessaire de surcharger `get_or_create_user` puisque sur DORA,
# les utilisateurs ont un e-mail unique qui leur sert de `username`.

def create_user(self, claims):
# on peut à la rigueur se passer de certains élements contenus dans les claims,
# mais pas de ceux-là :
email, sub = claims.get("email"), claims.get("sub")
if not email:
raise SuspiciousOperation(
"L'adresse e-mail n'est pas incluse dans les `claims`"
)

if not sub:
raise SuspiciousOperation(
"Le sujet (`sub`) n'est pas inclus dans les `claims`"
)

# L'utilisateur est créé sans mot de passe (aucune connexion à l'admin),
# et comme venant de ProConnect, on considère l'e-mail vérifié.
new_user = self.UserModel.objects.create_user(
email,
sub_pc=sub,
first_name=claims.get("given_name", "N/D"),
last_name=claims.get("usual_name", "N/D"),
is_valid=True,
)

# recupération du code SAFIR :
# même pour l'instant inutilisé, on pourra par la suite le passer au frontend
# pour rattachement direct à une agence France Travail
if custom := claims.get("custom"):
code_safir = custom.get("structureTravail") # noqa F481
# TODO: une fois le code SAFIR récupéré, voir quoi en faire (redirection vers un rattachement)

# compatibilité :
# durant la phase de migration vers ProConnect on ne replace *que* le fournisseur d'identité,
# et on ne touche pas aux mécanismes d'identification entre back et front.
self.get_or_create_drf_token(new_user)

return new_user

def update_user(self, user, claims):
# L'utilisateur peut déjà étre inscrit à IC, dans ce cas on réutilise la plupart
# des informations déjà connues
sub = claims.get("sub")

if not sub:
raise SuspiciousOperation(
"Le sujet (`sub`) n'est pas inclu dans les `claims`"
)

if user.sub_pc and str(user.sub_pc) != sub:
raise SuspiciousOperation(
"Le sub enregistré est différent de celui fourni par ProConnect"
)

if not user.sub_pc:
# utilisateur existant, mais non-enregistré sur ProConnect
user.sub_pc = sub
user.save()

return user

def get_user(self, user_id):
if user := super().get_user(user_id):
self.get_or_create_drf_token(user)
return user
return None

def get_or_create_drf_token(self, user_email):
# Pour être temporairement compatible, on crée un token d'identification DRF lié au nouvel utilisateur.
if not user_email:
raise SuspiciousOperation(
"Utilisateur non renseigné pour la création du token DRF"
)

user = User.objects.get(email=user_email)

token, created = Token.objects.get_or_create(user=user)

if created:
logger.info("Initialisation du token DRF pour l'utilisateur %s", user_email)

return token
13 changes: 13 additions & 0 deletions back/dora/oidc/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from django.apps import AppConfig

"""
dora.oidc:
Gère les connexions OIDC-Connect via ProConnect.
Basée sur un provider custom de django-allauth.
Remplace l'ancien système de connexion à Inclusion-Connect à partir de novembre 2024.
"""


class OIDCConfig(AppConfig):
name = "dora.oidc"
verbose_name = "Gestion des connexions ProConnect"
21 changes: 20 additions & 1 deletion back/dora/oidc/urls.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import mozilla_django_oidc.urls # noqa: F401
from django.urls import path

import dora.oidc.views as views

oidc_patterns = [
inclusion_connect_patterns = [
path(
"inclusion-connect-get-login-info/",
views.inclusion_connect_get_login_info,
Expand All @@ -20,3 +21,21 @@
views.inclusion_connect_authenticate,
),
]

proconnect_patterns = [
# les patterns internes pour le callback et le logout sont définis
# dans le fichier `urls.py` de mozilla_django_oidc
# redirection vers ProConnect pour la connexion
path("oidc/login/", views.oidc_login, name="oidc_login"),
# redirection une fois la connexion terminée
path("oidc/logged_in/", views.oidc_logged_in, name="oidc_logged_in"),
# preparation au logout : 2 étapes nécessaires
# l'une de déconnexion sur ProConnect, l'autre locale de destruction de la session active
path("oidc/pre_logout/", views.oidc_pre_logout, name="oidc_pre_logout"),
# la plupart des vues de `mozilla-django-oidc` sont paramètrables
# pas le logout
path("oidc/logout/", views.CustomLogoutView.as_view(), name="oidc_logout"),
]


oidc_patterns = inclusion_connect_patterns + proconnect_patterns
Loading
Loading