Skip to content
This repository has been archived by the owner on Oct 22, 2024. It is now read-only.

Migration vers ProConnect #325

Open
wants to merge 8 commits into
base: main
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
72 changes: 69 additions & 3 deletions 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 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 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
118 changes: 118 additions & 0 deletions dora/oidc/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,120 @@
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

if not user.sub_pc:
# utilisateur existant, mais non-enregistré sur ProConnect
sub = claims.get("sub")
if not sub:
raise SuspiciousOperation(
"Le sujet (`sub`) n'est pas inclu dans les `claims`"
)
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 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 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