From 2b821d6ee2ac30b9bf67e35fc3c3fe594bbebc57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Haasser?= Date: Mon, 28 Oct 2024 17:30:02 +0100 Subject: [PATCH 1/5] Add package mozilla-django-oidc --- pyproject.toml | 3 +-- requirements-dev.txt | 20 +++++++++++++++++++- requirements.txt | 20 +++++++++++++++++++- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 861ef93..0e4d3bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,9 +17,8 @@ dependencies = [ "django-dsfr", "django-referrer-policy", "django-csp", + "mozilla-django-oidc", ] -# to add someday: -# django-dsfr [tool.setuptools] packages = ["gsl"] diff --git a/requirements-dev.txt b/requirements-dev.txt index 7238550..5978c44 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -19,6 +19,8 @@ certifi==2024.8.30 # via # requests # sentry-sdk +cffi==1.17.1 + # via cryptography cfgv==3.4.0 # via pre-commit chardet==5.2.0 @@ -41,6 +43,11 @@ coverage[toml]==7.6.1 # via pytest-cov cron-descriptor==1.4.5 # via django-celery-beat +cryptography==43.0.3 + # via + # josepy + # mozilla-django-oidc + # pyopenssl diff-cover==9.2.0 # via gsl (pyproject.toml) distlib==0.3.8 @@ -58,6 +65,7 @@ django==5.1.1 # django-referrer-policy # django-timezone-field # gsl (pyproject.toml) + # mozilla-django-oidc django-celery-beat==2.7.0 # via gsl (pyproject.toml) django-celery-results==2.5.1 @@ -88,10 +96,14 @@ iniconfig==2.0.0 # via pytest jinja2==3.1.4 # via diff-cover +josepy==1.14.0 + # via mozilla-django-oidc kombu==5.4.2 # via celery markupsafe==2.1.5 # via jinja2 +mozilla-django-oidc==4.0.1 + # via gsl (pyproject.toml) nodeenv==1.9.1 # via pre-commit packaging==24.1 @@ -108,8 +120,12 @@ prompt-toolkit==3.0.48 # via click-repl psycopg2-binary==2.9.9 # via gsl (pyproject.toml) +pycparser==2.22 + # via cffi pygments==2.18.0 # via diff-cover +pyopenssl==24.2.1 + # via josepy pytest==8.3.3 # via # gsl (pyproject.toml) @@ -138,7 +154,9 @@ pyyaml==6.0.2 redis==5.1.0 # via gsl (pyproject.toml) requests==2.32.3 - # via django-dsfr + # via + # django-dsfr + # mozilla-django-oidc ruff==0.6.8 # via # gsl (pyproject.toml) diff --git a/requirements.txt b/requirements.txt index 51e3a95..0bf2d98 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,6 +19,8 @@ certifi==2024.8.30 # via # requests # sentry-sdk +cffi==1.17.1 + # via cryptography charset-normalizer==3.3.2 # via requests click==8.1.7 @@ -35,6 +37,11 @@ click-repl==0.3.0 # via celery cron-descriptor==1.4.5 # via django-celery-beat +cryptography==43.0.3 + # via + # josepy + # mozilla-django-oidc + # pyopenssl dj-database-url==2.2.0 # via gsl (pyproject.toml) django==5.1.1 @@ -48,6 +55,7 @@ django==5.1.1 # django-referrer-policy # django-timezone-field # gsl (pyproject.toml) + # mozilla-django-oidc django-celery-beat==2.7.0 # via gsl (pyproject.toml) django-celery-results==2.5.1 @@ -66,12 +74,20 @@ django-widget-tweaks==1.5.0 # via django-dsfr idna==3.10 # via requests +josepy==1.14.0 + # via mozilla-django-oidc kombu==5.4.2 # via celery +mozilla-django-oidc==4.0.1 + # via gsl (pyproject.toml) prompt-toolkit==3.0.48 # via click-repl psycopg2-binary==2.9.9 # via gsl (pyproject.toml) +pycparser==2.22 + # via cffi +pyopenssl==24.2.1 + # via josepy python-crontab==3.2.0 # via django-celery-beat python-dateutil==2.9.0.post0 @@ -83,7 +99,9 @@ python-dotenv==1.0.1 redis==5.1.0 # via gsl (pyproject.toml) requests==2.32.3 - # via django-dsfr + # via + # django-dsfr + # mozilla-django-oidc sentry-sdk==2.15.0 # via gsl (pyproject.toml) six==1.16.0 From 017dcb15783609ada1a5f35ef91d68dcf9edc2dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Haasser?= Date: Mon, 28 Oct 2024 17:58:57 +0100 Subject: [PATCH 2/5] Config of OIDC process --- .env.example | 13 ++++++++++++- gsl/settings.py | 25 ++++++++++++++++++++++++- gsl/urls.py | 17 ++++++----------- gsl_core/templates/blocks/header.html | 12 ++++++++++++ 4 files changed, 54 insertions(+), 13 deletions(-) diff --git a/.env.example b/.env.example index 10430d6..ff4b920 100644 --- a/.env.example +++ b/.env.example @@ -13,4 +13,15 @@ DATABASE_PORT=5432 DATABASE_URL=postgres://${DATABASE_USER}:${DATABASE_PASSWORD}@${DATABASE_HOST}:${DATABASE_PORT}/${DATABASE_NAME} # see https://doc.demarches-simplifiees.fr/api-graphql/jeton-dauthentification -DS_API_TOKEN= \ No newline at end of file +DS_API_TOKEN= + +# Pro Connect / Agent Connect - see colleagues +PROCONNECT_CLIENT_ID= +PROCONNECT_CLIENT_SECRET= +PROCONNECT_DOMAIN="fca.integ01.dev-agentconnect.fr" + +PROCONNECT_AUTHORIZATION_ENDPOINT="https://fca.integ01.dev-agentconnect.fr/api/v2/authorize" +PROCONNECT_TOKEN_ENDPOINT="https://fca.integ01.dev-agentconnect.fr/api/v2/token" +PROCONNECT_USER_ENDPOINT="https://fca.integ01.dev-agentconnect.fr/api/v2/userinfo" +PROCONNECT_JWKS_ENDPOINT="https://fca.integ01.dev-agentconnect.fr/api/v2/jwks" +PROCONNECT_SESSION_END="https://fca.integ01.dev-agentconnect.fr/session/end" \ No newline at end of file diff --git a/gsl/settings.py b/gsl/settings.py index b746ad7..fe32dae 100644 --- a/gsl/settings.py +++ b/gsl/settings.py @@ -59,6 +59,7 @@ INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", + "mozilla_django_oidc", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", @@ -83,6 +84,10 @@ "django.middleware.clickjacking.XFrameOptionsMiddleware", ] +AUTHENTICATION_BACKENDS = [ + "mozilla_django_oidc.auth.OIDCAuthenticationBackend", + "django.contrib.auth.backends.ModelBackend", +] AUTH_USER_MODEL = "gsl_core.Collegue" ROOT_URLCONF = "gsl.urls" LANGUAGE_CODE = "fr" @@ -150,7 +155,7 @@ # Internationalization # https://docs.djangoproject.com/en/4.1/topics/i18n/ -LANGUAGE_CODE = "en-us" +LANGUAGE_CODE = "fr-fr" TIME_ZONE = "UTC" @@ -174,3 +179,21 @@ DS_API_URL = os.getenv( "DS_API_URL", "https://www.demarches-simplifiees.fr/api/v2/graphql" ) + +# Connection to "Pro Connect" (OIDC) + +OIDC_RP_SIGN_ALGO = "RS256" +OIDC_OP_JWKS_ENDPOINT = os.getenv("PROCONNECT_JWKS_ENDPOINT") +OIDC_RP_CLIENT_ID = os.getenv("PROCONNECT_CLIENT_ID") +OIDC_RP_CLIENT_SECRET = os.getenv("PROCONNECT_CLIENT_SECRET") + +OIDC_OP_AUTHORIZATION_ENDPOINT = os.getenv("PROCONNECT_AUTHORIZATION_ENDPOINT") +OIDC_OP_TOKEN_ENDPOINT = os.getenv("PROCONNECT_TOKEN_ENDPOINT") +OIDC_OP_USER_ENDPOINT = os.getenv("PROCONNECT_USER_ENDPOINT") + +OIDC_OP_LOGOUT_ENDPOINT = os.getenv("PROCONNECT_SESSION_END") + +OIDC_AUTH_REQUEST_EXTRA_PARAMS = {"acr_values": "eidas1"} +OIDC_RENEW_ID_TOKEN_EXPIRY_SECONDS = 4 * 60 * 60 +OIDC_STORE_ID_TOKEN = True +ALLOW_LOGOUT_GET_METHOD = True diff --git a/gsl/urls.py b/gsl/urls.py index fe04c35..ccbc072 100644 --- a/gsl/urls.py +++ b/gsl/urls.py @@ -2,19 +2,14 @@ The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/4.1/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: path('', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.urls import include, path - 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) + """ from django.contrib import admin from django.urls import include, path -urlpatterns = [path("admin/", admin.site.urls), path("", include("gsl_pages.urls"))] +urlpatterns = [ + path("admin/", admin.site.urls), + path("", include("gsl_pages.urls")), + path("oidc/", include("mozilla_django_oidc.urls")), +] diff --git a/gsl_core/templates/blocks/header.html b/gsl_core/templates/blocks/header.html index 50c155c..52e6feb 100644 --- a/gsl_core/templates/blocks/header.html +++ b/gsl_core/templates/blocks/header.html @@ -10,6 +10,18 @@ {% translate "Display settings" %} +
  • + {% if user.is_authenticated %} +
    + {% csrf_token %} + +
    + {% else %} + + Se connecter + + {% endif %} +
  • {% endblock header_tools %} {% block operator_logo %} From a2821346f35006dbe1e3b76f65c71779f92c6223 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Haasser?= Date: Wed, 30 Oct 2024 11:29:16 +0100 Subject: [PATCH 3/5] Use a custom oidc backend to fix known issue --- gsl/settings.py | 3 ++- gsl_core/templates/blocks/header.html | 1 + gsl_oidc/__init__.py | 0 gsl_oidc/admin.py | 1 + gsl_oidc/apps.py | 6 +++++ gsl_oidc/backends.py | 33 +++++++++++++++++++++++++++ gsl_oidc/migrations/__init__.py | 0 gsl_oidc/views.py | 1 + 8 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 gsl_oidc/__init__.py create mode 100644 gsl_oidc/admin.py create mode 100644 gsl_oidc/apps.py create mode 100644 gsl_oidc/backends.py create mode 100644 gsl_oidc/migrations/__init__.py create mode 100644 gsl_oidc/views.py diff --git a/gsl/settings.py b/gsl/settings.py index fe32dae..b9b6da4 100644 --- a/gsl/settings.py +++ b/gsl/settings.py @@ -85,9 +85,10 @@ ] AUTHENTICATION_BACKENDS = [ - "mozilla_django_oidc.auth.OIDCAuthenticationBackend", + "gsl_oidc.backends.OIDCAuthenticationBackend", "django.contrib.auth.backends.ModelBackend", ] + AUTH_USER_MODEL = "gsl_core.Collegue" ROOT_URLCONF = "gsl.urls" LANGUAGE_CODE = "fr" diff --git a/gsl_core/templates/blocks/header.html b/gsl_core/templates/blocks/header.html index 52e6feb..6305109 100644 --- a/gsl_core/templates/blocks/header.html +++ b/gsl_core/templates/blocks/header.html @@ -12,6 +12,7 @@
  • {% if user.is_authenticated %} + {{ user.email }}
    {% csrf_token %} diff --git a/gsl_oidc/__init__.py b/gsl_oidc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gsl_oidc/admin.py b/gsl_oidc/admin.py new file mode 100644 index 0000000..846f6b4 --- /dev/null +++ b/gsl_oidc/admin.py @@ -0,0 +1 @@ +# Register your models here. diff --git a/gsl_oidc/apps.py b/gsl_oidc/apps.py new file mode 100644 index 0000000..03a4c2c --- /dev/null +++ b/gsl_oidc/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class GslOidcConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "gsl_oidc" diff --git a/gsl_oidc/backends.py b/gsl_oidc/backends.py new file mode 100644 index 0000000..c461369 --- /dev/null +++ b/gsl_oidc/backends.py @@ -0,0 +1,33 @@ +from logging import getLogger + +import requests +from mozilla_django_oidc.auth import ( + OIDCAuthenticationBackend as MozillaOIDCAuthenticationBackend, +) + +logger = getLogger(__name__) + + +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 JWT (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) diff --git a/gsl_oidc/migrations/__init__.py b/gsl_oidc/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gsl_oidc/views.py b/gsl_oidc/views.py new file mode 100644 index 0000000..60f00ef --- /dev/null +++ b/gsl_oidc/views.py @@ -0,0 +1 @@ +# Create your views here. From 71032d545311682edfc1e0eb00d6cfb162346c75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Haasser?= Date: Wed, 30 Oct 2024 11:51:48 +0100 Subject: [PATCH 4/5] Add two missing parameters to avoid 404 errors --- gsl/settings.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/gsl/settings.py b/gsl/settings.py index b9b6da4..57e361c 100644 --- a/gsl/settings.py +++ b/gsl/settings.py @@ -181,6 +181,11 @@ "DS_API_URL", "https://www.demarches-simplifiees.fr/api/v2/graphql" ) +# Redirect after login/logout - used by OIDC backends + +LOGIN_REDIRECT_URL = "/" +LOGOUT_REDIRECT_URL = "/" + # Connection to "Pro Connect" (OIDC) OIDC_RP_SIGN_ALGO = "RS256" From 00b52c1a4d30ac95ca077cc06047fb2d5228f5b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Haasser?= Date: Wed, 30 Oct 2024 11:58:30 +0100 Subject: [PATCH 5/5] Add available scopes for better matching of user data --- gsl/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gsl/settings.py b/gsl/settings.py index 57e361c..0613e4a 100644 --- a/gsl/settings.py +++ b/gsl/settings.py @@ -192,7 +192,7 @@ OIDC_OP_JWKS_ENDPOINT = os.getenv("PROCONNECT_JWKS_ENDPOINT") OIDC_RP_CLIENT_ID = os.getenv("PROCONNECT_CLIENT_ID") OIDC_RP_CLIENT_SECRET = os.getenv("PROCONNECT_CLIENT_SECRET") - +OIDC_RP_SCOPES = "openid email given_name usual_name uid siret idp_id" OIDC_OP_AUTHORIZATION_ENDPOINT = os.getenv("PROCONNECT_AUTHORIZATION_ENDPOINT") OIDC_OP_TOKEN_ENDPOINT = os.getenv("PROCONNECT_TOKEN_ENDPOINT") OIDC_OP_USER_ENDPOINT = os.getenv("PROCONNECT_USER_ENDPOINT")