diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f0867b97f4..e35a74295a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -166,14 +166,6 @@ jobs: setup-node: 'yes' npm-ci-flags: '--legacy-peer-deps' - # See https://playwright.dev/python/docs/ci#caching-browsers - - name: Cache Playwright browser - id: cache-browser - uses: actions/cache@v3 - with: - path: /home/runner/.cache/ms-playwright - key: ${{ runner.os }}-${{ matrix.browser }}-playwright-${{ hashFiles('requirements/ci.txt') }} - - name: Install playwright deps run: playwright install --with-deps ${{ matrix.browser }} diff --git a/README.rst b/README.rst index e479d42468..b6c1a41a10 100644 --- a/README.rst +++ b/README.rst @@ -3,7 +3,7 @@ Open Inwoner ================== -:Version: 1.21.0 +:Version: 1.22.0 :Source: https://github.com/maykinmedia/open-inwoner :Documentation: https://docs.openinwoner.nl :PythonVersion: 3.11 diff --git a/docs/configuration/admin_oidc.rst b/docs/configuration/admin_oidc.rst index 366c3ca0b5..5221f51c69 100644 --- a/docs/configuration/admin_oidc.rst +++ b/docs/configuration/admin_oidc.rst @@ -36,7 +36,6 @@ All settings: ADMIN_OIDC_DEFAULT_GROUPS ADMIN_OIDC_GROUPS_CLAIM ADMIN_OIDC_MAKE_USERS_STAFF - ADMIN_OIDC_OIDC_EXEMPT_URLS ADMIN_OIDC_OIDC_NONCE_SIZE ADMIN_OIDC_OIDC_OP_AUTHORIZATION_ENDPOINT ADMIN_OIDC_OIDC_OP_DISCOVERY_ENDPOINT @@ -65,12 +64,12 @@ Detailed Information Setting claim mapping Description Mapping from user-model fields to OIDC claims Possible values Mapping: {'some_key': 'Some value'} - Default value {'email': 'email', 'first_name': 'given_name', 'last_name': 'family_name'} + Default value {'email': ['email'], 'first_name': ['given_name'], 'last_name': ['family_name']} Variable ADMIN_OIDC_GROUPS_CLAIM Setting groups claim Description The name of the OIDC claim that holds the values to map to local user groups. - Possible values string + Possible values No information available Default value roles Variable ADMIN_OIDC_MAKE_USERS_STAFF @@ -79,12 +78,6 @@ Detailed Information Possible values True, False Default value False - Variable ADMIN_OIDC_OIDC_EXEMPT_URLS - Setting URLs exempt from session renewal - Description This is a list of absolute url paths, regular expressions for url paths, or Django view names. This plus the mozilla-django-oidc urls are exempted from the session renewal by the SessionRefresh middleware. - Possible values string, comma-delimited ('foo,bar,baz') - Default value - Variable ADMIN_OIDC_OIDC_NONCE_SIZE Setting Nonce size Description Sets the length of the random string used for OpenID Connect nonce verification @@ -190,5 +183,5 @@ Detailed Information Variable ADMIN_OIDC_USERNAME_CLAIM Setting username claim Description The name of the OIDC claim that is used as the username - Possible values string + Possible values No information available Default value sub diff --git a/docs/configuration/digid_oidc.rst b/docs/configuration/digid_oidc.rst index 9e726395da..7215d1b8d7 100644 --- a/docs/configuration/digid_oidc.rst +++ b/docs/configuration/digid_oidc.rst @@ -31,10 +31,8 @@ All settings: :: + DIGID_OIDC_BSN_CLAIM DIGID_OIDC_ENABLED - DIGID_OIDC_ERROR_MESSAGE_MAPPING - DIGID_OIDC_IDENTIFIER_CLAIM_NAME - DIGID_OIDC_OIDC_EXEMPT_URLS DIGID_OIDC_OIDC_KEYCLOAK_IDP_HINT DIGID_OIDC_OIDC_NONCE_SIZE DIGID_OIDC_OIDC_OP_AUTHORIZATION_ENDPOINT @@ -57,30 +55,18 @@ Detailed Information :: + Variable DIGID_OIDC_BSN_CLAIM + Setting BSN-claim + Description Naam van de claim die het BSN bevat van de ingelogde gebruiker. + Possible values No information available + Default value bsn + Variable DIGID_OIDC_ENABLED Setting inschakelen - Description Geeft aan of OpenID Connect voor authenticatie/autorisatie is ingeschakeld. Deze overschrijft het gebruik van SAML voor DigiD-authenticatie. + Description Indicates whether OpenID Connect for authentication/authorization is enabled Possible values True, False Default value False - Variable DIGID_OIDC_ERROR_MESSAGE_MAPPING - Setting Foutmelding mapping - Description Mapping die de door de identiteitsprovider geretourneerde foutmeldingen, omzet in leesbare meldingen die aan de gebruiker worden getoond - Possible values Mapping: {'some_key': 'Some value'} - Default value {} - - Variable DIGID_OIDC_IDENTIFIER_CLAIM_NAME - Setting BSN claim naam - Description De naam van de claim waarin het BSN nummer van de gebruiker is opgeslagen - Possible values string - Default value bsn - - Variable DIGID_OIDC_OIDC_EXEMPT_URLS - Setting URLs exempt from session renewal - Description This is a list of absolute url paths, regular expressions for url paths, or Django view names. This plus the mozilla-django-oidc urls are exempted from the session renewal by the SessionRefresh middleware. - Possible values No information available - Default value - Variable DIGID_OIDC_OIDC_KEYCLOAK_IDP_HINT Setting Keycloak-identiteitsprovider hint Description Specifiek voor Keycloak: parameter die aangeeft welke identiteitsprovider gebruikt moet worden (inlogscherm van Keycloak overslaan). @@ -149,7 +135,7 @@ Detailed Information Variable DIGID_OIDC_OIDC_RP_SCOPES_LIST Setting OpenID Connect scopes - Description OpenID Connect-scopes die worden bevraagd tijdens het inloggen. Deze zijn hardcoded en moeten worden ondersteund door de identiteitsprovider. + Description OpenID Connect scopes that are requested during login. These scopes are hardcoded and must be supported by the identity provider. Possible values No information available Default value openid, bsn diff --git a/docs/configuration/eherkenning_oidc.rst b/docs/configuration/eherkenning_oidc.rst index 933c2a63e4..ed312af924 100644 --- a/docs/configuration/eherkenning_oidc.rst +++ b/docs/configuration/eherkenning_oidc.rst @@ -32,9 +32,7 @@ All settings: :: EHERKENNING_OIDC_ENABLED - EHERKENNING_OIDC_ERROR_MESSAGE_MAPPING - EHERKENNING_OIDC_IDENTIFIER_CLAIM_NAME - EHERKENNING_OIDC_OIDC_EXEMPT_URLS + EHERKENNING_OIDC_LEGAL_SUBJECT_CLAIM EHERKENNING_OIDC_OIDC_KEYCLOAK_IDP_HINT EHERKENNING_OIDC_OIDC_NONCE_SIZE EHERKENNING_OIDC_OIDC_OP_AUTHORIZATION_ENDPOINT @@ -59,27 +57,15 @@ Detailed Information Variable EHERKENNING_OIDC_ENABLED Setting inschakelen - Description Geeft aan of OpenID Connect voor authenticatie/autorisatie is ingeschakeld. Deze heeft voorrang op het gebruik van SAML voor eHerkenning-authenticatie. + Description Indicates whether OpenID Connect for authentication/authorization is enabled Possible values True, False Default value False - Variable EHERKENNING_OIDC_ERROR_MESSAGE_MAPPING - Setting Foutmelding mapping - Description Mapping die de door de identiteitsprovider geretourneerde foutmeldingen, omzet in leesbare meldingen die aan de gebruiker worden getoond - Possible values Mapping: {'some_key': 'Some value'} - Default value {} - - Variable EHERKENNING_OIDC_IDENTIFIER_CLAIM_NAME - Setting KVK claim naam - Description De naam van de claim waarin het KVK nummer van de gebruiker is opgeslagen - Possible values string - Default value kvk - - Variable EHERKENNING_OIDC_OIDC_EXEMPT_URLS - Setting URLs exempt from session renewal - Description This is a list of absolute url paths, regular expressions for url paths, or Django view names. This plus the mozilla-django-oidc urls are exempted from the session renewal by the SessionRefresh middleware. - Possible values string, comma-delimited ('foo,bar,baz') - Default value + Variable EHERKENNING_OIDC_LEGAL_SUBJECT_CLAIM + Setting bedrijfsidenticatie-claim + Description Naam van de claim die de identificatie van het ingelogde/vertegenwoordigde bedrijf bevat. + Possible values No information available + Default value urn:etoegang:core:LegalSubjectID Variable EHERKENNING_OIDC_OIDC_KEYCLOAK_IDP_HINT Setting Keycloak-identiteitsprovider hint @@ -149,7 +135,7 @@ Detailed Information Variable EHERKENNING_OIDC_OIDC_RP_SCOPES_LIST Setting OpenID Connect scopes - Description OpenID Connect-scopes die worden bevraagd tijdens het inloggen. Deze zijn hardcoded en moeten worden ondersteund door de identiteitsprovider. + Description OpenID Connect scopes that are requested during login. These scopes are hardcoded and must be supported by the identity provider. Possible values string, comma-delimited ('foo,bar,baz') Default value openid, kvk diff --git a/docs/configuration/eherkenning_saml.rst b/docs/configuration/eherkenning_saml.rst index 41f9f6e68f..80ed4d896f 100644 --- a/docs/configuration/eherkenning_saml.rst +++ b/docs/configuration/eherkenning_saml.rst @@ -134,7 +134,7 @@ Detailed Information Variable EHERKENNING_SAML_EH_LOA Setting eHerkenning LoA - Description Level of Assurance (LoA) to use for the eHerkenning service. + Description Betrouwbaarheidsniveau (LoA) voor de eHerkenningservice. Possible values urn:etoegang:core:assurance-class:loa1, urn:etoegang:core:assurance-class:loa2, urn:etoegang:core:assurance-class:loa2plus, urn:etoegang:core:assurance-class:loa3, urn:etoegang:core:assurance-class:loa4 Default value urn:etoegang:core:assurance-class:loa3 @@ -164,7 +164,7 @@ Detailed Information Variable EHERKENNING_SAML_EIDAS_LOA Setting eIDAS LoA - Description Level of Assurance (LoA) to use for the eIDAS service. + Description Betrouwbaarheidsniveau (LoA) voor de eIDAS-service. Possible values urn:etoegang:core:assurance-class:loa1, urn:etoegang:core:assurance-class:loa2, urn:etoegang:core:assurance-class:loa2plus, urn:etoegang:core:assurance-class:loa3, urn:etoegang:core:assurance-class:loa4 Default value urn:etoegang:core:assurance-class:loa3 diff --git a/requirements/base.in b/requirements/base.in index 0855ed7173..b50e507f29 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -79,7 +79,7 @@ elastic-apm # Elastic APM integration beautifulsoup4 # DigidLocal -django-digid-eherkenning +django-digid-eherkenning[oidc] maykin-python3-saml pyopenssl django-sessionprofile diff --git a/requirements/base.txt b/requirements/base.txt index fe1268cae5..72eacf0fe0 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -190,7 +190,7 @@ django-csp==3.7 # via -r requirements/base.in django-csp-reports==1.8.1 # via -r requirements/base.in -django-digid-eherkenning==0.13.1 +django-digid-eherkenning[oidc]==0.16.0 # via -r requirements/base.in django-elasticsearch-dsl==7.4 # via -r requirements/base.in @@ -273,7 +273,7 @@ django-solo==2.2.0 # mozilla-django-oidc-db # notifications-api-common # zgw-consumers -django-timeline-logger==3.0.0 +django-timeline-logger==4.0.0 # via -r requirements/base.in django-timezone-field==6.1.0 # via django-celery-beat @@ -403,10 +403,12 @@ maykin-python3-saml==1.16.1 # django-digid-eherkenning messagebird==2.1.0 # via -r requirements/base.in -mozilla-django-oidc==2.0.0 +mozilla-django-oidc==4.0.1 # via mozilla-django-oidc-db -mozilla-django-oidc-db==0.14.1 - # via -r requirements/base.in +mozilla-django-oidc-db==0.19.0 + # via + # -r requirements/base.in + # django-digid-eherkenning notifications-api-common==0.2.2 # via -r requirements/base.in oath==1.4.4 @@ -544,6 +546,7 @@ tinycss2==1.1.1 typing-extensions==4.10.0 # via # -r requirements/base.in + # mozilla-django-oidc-db # pydantic # pydantic-core # pyee diff --git a/requirements/ci.txt b/requirements/ci.txt index 0d8eb2936b..cbdfbbeb18 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -314,10 +314,11 @@ django-csp-reports==1.8.1 # via # -c requirements/base.txt # -r requirements/base.txt -django-digid-eherkenning==0.13.1 +django-digid-eherkenning[oidc]==0.16.0 # via # -c requirements/base.txt # -r requirements/base.txt + # django-digid-eherkenning django-elasticsearch-dsl==7.4 # via # -c requirements/base.txt @@ -461,7 +462,7 @@ django-solo==2.2.0 # mozilla-django-oidc-db # notifications-api-common # zgw-consumers -django-timeline-logger==3.0.0 +django-timeline-logger==4.0.0 # via # -c requirements/base.txt # -r requirements/base.txt @@ -743,15 +744,16 @@ messagebird==2.1.0 # via # -c requirements/base.txt # -r requirements/base.txt -mozilla-django-oidc==2.0.0 +mozilla-django-oidc==4.0.1 # via # -c requirements/base.txt # -r requirements/base.txt # mozilla-django-oidc-db -mozilla-django-oidc-db==0.14.1 +mozilla-django-oidc-db==0.19.0 # via # -c requirements/base.txt # -r requirements/base.txt + # django-digid-eherkenning multidict==6.0.5 # via yarl mypy-extensions==1.0.0 @@ -1066,6 +1068,7 @@ typing-extensions==4.10.0 # via # -c requirements/base.txt # -r requirements/base.txt + # mozilla-django-oidc-db # polyfactory # pydantic # pydantic-core diff --git a/requirements/dev.txt b/requirements/dev.txt index fccd6333bd..7a4fc8326b 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -356,10 +356,11 @@ django-csp-reports==1.8.1 # -r requirements/ci.txt django-debug-toolbar==3.2.2 # via -r requirements/dev.in -django-digid-eherkenning==0.13.1 +django-digid-eherkenning[oidc]==0.16.0 # via # -c requirements/ci.txt # -r requirements/ci.txt + # django-digid-eherkenning django-elasticsearch-dsl==7.4 # via # -c requirements/ci.txt @@ -507,7 +508,7 @@ django-solo==2.2.0 # mozilla-django-oidc-db # notifications-api-common # zgw-consumers -django-timeline-logger==3.0.0 +django-timeline-logger==4.0.0 # via # -c requirements/ci.txt # -r requirements/ci.txt @@ -845,15 +846,16 @@ messagebird==2.1.0 # via # -c requirements/ci.txt # -r requirements/ci.txt -mozilla-django-oidc==2.0.0 +mozilla-django-oidc==4.0.1 # via # -c requirements/ci.txt # -r requirements/ci.txt # mozilla-django-oidc-db -mozilla-django-oidc-db==0.14.1 +mozilla-django-oidc-db==0.19.0 # via # -c requirements/ci.txt # -r requirements/ci.txt + # django-digid-eherkenning msgpack==1.0.7 # via locust multidict==6.0.5 @@ -1269,6 +1271,7 @@ typing-extensions==4.10.0 # via # -c requirements/ci.txt # -r requirements/ci.txt + # mozilla-django-oidc-db # polyfactory # pydantic # pydantic-core diff --git a/src/digid_eherkenning_oidc_generics/__init__.py b/src/digid_eherkenning_oidc_generics/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/digid_eherkenning_oidc_generics/admin.py b/src/digid_eherkenning_oidc_generics/admin.py deleted file mode 100644 index b7bc796ae9..0000000000 --- a/src/digid_eherkenning_oidc_generics/admin.py +++ /dev/null @@ -1,69 +0,0 @@ -from django.contrib import admin -from django.utils.translation import gettext_lazy as _ - -from solo.admin import SingletonModelAdmin - -from .forms import OpenIDConnectDigiDConfigForm, OpenIDConnectEHerkenningConfigForm -from .models import OpenIDConnectDigiDConfig, OpenIDConnectEHerkenningConfig - - -class OpenIDConnectConfigBaseAdmin(SingletonModelAdmin): - fieldsets = ( - ( - _("Activation"), - {"fields": ("enabled",)}, - ), - ( - _("Common settings"), - { - "fields": ( - "identifier_claim_name", - "oidc_rp_client_id", - "oidc_rp_client_secret", - "oidc_rp_scopes_list", - "oidc_rp_sign_algo", - "oidc_rp_idp_sign_key", - "userinfo_claims_source", - "error_message_mapping", - ) - }, - ), - ( - _("Endpoints"), - { - "fields": ( - "oidc_op_discovery_endpoint", - "oidc_op_jwks_endpoint", - "oidc_op_authorization_endpoint", - "oidc_op_token_endpoint", - "oidc_op_user_endpoint", - "oidc_op_logout_endpoint", - ) - }, - ), - (_("Keycloak specific settings"), {"fields": ("oidc_keycloak_idp_hint",)}), - ( - _("Advanced settings"), - { - "fields": ( - "oidc_use_nonce", - "oidc_nonce_size", - "oidc_state_size", - "oidc_exempt_urls", - ), - "classes": [ - "collapse in", - ], - }, - ), - ) - - -@admin.register(OpenIDConnectDigiDConfig) -class OpenIDConnectConfigDigiDAdmin(OpenIDConnectConfigBaseAdmin): - form = OpenIDConnectDigiDConfigForm - - -@admin.register(OpenIDConnectEHerkenningConfig) -class OpenIDConnectConfigEHerkenningAdmin(OpenIDConnectConfigBaseAdmin): - form = OpenIDConnectEHerkenningConfigForm diff --git a/src/digid_eherkenning_oidc_generics/apps.py b/src/digid_eherkenning_oidc_generics/apps.py deleted file mode 100644 index 77906b4bb5..0000000000 --- a/src/digid_eherkenning_oidc_generics/apps.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.apps import AppConfig -from django.utils.translation import gettext_lazy as _ - - -class DigiDeHerkenningOIDCAppConfig(AppConfig): - name = "digid_eherkenning_oidc_generics" - verbose_name = _("DigiD & eHerkenning via OpenID Connect") diff --git a/src/digid_eherkenning_oidc_generics/backends.py b/src/digid_eherkenning_oidc_generics/backends.py deleted file mode 100644 index 87665eacb1..0000000000 --- a/src/digid_eherkenning_oidc_generics/backends.py +++ /dev/null @@ -1,82 +0,0 @@ -import logging - -from django.urls import reverse_lazy - -from mozilla_django_oidc_db.backends import ( - OIDCAuthenticationBackend as _OIDCAuthenticationBackend, -) - -from open_inwoner.accounts.choices import LoginTypeChoices -from open_inwoner.utils.hash import generate_email_from_string - -from .mixins import SoloConfigDigiDMixin, SoloConfigEHerkenningMixin - -logger = logging.getLogger(__name__) - - -class OIDCAuthenticationBackend(_OIDCAuthenticationBackend): - config_identifier_field = "identifier_claim_name" - callback_path = None - unique_id_user_fieldname = "" - - def authenticate(self, request, *args, **kwargs): - # Avoid attempting OIDC for a specific variant if we know that that is not the - # correct variant being attempted - if request and request.path != self.callback_path: - return - - return super().authenticate(request, *args, **kwargs) - - def filter_users_by_claims(self, claims): - """Return all users matching the specified subject.""" - unique_id = self.retrieve_identifier_claim(claims) - - if not unique_id: - return self.UserModel.objects.none() - return self.UserModel.objects.filter( - **{f"{self.unique_id_user_fieldname}__iexact": unique_id} - ) - - def create_user(self, claims): - """Return object for a newly created user account.""" - unique_id = self.retrieve_identifier_claim(claims) - - logger.debug("Creating OIDC user: %s", unique_id) - - user = self.UserModel.objects.create_user( - **{ - self.UserModel.USERNAME_FIELD: generate_email_from_string( - unique_id, domain="localhost" - ), - self.unique_id_user_fieldname: unique_id, - "login_type": self.login_type, - } - ) - - return user - - def update_user(self, user, claims): - # TODO should we do anything here? or do we only fetch data from HaalCentraal - return user - - -class OIDCAuthenticationDigiDBackend(SoloConfigDigiDMixin, OIDCAuthenticationBackend): - """ - Allows logging in via OIDC with DigiD - """ - - login_type = LoginTypeChoices.digid - callback_path = reverse_lazy("digid_oidc:callback") - unique_id_user_fieldname = "bsn" - - -class OIDCAuthenticationEHerkenningBackend( - SoloConfigEHerkenningMixin, OIDCAuthenticationBackend -): - """ - Allows logging in via OIDC with eHerkenning - """ - - login_type = LoginTypeChoices.eherkenning - callback_path = reverse_lazy("eherkenning_oidc:callback") - unique_id_user_fieldname = "kvk" diff --git a/src/digid_eherkenning_oidc_generics/constants.py b/src/digid_eherkenning_oidc_generics/constants.py deleted file mode 100644 index a2f917db49..0000000000 --- a/src/digid_eherkenning_oidc_generics/constants.py +++ /dev/null @@ -1,2 +0,0 @@ -DIGID_OIDC_AUTH_SESSION_KEY = "digid_oidc:bsn" -EHERKENNING_OIDC_AUTH_SESSION_KEY = "eherkenning_oidc:kvk" diff --git a/src/digid_eherkenning_oidc_generics/digid_settings.py b/src/digid_eherkenning_oidc_generics/digid_settings.py deleted file mode 100644 index 212c1d2303..0000000000 --- a/src/digid_eherkenning_oidc_generics/digid_settings.py +++ /dev/null @@ -1,2 +0,0 @@ -DIGID_CUSTOM_OIDC_DB_PREFIX = "digid_oidc" -OIDC_AUTHENTICATION_CALLBACK_URL = "digid_oidc:callback" diff --git a/src/digid_eherkenning_oidc_generics/digid_urls.py b/src/digid_eherkenning_oidc_generics/digid_urls.py deleted file mode 100644 index 8c591eba4a..0000000000 --- a/src/digid_eherkenning_oidc_generics/digid_urls.py +++ /dev/null @@ -1,30 +0,0 @@ -from django.urls import path - -from mozilla_django_oidc.urls import urlpatterns - -from .views import ( - DigiDOIDCAuthenticationCallbackView, - DigiDOIDCAuthenticationRequestView, - DigiDOIDCLogoutView, -) - -app_name = "digid_oidc" - - -urlpatterns = [ - path( - "callback/", - DigiDOIDCAuthenticationCallbackView.as_view(), - name="callback", - ), - path( - "authenticate/", - DigiDOIDCAuthenticationRequestView.as_view(), - name="init", - ), - path( - "logout/", - DigiDOIDCLogoutView.as_view(), - name="logout", - ), -] + urlpatterns diff --git a/src/digid_eherkenning_oidc_generics/eherkenning_settings.py b/src/digid_eherkenning_oidc_generics/eherkenning_settings.py deleted file mode 100644 index 3b8c2871bf..0000000000 --- a/src/digid_eherkenning_oidc_generics/eherkenning_settings.py +++ /dev/null @@ -1,2 +0,0 @@ -EHERKENNING_CUSTOM_OIDC_DB_PREFIX = "eherkenning_oidc" -OIDC_AUTHENTICATION_CALLBACK_URL = "eherkenning_oidc:callback" diff --git a/src/digid_eherkenning_oidc_generics/eherkenning_urls.py b/src/digid_eherkenning_oidc_generics/eherkenning_urls.py deleted file mode 100644 index 453f3ab754..0000000000 --- a/src/digid_eherkenning_oidc_generics/eherkenning_urls.py +++ /dev/null @@ -1,30 +0,0 @@ -from django.urls import path - -from mozilla_django_oidc.urls import urlpatterns - -from .views import ( - eHerkenningOIDCAuthenticationCallbackView, - eHerkenningOIDCAuthenticationRequestView, - eHerkenningOIDCLogoutView, -) - -app_name = "eherkenning_oidc" - - -urlpatterns = [ - path( - "callback/", - eHerkenningOIDCAuthenticationCallbackView.as_view(), - name="callback", - ), - path( - "authenticate/", - eHerkenningOIDCAuthenticationRequestView.as_view(), - name="init", - ), - path( - "logout/", - eHerkenningOIDCLogoutView.as_view(), - name="logout", - ), -] + urlpatterns diff --git a/src/digid_eherkenning_oidc_generics/forms.py b/src/digid_eherkenning_oidc_generics/forms.py deleted file mode 100644 index 34a64ad445..0000000000 --- a/src/digid_eherkenning_oidc_generics/forms.py +++ /dev/null @@ -1,32 +0,0 @@ -from copy import deepcopy - -from mozilla_django_oidc_db.constants import OIDC_MAPPING as _OIDC_MAPPING -from mozilla_django_oidc_db.forms import OpenIDConnectConfigForm - -from .models import OpenIDConnectDigiDConfig, OpenIDConnectEHerkenningConfig - -OIDC_MAPPING = deepcopy(_OIDC_MAPPING) - -OIDC_MAPPING["oidc_op_logout_endpoint"] = "end_session_endpoint" - - -class OpenIDConnectBaseConfigForm(OpenIDConnectConfigForm): - required_endpoints = [ - "oidc_op_authorization_endpoint", - "oidc_op_token_endpoint", - "oidc_op_user_endpoint", - "oidc_op_logout_endpoint", - ] - oidc_mapping = OIDC_MAPPING - - -class OpenIDConnectDigiDConfigForm(OpenIDConnectBaseConfigForm): - class Meta: - model = OpenIDConnectDigiDConfig - fields = "__all__" - - -class OpenIDConnectEHerkenningConfigForm(OpenIDConnectBaseConfigForm): - class Meta: - model = OpenIDConnectEHerkenningConfig - fields = "__all__" diff --git a/src/digid_eherkenning_oidc_generics/migrations/0001_initial.py b/src/digid_eherkenning_oidc_generics/migrations/0001_initial.py deleted file mode 100644 index 1933a5ad77..0000000000 --- a/src/digid_eherkenning_oidc_generics/migrations/0001_initial.py +++ /dev/null @@ -1,400 +0,0 @@ -# Generated by Django 3.2.20 on 2023-12-07 12:02 - -import digid_eherkenning_oidc_generics.models -from django.db import migrations, models -import django_jsonform.models.fields -import mozilla_django_oidc_db.models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [] - - operations = [ - migrations.CreateModel( - name="OpenIDConnectDigiDConfig", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "oidc_rp_client_id", - models.CharField( - help_text="OpenID Connect client ID provided by the OIDC Provider", - max_length=1000, - verbose_name="OpenID Connect client ID", - ), - ), - ( - "oidc_rp_client_secret", - models.CharField( - help_text="OpenID Connect secret provided by the OIDC Provider", - max_length=1000, - verbose_name="OpenID Connect secret", - ), - ), - ( - "oidc_rp_sign_algo", - models.CharField( - default="HS256", - help_text="Algorithm the Identity Provider uses to sign ID tokens", - max_length=50, - verbose_name="OpenID sign algorithm", - ), - ), - ( - "oidc_op_discovery_endpoint", - models.URLField( - blank=True, - help_text="URL of your OpenID Connect provider discovery endpoint ending with a slash (`.well-known/...` will be added automatically). If this is provided, the remaining endpoints can be omitted, as they will be derived from this endpoint.", - max_length=1000, - verbose_name="Discovery endpoint", - ), - ), - ( - "oidc_op_jwks_endpoint", - models.URLField( - blank=True, - help_text="URL of your OpenID Connect provider JSON Web Key Set endpoint. Required if `RS256` is used as signing algorithm.", - max_length=1000, - verbose_name="JSON Web Key Set endpoint", - ), - ), - ( - "oidc_op_authorization_endpoint", - models.URLField( - help_text="URL of your OpenID Connect provider authorization endpoint", - max_length=1000, - verbose_name="Authorization endpoint", - ), - ), - ( - "oidc_op_token_endpoint", - models.URLField( - help_text="URL of your OpenID Connect provider token endpoint", - max_length=1000, - verbose_name="Token endpoint", - ), - ), - ( - "oidc_op_user_endpoint", - models.URLField( - help_text="URL of your OpenID Connect provider userinfo endpoint", - max_length=1000, - verbose_name="User endpoint", - ), - ), - ( - "oidc_rp_idp_sign_key", - models.CharField( - blank=True, - help_text="Key the Identity Provider uses to sign ID tokens in the case of an RSA sign algorithm. Should be the signing key in PEM or DER format.", - max_length=1000, - verbose_name="Sign key", - ), - ), - ( - "oidc_use_nonce", - models.BooleanField( - default=True, - help_text="Controls whether the OpenID Connect client uses nonce verification", - verbose_name="Use nonce", - ), - ), - ( - "oidc_nonce_size", - models.PositiveIntegerField( - default=32, - help_text="Sets the length of the random string used for OpenID Connect nonce verification", - verbose_name="Nonce size", - ), - ), - ( - "oidc_state_size", - models.PositiveIntegerField( - default=32, - help_text="Sets the length of the random string used for OpenID Connect state verification", - verbose_name="State size", - ), - ), - ( - "oidc_exempt_urls", - django_jsonform.models.fields.ArrayField( - base_field=models.CharField( - max_length=1000, verbose_name="Exempt URL" - ), - blank=True, - default=list, - help_text="This is a list of absolute url paths, regular expressions for url paths, or Django view names. This plus the mozilla-django-oidc urls are exempted from the session renewal by the SessionRefresh middleware.", - size=None, - verbose_name="URLs exempt from session renewal", - ), - ), - ( - "userinfo_claims_source", - models.CharField( - choices=[ - ("userinfo_endpoint", "Userinfo endpoint"), - ("id_token", "ID token"), - ], - default="userinfo_endpoint", - help_text="Indicates the source from which the user information claims should be extracted.", - max_length=100, - verbose_name="user information claims extracted from", - ), - ), - ( - "oidc_op_logout_endpoint", - models.URLField( - blank=True, - help_text="URL of your OpenID Connect provider logout endpoint", - max_length=1000, - verbose_name="Logout endpoint", - ), - ), - ( - "oidc_keycloak_idp_hint", - models.CharField( - blank=True, - help_text="Specific for Keycloak: parameter that indicates which identity provider should be used (therefore skipping the Keycloak login screen).", - max_length=1000, - verbose_name="Keycloak Identity Provider hint", - ), - ), - ( - "enabled", - models.BooleanField( - default=False, - help_text="Indicates whether OpenID Connect for authentication/authorization is enabled. This overrides overrides the usage of SAML for DigiD authentication.", - verbose_name="enable", - ), - ), - ( - "identifier_claim_name", - models.CharField( - default="bsn", - help_text="The name of the claim in which the BSN of the user is stored", - max_length=100, - verbose_name="BSN claim name", - ), - ), - ( - "oidc_rp_scopes_list", - django_jsonform.models.fields.ArrayField( - base_field=models.CharField( - max_length=50, verbose_name="OpenID Connect scope" - ), - blank=True, - default=digid_eherkenning_oidc_generics.models.get_default_scopes_bsn, - help_text="OpenID Connect scopes that are requested during login. These scopes are hardcoded and must be supported by the identity provider", - size=None, - verbose_name="OpenID Connect scopes", - ), - ), - ], - options={ - "verbose_name": "OpenID Connect configuration for DigiD", - }, - bases=(mozilla_django_oidc_db.models.CachingMixin, models.Model), - ), - migrations.CreateModel( - name="OpenIDConnectEHerkenningConfig", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "oidc_rp_client_id", - models.CharField( - help_text="OpenID Connect client ID provided by the OIDC Provider", - max_length=1000, - verbose_name="OpenID Connect client ID", - ), - ), - ( - "oidc_rp_client_secret", - models.CharField( - help_text="OpenID Connect secret provided by the OIDC Provider", - max_length=1000, - verbose_name="OpenID Connect secret", - ), - ), - ( - "oidc_rp_sign_algo", - models.CharField( - default="HS256", - help_text="Algorithm the Identity Provider uses to sign ID tokens", - max_length=50, - verbose_name="OpenID sign algorithm", - ), - ), - ( - "oidc_op_discovery_endpoint", - models.URLField( - blank=True, - help_text="URL of your OpenID Connect provider discovery endpoint ending with a slash (`.well-known/...` will be added automatically). If this is provided, the remaining endpoints can be omitted, as they will be derived from this endpoint.", - max_length=1000, - verbose_name="Discovery endpoint", - ), - ), - ( - "oidc_op_jwks_endpoint", - models.URLField( - blank=True, - help_text="URL of your OpenID Connect provider JSON Web Key Set endpoint. Required if `RS256` is used as signing algorithm.", - max_length=1000, - verbose_name="JSON Web Key Set endpoint", - ), - ), - ( - "oidc_op_authorization_endpoint", - models.URLField( - help_text="URL of your OpenID Connect provider authorization endpoint", - max_length=1000, - verbose_name="Authorization endpoint", - ), - ), - ( - "oidc_op_token_endpoint", - models.URLField( - help_text="URL of your OpenID Connect provider token endpoint", - max_length=1000, - verbose_name="Token endpoint", - ), - ), - ( - "oidc_op_user_endpoint", - models.URLField( - help_text="URL of your OpenID Connect provider userinfo endpoint", - max_length=1000, - verbose_name="User endpoint", - ), - ), - ( - "oidc_rp_idp_sign_key", - models.CharField( - blank=True, - help_text="Key the Identity Provider uses to sign ID tokens in the case of an RSA sign algorithm. Should be the signing key in PEM or DER format.", - max_length=1000, - verbose_name="Sign key", - ), - ), - ( - "oidc_use_nonce", - models.BooleanField( - default=True, - help_text="Controls whether the OpenID Connect client uses nonce verification", - verbose_name="Use nonce", - ), - ), - ( - "oidc_nonce_size", - models.PositiveIntegerField( - default=32, - help_text="Sets the length of the random string used for OpenID Connect nonce verification", - verbose_name="Nonce size", - ), - ), - ( - "oidc_state_size", - models.PositiveIntegerField( - default=32, - help_text="Sets the length of the random string used for OpenID Connect state verification", - verbose_name="State size", - ), - ), - ( - "oidc_exempt_urls", - django_jsonform.models.fields.ArrayField( - base_field=models.CharField( - max_length=1000, verbose_name="Exempt URL" - ), - blank=True, - default=list, - help_text="This is a list of absolute url paths, regular expressions for url paths, or Django view names. This plus the mozilla-django-oidc urls are exempted from the session renewal by the SessionRefresh middleware.", - size=None, - verbose_name="URLs exempt from session renewal", - ), - ), - ( - "userinfo_claims_source", - models.CharField( - choices=[ - ("userinfo_endpoint", "Userinfo endpoint"), - ("id_token", "ID token"), - ], - default="userinfo_endpoint", - help_text="Indicates the source from which the user information claims should be extracted.", - max_length=100, - verbose_name="user information claims extracted from", - ), - ), - ( - "oidc_op_logout_endpoint", - models.URLField( - blank=True, - help_text="URL of your OpenID Connect provider logout endpoint", - max_length=1000, - verbose_name="Logout endpoint", - ), - ), - ( - "oidc_keycloak_idp_hint", - models.CharField( - blank=True, - help_text="Specific for Keycloak: parameter that indicates which identity provider should be used (therefore skipping the Keycloak login screen).", - max_length=1000, - verbose_name="Keycloak Identity Provider hint", - ), - ), - ( - "enabled", - models.BooleanField( - default=False, - help_text="Indicates whether OpenID Connect for authentication/authorization is enabled. This overrides overrides the usage of SAML for eHerkenning authentication.", - verbose_name="enable", - ), - ), - ( - "identifier_claim_name", - models.CharField( - default="kvk", - help_text="The name of the claim in which the KVK of the user is stored", - max_length=100, - verbose_name="KVK claim name", - ), - ), - ( - "oidc_rp_scopes_list", - django_jsonform.models.fields.ArrayField( - base_field=models.CharField( - max_length=50, verbose_name="OpenID Connect scope" - ), - blank=True, - default=digid_eherkenning_oidc_generics.models.get_default_scopes_kvk, - help_text="OpenID Connect scopes that are requested during login. These scopes are hardcoded and must be supported by the identity provider", - size=None, - verbose_name="OpenID Connect scopes", - ), - ), - ], - options={ - "verbose_name": "OpenID Connect configuration for eHerkenning", - }, - bases=(mozilla_django_oidc_db.models.CachingMixin, models.Model), - ), - ] diff --git a/src/digid_eherkenning_oidc_generics/migrations/0002_auto_20240109_1055.py b/src/digid_eherkenning_oidc_generics/migrations/0002_auto_20240109_1055.py deleted file mode 100644 index 34ff0cf0d6..0000000000 --- a/src/digid_eherkenning_oidc_generics/migrations/0002_auto_20240109_1055.py +++ /dev/null @@ -1,35 +0,0 @@ -# Generated by Django 3.2.23 on 2024-01-09 09:55 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("digid_eherkenning_oidc_generics", "0001_initial"), - ] - - operations = [ - migrations.AddField( - model_name="openidconnectdigidconfig", - name="error_message_mapping", - field=models.JSONField( - blank=True, - default=dict, - help_text="Mapping that maps error messages returned by the identity provider to human readable error messages that are shown to the user", - max_length=1000, - verbose_name="Error message mapping", - ), - ), - migrations.AddField( - model_name="openidconnecteherkenningconfig", - name="error_message_mapping", - field=models.JSONField( - blank=True, - default=dict, - help_text="Mapping that maps error messages returned by the identity provider to human readable error messages that are shown to the user", - max_length=1000, - verbose_name="Error message mapping", - ), - ), - ] diff --git a/src/digid_eherkenning_oidc_generics/migrations/0003_alter_openidconnectdigidconfig_oidc_exempt_urls_and_more.py b/src/digid_eherkenning_oidc_generics/migrations/0003_alter_openidconnectdigidconfig_oidc_exempt_urls_and_more.py deleted file mode 100644 index 183b085ed6..0000000000 --- a/src/digid_eherkenning_oidc_generics/migrations/0003_alter_openidconnectdigidconfig_oidc_exempt_urls_and_more.py +++ /dev/null @@ -1,38 +0,0 @@ -# Generated by Django 4.2.10 on 2024-03-11 16:12 - -from django.db import migrations, models -import django_jsonform.models.fields - - -class Migration(migrations.Migration): - - dependencies = [ - ("digid_eherkenning_oidc_generics", "0002_auto_20240109_1055"), - ] - - operations = [ - migrations.AlterField( - model_name="openidconnectdigidconfig", - name="oidc_exempt_urls", - field=django_jsonform.models.fields.ArrayField( - base_field=models.CharField(max_length=1000, verbose_name="Exempt URL"), - blank=True, - default=list, - help_text="This is a list of absolute url paths, regular expressions for url paths, or Django view names. This plus the mozilla-django-oidc urls are exempted from the session renewal by the SessionRefresh middleware.", - size=None, - verbose_name="URLs exempt from session renewal", - ), - ), - migrations.AlterField( - model_name="openidconnecteherkenningconfig", - name="oidc_exempt_urls", - field=django_jsonform.models.fields.ArrayField( - base_field=models.CharField(max_length=1000, verbose_name="Exempt URL"), - blank=True, - default=list, - help_text="This is a list of absolute url paths, regular expressions for url paths, or Django view names. This plus the mozilla-django-oidc urls are exempted from the session renewal by the SessionRefresh middleware.", - size=None, - verbose_name="URLs exempt from session renewal", - ), - ), - ] diff --git a/src/digid_eherkenning_oidc_generics/migrations/__init__.py b/src/digid_eherkenning_oidc_generics/migrations/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/digid_eherkenning_oidc_generics/mixins.py b/src/digid_eherkenning_oidc_generics/mixins.py deleted file mode 100644 index 40c8223650..0000000000 --- a/src/digid_eherkenning_oidc_generics/mixins.py +++ /dev/null @@ -1,28 +0,0 @@ -import logging - -from mozilla_django_oidc_db.mixins import SoloConfigMixin as _SoloConfigMixin - -from . import digid_settings, eherkenning_settings -from .models import OpenIDConnectDigiDConfig, OpenIDConnectEHerkenningConfig - -logger = logging.getLogger(__name__) - - -class SoloConfigMixin(_SoloConfigMixin): - config_class = "" - settings_attribute = None - - def get_settings(self, attr, *args): - if hasattr(self.settings_attribute, attr): - return getattr(self.settings_attribute, attr) - return super().get_settings(attr, *args) - - -class SoloConfigDigiDMixin(SoloConfigMixin): - config_class = OpenIDConnectDigiDConfig - settings_attribute = digid_settings - - -class SoloConfigEHerkenningMixin(SoloConfigMixin): - config_class = OpenIDConnectEHerkenningConfig - settings_attribute = eherkenning_settings diff --git a/src/digid_eherkenning_oidc_generics/models.py b/src/digid_eherkenning_oidc_generics/models.py deleted file mode 100644 index 15218033ee..0000000000 --- a/src/digid_eherkenning_oidc_generics/models.py +++ /dev/null @@ -1,140 +0,0 @@ -from django.db import models -from django.utils.functional import classproperty -from django.utils.translation import gettext_lazy as _ - -from django_jsonform.models.fields import ArrayField -from mozilla_django_oidc_db.models import CachingMixin, OpenIDConnectConfigBase - -from .digid_settings import DIGID_CUSTOM_OIDC_DB_PREFIX -from .eherkenning_settings import EHERKENNING_CUSTOM_OIDC_DB_PREFIX - - -def get_default_scopes_bsn(): - """ - Returns the default scopes to request for OpenID Connect logins - """ - return ["openid", "bsn"] - - -def get_default_scopes_kvk(): - """ - Returns the default scopes to request for OpenID Connect logins - """ - return ["openid", "kvk"] - - -class OpenIDConnectBaseConfig(CachingMixin, OpenIDConnectConfigBase): - """ - Configuration for DigiD authentication via OpenID connect - """ - - oidc_op_logout_endpoint = models.URLField( - _("Logout endpoint"), - max_length=1000, - help_text=_("URL of your OpenID Connect provider logout endpoint"), - blank=True, - ) - - error_message_mapping = models.JSONField( - _("Error message mapping"), - max_length=1000, - help_text=_( - "Mapping that maps error messages returned by the identity provider " - "to human readable error messages that are shown to the user" - ), - default=dict, - blank=True, - ) - - # Keycloak specific config - oidc_keycloak_idp_hint = models.CharField( - _("Keycloak Identity Provider hint"), - max_length=1000, - help_text=_( - "Specific for Keycloak: parameter that indicates which identity provider " - "should be used (therefore skipping the Keycloak login screen)." - ), - blank=True, - ) - - class Meta: - verbose_name = _("OpenID Connect configuration") - abstract = True - - -class OpenIDConnectDigiDConfig(OpenIDConnectBaseConfig): - """ - Configuration for DigiD authentication via OpenID connect - """ - - enabled = models.BooleanField( - _("enable"), - default=False, - help_text=_( - "Indicates whether OpenID Connect for authentication/authorization is enabled. " - "This overrides overrides the usage of SAML for DigiD authentication." - ), - ) - - identifier_claim_name = models.CharField( - _("BSN claim name"), - max_length=100, - help_text=_("The name of the claim in which the BSN of the user is stored"), - default="bsn", - ) - oidc_rp_scopes_list = ArrayField( - verbose_name=_("OpenID Connect scopes"), - base_field=models.CharField(_("OpenID Connect scope"), max_length=50), - default=get_default_scopes_bsn, - blank=True, - help_text=_( - "OpenID Connect scopes that are requested during login. " - "These scopes are hardcoded and must be supported by the identity provider" - ), - ) - - @classproperty - def custom_oidc_db_prefix(cls): - return DIGID_CUSTOM_OIDC_DB_PREFIX - - class Meta: - verbose_name = _("OpenID Connect configuration for DigiD") - - -class OpenIDConnectEHerkenningConfig(OpenIDConnectBaseConfig): - """ - Configuration for eHerkenning authentication via OpenID connect - """ - - enabled = models.BooleanField( - _("enable"), - default=False, - help_text=_( - "Indicates whether OpenID Connect for authentication/authorization is enabled. " - "This overrides overrides the usage of SAML for eHerkenning authentication." - ), - ) - - identifier_claim_name = models.CharField( - _("KVK claim name"), - max_length=100, - help_text=_("The name of the claim in which the KVK of the user is stored"), - default="kvk", - ) - oidc_rp_scopes_list = ArrayField( - verbose_name=_("OpenID Connect scopes"), - base_field=models.CharField(_("OpenID Connect scope"), max_length=50), - default=get_default_scopes_kvk, - blank=True, - help_text=_( - "OpenID Connect scopes that are requested during login. " - "These scopes are hardcoded and must be supported by the identity provider" - ), - ) - - @classproperty - def custom_oidc_db_prefix(cls): - return EHERKENNING_CUSTOM_OIDC_DB_PREFIX - - class Meta: - verbose_name = _("OpenID Connect configuration for eHerkenning") diff --git a/src/digid_eherkenning_oidc_generics/views.py b/src/digid_eherkenning_oidc_generics/views.py deleted file mode 100644 index d9b32dbe2b..0000000000 --- a/src/digid_eherkenning_oidc_generics/views.py +++ /dev/null @@ -1,135 +0,0 @@ -import logging - -from django.conf import settings -from django.contrib import auth, messages -from django.http import HttpResponseRedirect -from django.shortcuts import resolve_url -from django.urls import reverse, reverse_lazy -from django.utils.translation import gettext_lazy as _ -from django.views.generic import View - -import requests -from furl import furl -from mozilla_django_oidc.views import ( - OIDCAuthenticationRequestView as _OIDCAuthenticationRequestView, -) -from mozilla_django_oidc_db.views import ( - OIDC_ERROR_SESSION_KEY, - OIDCCallbackView as _OIDCCallbackView, -) - -from digid_eherkenning_oidc_generics.mixins import ( - SoloConfigDigiDMixin, - SoloConfigEHerkenningMixin, -) - -logger = logging.getLogger(__name__) - - -GENERIC_DIGID_ERROR_MSG = _( - "Inloggen bij deze organisatie is niet gelukt. Probeert u het later " - "nog een keer. Lukt het nog steeds niet? Log in bij Mijn DigiD. " - "Zo controleert u of uw DigiD goed werkt. Mogelijk is er een " - "storing bij de organisatie waar u inlogt." -) -GENERIC_EHERKENNING_ERROR_MSG = _( - "Inloggen bij deze organisatie is niet gelukt. Probeert u het later nog een keer. " - "Lukt het nog steeds niet? Neem dan contact op met uw eHerkenning leverancier of " - "kijk op https://www.eherkenning.nl" -) - - -class OIDCAuthenticationRequestView(_OIDCAuthenticationRequestView): - def get_extra_params(self, request): - kc_idp_hint = self.get_settings("OIDC_KEYCLOAK_IDP_HINT", "") - if kc_idp_hint: - return {"kc_idp_hint": kc_idp_hint} - return {} - - -class OIDCFailureView(View): - def get(self, request): - if OIDC_ERROR_SESSION_KEY in self.request.session: - message = self.request.session[OIDC_ERROR_SESSION_KEY] - del self.request.session[OIDC_ERROR_SESSION_KEY] - messages.error(request, message) - else: - messages.error( - request, - _("Something went wrong while logging in, please try again later."), - ) - return HttpResponseRedirect(reverse("login")) - - -class OIDCCallbackView(_OIDCCallbackView): - failure_url = reverse_lazy("oidc-error") - generic_error_msg = "" - - def get(self, request): - response = super().get(request) - - error = request.GET.get("error_description") - error_label = self.config.error_message_mapping.get( - error, str(self.generic_error_msg) - ) - if error and error_label: - request.session[OIDC_ERROR_SESSION_KEY] = error_label - elif OIDC_ERROR_SESSION_KEY in request.session and error_label: - request.session[OIDC_ERROR_SESSION_KEY] = error_label - - return response - - -class OIDCLogoutView(View): - def get_success_url(self): - return resolve_url(settings.LOGOUT_REDIRECT_URL) - - def get(self, request): - if "oidc_id_token" in request.session: - logout_endpoint = self.config_class.get_solo().oidc_op_logout_endpoint - if logout_endpoint: - logout_url = furl(logout_endpoint).set( - { - "id_token_hint": request.session["oidc_id_token"], - } - ) - requests.get(str(logout_url)) - - del request.session["oidc_id_token"] - - if "oidc_login_next" in request.session: - del request.session["oidc_login_next"] - - auth.logout(request) - - return HttpResponseRedirect(self.get_success_url()) - - -class DigiDOIDCAuthenticationRequestView( - SoloConfigDigiDMixin, OIDCAuthenticationRequestView -): - pass - - -class DigiDOIDCAuthenticationCallbackView(SoloConfigDigiDMixin, OIDCCallbackView): - generic_error_msg = GENERIC_DIGID_ERROR_MSG - - -class DigiDOIDCLogoutView(SoloConfigDigiDMixin, OIDCLogoutView): - pass - - -class eHerkenningOIDCAuthenticationRequestView( - SoloConfigEHerkenningMixin, OIDCAuthenticationRequestView -): - pass - - -class eHerkenningOIDCAuthenticationCallbackView( - SoloConfigEHerkenningMixin, OIDCCallbackView -): - generic_error_msg = GENERIC_EHERKENNING_ERROR_MSG - - -class eHerkenningOIDCLogoutView(SoloConfigEHerkenningMixin, OIDCLogoutView): - pass diff --git a/src/open_inwoner/accounts/backends.py b/src/open_inwoner/accounts/backends.py index bf55a60c82..c1bb8e2fc6 100644 --- a/src/open_inwoner/accounts/backends.py +++ b/src/open_inwoner/accounts/backends.py @@ -1,4 +1,5 @@ import logging +from typing import Literal from django.conf import settings from django.contrib.auth import get_user_model @@ -7,13 +8,16 @@ from django.urls import reverse, reverse_lazy from axes.backends import AxesBackend +from digid_eherkenning.oidc.backends import BaseBackend from mozilla_django_oidc_db.backends import OIDCAuthenticationBackend +from mozilla_django_oidc_db.config import dynamic_setting from oath import accept_totp from open_inwoner.configurations.models import SiteConfiguration from open_inwoner.utils.hash import generate_email_from_string from .choices import LoginTypeChoices +from .models import OpenIDDigiDConfig, OpenIDEHerkenningConfig logger = logging.getLogger(__name__) @@ -76,6 +80,8 @@ class CustomOIDCBackend(OIDCAuthenticationBackend): def authenticate(self, request, *args, **kwargs): # Avoid attempting OIDC for a specific variant if we know that that is not the # correct variant being attempted + # XXX, TODO, check the config class rather than the path once there's + # a single callback URL. We can override ``_check_candidate_backend``. if request and request.path != self.callback_path: return @@ -91,7 +97,7 @@ def create_user(self, claims): before we got here we already checked for existing users based on the overriden queryset from the .filter_users_by_claims() """ - unique_id = self.retrieve_identifier_claim(claims) + unique_id = self._extract_username(claims) if "email" in claims: email = claims["email"] @@ -134,8 +140,49 @@ def create_user(self, claims): def filter_users_by_claims(self, claims): """Return all users matching the specified subject.""" - unique_id = self.retrieve_identifier_claim(claims) + unique_id = self._extract_username(claims) if not unique_id: return self.UserModel.objects.none() return self.UserModel.objects.filter(**{"oidc_id__iexact": unique_id}) + + +class DigiDEHerkenningOIDCBackend(BaseBackend): + OIP_UNIQUE_ID_USER_FIELDNAME = dynamic_setting[Literal["bsn", "kvk"]]() + OIP_LOGIN_TYPE = dynamic_setting[LoginTypeChoices]() + + def _check_candidate_backend(self) -> bool: + parent = super()._check_candidate_backend() + return parent and self.config_class in ( + OpenIDDigiDConfig, + OpenIDEHerkenningConfig, + ) + + def filter_users_by_claims(self, claims): + """Return all users matching the specified subject.""" + unique_id = self._extract_username(claims) + + if not unique_id: + return self.UserModel.objects.none() + return self.UserModel.objects.filter( + **{f"{self.OIP_UNIQUE_ID_USER_FIELDNAME}__iexact": unique_id} + ) + + def create_user(self, claims): + """Return object for a newly created user account.""" + + unique_id = self._extract_username(claims) + + logger.debug("Creating OIDC user: %s", unique_id) + + user = self.UserModel.objects.create_user( + **{ + self.UserModel.USERNAME_FIELD: generate_email_from_string( + unique_id, domain="localhost" + ), + self.OIP_UNIQUE_ID_USER_FIELDNAME: unique_id, + "login_type": self.OIP_LOGIN_TYPE, + } + ) + + return user diff --git a/src/open_inwoner/accounts/digid_urls.py b/src/open_inwoner/accounts/digid_urls.py new file mode 100644 index 0000000000..96d79f0626 --- /dev/null +++ b/src/open_inwoner/accounts/digid_urls.py @@ -0,0 +1,16 @@ +from django.urls import path + +from mozilla_django_oidc.urls import urlpatterns +from mozilla_django_oidc_db.views import OIDCCallbackView + +from .views import digid_init, digid_logout + +app_name = "digid_oidc" + + +urlpatterns = [ + # XXX: generic callback view, this can move to a single URL. + path("callback/", OIDCCallbackView.as_view(), name="callback"), + path("authenticate/", digid_init, name="init"), + path("logout/", digid_logout, name="logout"), +] + urlpatterns diff --git a/src/open_inwoner/accounts/eherkenning_urls.py b/src/open_inwoner/accounts/eherkenning_urls.py index bd03f5cda9..069826c467 100644 --- a/src/open_inwoner/accounts/eherkenning_urls.py +++ b/src/open_inwoner/accounts/eherkenning_urls.py @@ -1,16 +1,16 @@ from django.urls import path -from digid_eherkenning_oidc_generics.eherkenning_urls import urlpatterns +from mozilla_django_oidc.urls import urlpatterns +from mozilla_django_oidc_db.views import OIDCCallbackView -from .views import CustomEHerkenningOIDCAuthenticationCallbackView +from .views import eherkenning_init, eherkenning_logout app_name = "eherkenning_oidc" urlpatterns = [ - path( - "callback/", - CustomEHerkenningOIDCAuthenticationCallbackView.as_view(), - name="callback", - ), + # XXX: generic callback view, this can move to a single URL. + path("callback/", OIDCCallbackView.as_view(), name="callback"), + path("authenticate/", eherkenning_init, name="init"), + path("logout/", eherkenning_logout, name="logout"), ] + urlpatterns diff --git a/src/open_inwoner/accounts/migrations/0078_drop_digid_eh_oidc_generics_legacy_tables.py b/src/open_inwoner/accounts/migrations/0078_drop_digid_eh_oidc_generics_legacy_tables.py new file mode 100644 index 0000000000..1400834476 --- /dev/null +++ b/src/open_inwoner/accounts/migrations/0078_drop_digid_eh_oidc_generics_legacy_tables.py @@ -0,0 +1,28 @@ +from django.db import migrations + +# The tables for our vendored copy of the digid_eherkenning library +# need to be removed manually before the migrations of the new library +# can be run in order to avoid conflicts due to duplicate tables (the +# db tables from the library use the same app label as our legacy package) + + +class Migration(migrations.Migration): + + dependencies = [ + ("accounts", "0077_no_roepnaam"), + ] + run_before = [ + ( + "digid_eherkenning_oidc_generics", + "0001_initial_squashed_0007_auto_20221213_1347", + ) + ] + + operations = [ + migrations.RunSQL( + sql="DROP TABLE IF EXISTS digid_eherkenning_oidc_generics_openidconnectdigidconfig;", + ), + migrations.RunSQL( + sql="DROP TABLE IF EXISTS digid_eherkenning_oidc_generics_openidconnecteherkenningconfig;", + ), + ] diff --git a/src/open_inwoner/accounts/migrations/0079_digid_eherkenning_configs.py b/src/open_inwoner/accounts/migrations/0079_digid_eherkenning_configs.py new file mode 100644 index 0000000000..1ec7e9d809 --- /dev/null +++ b/src/open_inwoner/accounts/migrations/0079_digid_eherkenning_configs.py @@ -0,0 +1,37 @@ +# Generated by Django 4.2.16 on 2024-10-17 10:00 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ( + "digid_eherkenning_oidc_generics", + "0009_remove_digidconfig_oidc_exempt_urls_and_more", + ), + ("accounts", "0078_drop_digid_eh_oidc_generics_legacy_tables"), + ] + + operations = [ + migrations.CreateModel( + name="OpenIDDigiDConfig", + fields=[], + options={ + "proxy": True, + "indexes": [], + "constraints": [], + }, + bases=("digid_eherkenning_oidc_generics.digidconfig",), + ), + migrations.CreateModel( + name="OpenIDEHerkenningConfig", + fields=[], + options={ + "proxy": True, + "indexes": [], + "constraints": [], + }, + bases=("digid_eherkenning_oidc_generics.eherkenningconfig",), + ), + ] diff --git a/src/open_inwoner/accounts/models.py b/src/open_inwoner/accounts/models.py index 11f8463cb5..25ffd147e5 100644 --- a/src/open_inwoner/accounts/models.py +++ b/src/open_inwoner/accounts/models.py @@ -11,18 +11,19 @@ from django.urls import reverse from django.utils import timezone from django.utils.crypto import get_random_string +from django.utils.functional import classproperty from django.utils.translation import gettext_lazy as _ +from digid_eherkenning.oidc.models import ( + DigiDConfig as _OIDCDigiDConfig, + EHerkenningConfig as _OIDCEHerkenningConfig, +) from image_cropping import ImageCropField, ImageRatioField from localflavor.nl.models import NLBSNField, NLZipCodeField from mail_editor.helpers import find_template from privates.storages import PrivateMediaFileSystemStorage from timeline_logger.models import TimelineLog -from digid_eherkenning_oidc_generics.models import ( - OpenIDConnectDigiDConfig, - OpenIDConnectEHerkenningConfig, -) from open_inwoner.configurations.models import SiteConfiguration from open_inwoner.utils.hash import create_sha256_hash from open_inwoner.utils.validators import ( @@ -42,6 +43,69 @@ from .managers import ActionQueryset, DigidManager, UserManager, eHerkenningManager from .query import InviteQuerySet, MessageQuerySet +### +# Configuration +### + + +class OpenIDDigiDConfig(_OIDCDigiDConfig): + """ + Proxy upstream library configuration model to override Python behaviour. + """ + + oip_unique_id_user_fieldname = "bsn" + oip_login_type = LoginTypeChoices.digid + + class Meta: + proxy = True + + # XXX: enabling this requires the tests/mocks to be updated. exercise left to the + # reader. + @classproperty + def oidcdb_check_idp_availability(cls): + return False + + @property + def oidc_authentication_callback_url(self): + return "digid_oidc:callback" + + def get_callback_view(self): + from .views import digid_callback + + return digid_callback + + +class OpenIDEHerkenningConfig(_OIDCEHerkenningConfig): + """ + Proxy upstream library configuration model to override Python behaviour. + """ + + oip_unique_id_user_fieldname = "kvk" + oip_login_type = LoginTypeChoices.eherkenning + + class Meta: + proxy = True + + # XXX: enabling this requires the tests/mocks to be updated. exercise left to the + # reader. + @classproperty + def oidcdb_check_idp_availability(cls): + return False + + @property + def oidc_authentication_callback_url(self): + return "eherkenning_oidc:callback" + + def get_callback_view(self): + from .views import eherkenning_callback + + return eherkenning_callback + + +### +# Content +### + def generate_uuid_image_name(instance, filename): filename, file_extension = os.path.splitext(filename) @@ -432,11 +496,11 @@ def get_logout_url(self) -> str: return reverse("logout") if self.login_type == LoginTypeChoices.digid: - if OpenIDConnectDigiDConfig.get_solo().enabled: + if OpenIDDigiDConfig.get_solo().enabled: return reverse("digid_oidc:logout") return reverse("digid:logout") elif self.login_type == LoginTypeChoices.eherkenning: - if OpenIDConnectEHerkenningConfig.get_solo().enabled: + if OpenIDEHerkenningConfig.get_solo().enabled: return reverse("eherkenning_oidc:logout") return reverse("logout") diff --git a/src/open_inwoner/accounts/templates/accounts/email_verification.html b/src/open_inwoner/accounts/templates/accounts/email_verification.html index 9ca1d11942..d2a69e5655 100644 --- a/src/open_inwoner/accounts/templates/accounts/email_verification.html +++ b/src/open_inwoner/accounts/templates/accounts/email_verification.html @@ -7,7 +7,7 @@ {% render_column span=9 %} {% render_card tinted=True %} {% get_solo 'configurations.SiteConfiguration' as config %} -
{{ verification_text|linebreaksbr }}