Skip to content

Commit

Permalink
🛂(dashboard) add Pro Connect (OIDC) integration with mozilla-django-o…
Browse files Browse the repository at this point in the history
…idc library

Integrated Pro Connect (OIDC) support using mozilla-django-oidc.
Configured authentication backends, middleware, and endpoints for secure OIDC authentication.
Added support for Pro Connect (OIDC provider) with necessary settings and URL patterns.
Added `siret` field to the model user.
  • Loading branch information
ssorin committed Feb 6, 2025
1 parent dcf2970 commit 34ac66b
Show file tree
Hide file tree
Showing 12 changed files with 450 additions and 128 deletions.
10 changes: 10 additions & 0 deletions env.d/dashboard
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ DJANGO_SUPERUSER_PASSWORD=admin
DJANGO_SUPERUSER_USERNAME=admin
[email protected]

# Pro Connect
DASHBOARD_PROCONNECT_CLIENT_ID=
DASHBOARD_PROCONNECT_CLIENT_SECRET=
DASHBOARD_PROCONNECT_DOMAIN="fca.integ01.dev-agentconnect.fr"
DASHBOARD_PROCONNECT_AUTHORIZATION_ENDPOINT="https://fca.integ01.dev-agentconnect.fr/api/v2/authorize"
DASHBOARD_PROCONNECT_TOKEN_ENDPOINT="https://fca.integ01.dev-agentconnect.fr/api/v2/token"
DASHBOARD_PROCONNECT_USER_ENDPOINT="https://fca.integ01.dev-agentconnect.fr/api/v2/userinfo"
DASHBOARD_PROCONNECT_JWKS_ENDPOINT="https://fca.integ01.dev-agentconnect.fr/api/v2/jwks"
DASHBOARD_PROCONNECT_SESSION_END="https://fca.integ01.dev-agentconnect.fr/session/end"

# Control authority contact
DASHBOARD_CONTROL_AUTHORITY_NAME=QualiCharge
DASHBOARD_CONTROL_AUTHORITY_ADDRESS_1=1 rue de test
Expand Down
3 changes: 3 additions & 0 deletions src/dashboard/Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ django-extensions = "==3.2.3"
django-stubs = {extras = ["compatible-mypy"], version = "==5.1.2"}
gunicorn = "==23.0.0"
jsonschema = "==4.23.0"
mozilla-django-oidc = "==4.0.1"
psycopg = {extras = ["pool", "binary"], version = "==3.2.4"}
requests = "==2.32.3"
sentry-sdk = {extras = ["django"], version = "==2.20.0"}
types-requests = "==2.32.0.20241016"
whitenoise = "==6.8.2"

[dev-packages]
Expand Down
168 changes: 158 additions & 10 deletions src/dashboard/Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 19 additions & 1 deletion src/dashboard/apps/auth/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.utils.translation import gettext_lazy as _

from .models import DashboardUser

Expand All @@ -10,4 +11,21 @@
class DashboardUserAdmin(UserAdmin):
"""Dashboard user admin based on UserAdmin."""

pass
fieldsets = (
(None, {"fields": ("username", "password")}),
(_("Personal info"), {"fields": ("first_name", "last_name", "email")}),
(_("ProConnect"), {"fields": ("siret",)}),
(
_("Permissions"),
{
"fields": (
"is_active",
"is_staff",
"is_superuser",
"groups",
"user_permissions",
),
},
),
(_("Important dates"), {"fields": ("last_login", "date_joined")}),
)
72 changes: 72 additions & 0 deletions src/dashboard/apps/auth/backends.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""Dashboard auth middleware."""

import requests
from mozilla_django_oidc.auth import (
OIDCAuthenticationBackend as MozillaOIDCAuthenticationBackend,
)
from mozilla_django_oidc.auth import default_username_algo


class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
"""Override mozilla_django_oidc's authentication."""

# Bluntly stolen from betagouv/gestion-des-subventions-locales.
# Thanks to Agnès Haasser for the tip.
# https://github.com/betagouv/gestion-des-subventions-locales/blob/develop/gsl_oidc/backends.py

def get_userinfo(self, access_token, id_token, payload):
"""Return user details dictionary.
Overridden original method to allow ProConnect tokens to be decoded:
JSON decoding of JWT content is problematic with ProConnect,
which returns it in JWT format (content-type: application/jwt)
"""
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:
# default case: JWT token is `application/json`
return user_response.json()
except requests.exceptions.JSONDecodeError:
# if except, it is assumed to be a JWT token in `application/jwt` format
# as happens for ProConnect.
return self.verify_token(user_response.text)

def get_data_for_user_create_and_update(self, claims):
"""Return data for user creation and update."""
return {
"email": claims.get("email"),
"first_name": claims.get("given_name", ""),
"last_name": claims.get("usual_name", ""),
"siret": claims.get("siret", ""),
}

def filter_users_by_claims(self, claims):
"""Return all users matching the specified username."""
username = self.get_username(claims)
return self.UserModel.objects.filter(username=username)

def create_user(self, claims):
"""Return object for a newly created user account."""
username = self.get_username(claims)
return self.UserModel.objects.create_user(
username, **self.get_data_for_user_create_and_update(claims)
)

def update_user(self, user, claims):
"""Update existing user with new claims, if necessary save, and return user."""
for key, value in self.get_data_for_user_create_and_update(claims).items():
if value:
user.__setattr__(key, value)
user.save()
return user

def get_username(self, claims):
"""Generate username based on claims."""
return default_username_algo(claims.get("sub"))
Loading

0 comments on commit 34ac66b

Please sign in to comment.