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 %} -

{% trans "E-mailadres bevestigen" %}


+

{% trans "E-mail is verzonden" %}


{% if verification_text %}

{{ verification_text|linebreaksbr }}


{% endif %}
{% csrf_token %} diff --git a/src/open_inwoner/accounts/templates/registration/login.html b/src/open_inwoner/accounts/templates/registration/login.html index 1032360765..238ea74a0c 100644 --- a/src/open_inwoner/accounts/templates/registration/login.html +++ b/src/open_inwoner/accounts/templates/registration/login.html @@ -32,7 +32,7 @@

{% trans 'Welkom' %}

{# Digid / Open ID Connect #} {% if settings.DIGID_ENABLED %} {% render_column start=4 span=5 %} - {% get_solo 'digid_eherkenning_oidc_generics.OpenIDConnectDigiDConfig' as digid_oidc_config %} + {% get_solo 'accounts.OpenIDDigiDConfig' as digid_oidc_config %} {% if digid_oidc_config.enabled %} {% render_card direction='horizontal' tinted=True compact=True %} {% url 'digid_oidc:init' as href %} @@ -132,7 +132,7 @@

Of registreer

{% render_grid %} {% render_column start=4 span=5 %} {% if eherkenning_enabled %} - {% get_solo 'digid_eherkenning_oidc_generics.OpenIDConnectEHerkenningConfig' as eherkenning_oidc_config %} + {% get_solo 'accounts.OpenIDEHerkenningConfig' as eherkenning_oidc_config %} {% if eherkenning_oidc_config.enabled %} {% render_card direction='horizontal' tinted=True compact=True %} {% url 'eherkenning_oidc:init' as href %} diff --git a/src/open_inwoner/accounts/tests/test_auth.py b/src/open_inwoner/accounts/tests/test_auth.py index c1ce22d7bb..50a2acd422 100644 --- a/src/open_inwoner/accounts/tests/test_auth.py +++ b/src/open_inwoner/accounts/tests/test_auth.py @@ -13,10 +13,6 @@ from furl import furl from pyquery import PyQuery as PQ -from digid_eherkenning_oidc_generics.models import ( - OpenIDConnectDigiDConfig, - OpenIDConnectEHerkenningConfig, -) from open_inwoner.accounts.choices import NotificationChannelChoice from open_inwoner.accounts.signals import update_user_from_klant_on_login from open_inwoner.configurations.models import SiteConfiguration @@ -33,7 +29,7 @@ from ...utils.test import ClearCachesMixin from ...utils.tests.helpers import AssertRedirectsMixin from ..choices import LoginTypeChoices -from ..models import User +from ..models import OpenIDDigiDConfig, OpenIDEHerkenningConfig, User from .factories import ( DigidUserFactory, InviteFactory, @@ -59,7 +55,7 @@ class DigiDRegistrationTest( def setUpTestData(cls): cls.homepage = cms_tools.create_homepage() - @patch("digid_eherkenning_oidc_generics.models.OpenIDConnectDigiDConfig.get_solo") + @patch("open_inwoner.accounts.models.OpenIDDigiDConfig.get_solo") def test_registration_page_only_digid(self, mock_solo): for oidc_enabled in [True, False]: with self.subTest(oidc_enabled=oidc_enabled): @@ -523,9 +519,7 @@ class eHerkenningRegistrationTest(AssertRedirectsMixin, WebTest): def setUpTestData(cls): cms_tools.create_homepage() - @patch( - "digid_eherkenning_oidc_generics.models.OpenIDConnectEHerkenningConfig.get_solo" - ) + @patch("open_inwoner.accounts.models.OpenIDEHerkenningConfig.get_solo") @patch("open_inwoner.configurations.models.SiteConfiguration.get_solo") def test_registration_page_eherkenning(self, mock_solo, mock_eherkenning_config): mock_solo.return_value.eherkenning_enabled = True @@ -1716,7 +1710,7 @@ def test_login(self): self.assertIn("_auth_user_id", self.app.session) def test_login_page_shows_correct_digid_login_url(self): - config = OpenIDConnectDigiDConfig.get_solo() + config = OpenIDDigiDConfig.get_solo() for oidc_enabled in [True, False]: with self.subTest(oidc_enabled=oidc_enabled): @@ -1741,7 +1735,7 @@ def test_login_page_shows_correct_eherkenning_login_url(self): site_config.eherkenning_enabled = True site_config.save() - config = OpenIDConnectEHerkenningConfig.get_solo() + config = OpenIDEHerkenningConfig.get_solo() for oidc_enabled in [True, False]: with self.subTest(oidc_enabled=oidc_enabled): diff --git a/src/open_inwoner/accounts/tests/test_backends.py b/src/open_inwoner/accounts/tests/test_backends.py index 595702ef9d..e72fab9660 100644 --- a/src/open_inwoner/accounts/tests/test_backends.py +++ b/src/open_inwoner/accounts/tests/test_backends.py @@ -4,7 +4,10 @@ from django.test import RequestFactory, TestCase, override_settings from django.urls import reverse -from open_inwoner.accounts.tests.factories import UserFactory +from furl import furl +from mozilla_django_oidc_db.config import store_config + +from .factories import UserFactory class OIDCBackendTestCase(TestCase): @@ -17,45 +20,51 @@ def setUpTestData(cls): @override_settings( AUTHENTICATION_BACKENDS=[ "open_inwoner.accounts.backends.CustomOIDCBackend", - "digid_eherkenning_oidc_generics.backends.OIDCAuthenticationEHerkenningBackend", - "digid_eherkenning_oidc_generics.backends.OIDCAuthenticationDigiDBackend", + "open_inwoner.accounts.backends.DigiDEHerkenningOIDCBackend", ] ) - @patch( - "digid_eherkenning_oidc_generics.backends.OIDCAuthenticationDigiDBackend.authenticate" - ) - @patch( - "open_inwoner.accounts.backends.OIDCAuthenticationBackend.authenticate", - side_effect=Exception, - ) - def test_digid_oidc_use_correct_backend( - self, mock_authenticate, mock_digid_authenticate - ): + @patch("open_inwoner.accounts.backends.DigiDEHerkenningOIDCBackend.authenticate") + def test_digid_oidc_selects_correct_backend(self, mock_authenticate): """ Both the regular OIDC and eHerkenning backend should check if the request path matches their callback before trying to authenticate """ - mock_digid_authenticate.return_value = self.user + mock_authenticate.return_value = self.user + + init_response = self.client.get(reverse("digid_oidc:init")) - request = RequestFactory().get(reverse("digid_oidc:callback")) + assert "oidc_states" in self.client.session - result = auth.authenticate(request) + state = furl(init_response["Location"]).query.params["state"] + nonce = self.client.session["oidc_states"][state]["nonce"] + # set up a request + callback_request = RequestFactory().get( + reverse("digid_oidc:callback"), + {"state": state, "nonce": nonce}, + ) + callback_request.session = self.client.session + store_config(callback_request) + + result = auth.authenticate(callback_request) self.assertEqual(result, self.user) + # django keeps track of which backend was used to authenticate + self.assertEqual( + result.backend, "open_inwoner.accounts.backends.DigiDEHerkenningOIDCBackend" + ) @override_settings( AUTHENTICATION_BACKENDS=[ - "digid_eherkenning_oidc_generics.backends.OIDCAuthenticationDigiDBackend", - "digid_eherkenning_oidc_generics.backends.OIDCAuthenticationEHerkenningBackend", + "open_inwoner.accounts.backends.DigiDEHerkenningOIDCBackend", "open_inwoner.accounts.backends.CustomOIDCBackend", ] ) @patch( - "mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.authenticate", + "mozilla_django_oidc_db.backends.BaseBackend.authenticate", side_effect=Exception, ) @patch("open_inwoner.accounts.backends.CustomOIDCBackend.authenticate") - def test_admin_oidc_use_correct_backend( + def test_admin_oidc_selects_correct_backend( self, mock_authenticate, mock_digid_eherkenning_authenticate ): """ @@ -63,9 +72,21 @@ def test_admin_oidc_use_correct_backend( their callback before trying to authenticate """ mock_authenticate.return_value = self.user + init_response = self.client.get(reverse("oidc_authentication_init")) + assert "oidc_states" in self.client.session + state = furl(init_response["Location"]).query.params["state"] + nonce = self.client.session["oidc_states"][state]["nonce"] + # set up a request + callback_request = RequestFactory().get( + reverse("oidc_authentication_callback"), + {"state": state, "nonce": nonce}, + ) + callback_request.session = self.client.session + store_config(callback_request) - request = RequestFactory().get(reverse("oidc_authentication_callback")) - - result = auth.authenticate(request) + result = auth.authenticate(callback_request) self.assertEqual(result, self.user) + self.assertEqual( + result.backend, "open_inwoner.accounts.backends.CustomOIDCBackend" + ) diff --git a/src/open_inwoner/accounts/tests/test_oidc_views.py b/src/open_inwoner/accounts/tests/test_oidc_views.py index c788e040ca..3453c6a4d9 100644 --- a/src/open_inwoner/accounts/tests/test_oidc_views.py +++ b/src/open_inwoner/accounts/tests/test_oidc_views.py @@ -15,11 +15,7 @@ from mozilla_django_oidc_db.models import OpenIDConnectConfig from pyquery import PyQuery as PQ -from digid_eherkenning_oidc_generics.models import ( - OpenIDConnectDigiDConfig, - OpenIDConnectEHerkenningConfig, -) -from digid_eherkenning_oidc_generics.views import ( +from open_inwoner.accounts.views.auth_oidc import ( GENERIC_DIGID_ERROR_MSG, GENERIC_EHERKENNING_ERROR_MSG, ) @@ -31,6 +27,7 @@ from ...cms.profile.cms_apps import ProfileApphook from ...cms.tests import cms_tools from ..choices import LoginTypeChoices +from ..models import OpenIDDigiDConfig, OpenIDEHerkenningConfig from .factories import DigidUserFactory, UserFactory, eHerkenningUserFactory User = get_user_model() @@ -94,7 +91,7 @@ def setUpClass(cls): @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.verify_token") @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.get_token") @patch( - "mozilla_django_oidc_db.mixins.OpenIDConnectConfig.get_solo", + "mozilla_django_oidc_db.models.OpenIDConnectConfig.get_solo", return_value=OpenIDConnectConfig(id=1, enabled=True, make_users_staff=True), ) @patch( @@ -119,7 +116,12 @@ def test_existing_email_updates_admin_user( user = UserFactory.create(email="existing_user@example.com") self.assertEqual(user.oidc_id, "") session = self.client.session - session["oidc_states"] = {"mock": {"nonce": "nonce"}} + session["oidc_states"] = { + "mock": { + "nonce": "nonce", + "config_class": "mozilla_django_oidc_db.OpenIDConnectConfig", + } + } session.save() callback_url = reverse("oidc_authentication_callback") @@ -145,7 +147,7 @@ def test_existing_email_updates_admin_user( @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.verify_token") @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.get_token") @patch( - "mozilla_django_oidc_db.mixins.OpenIDConnectConfig.get_solo", + "mozilla_django_oidc_db.models.OpenIDConnectConfig.get_solo", return_value=OpenIDConnectConfig(id=1, enabled=True, make_users_staff=False), ) @patch( @@ -170,7 +172,12 @@ def test_existing_email_updates_regular_user( user = UserFactory.create(email="existing_user@example.com") self.assertEqual(user.oidc_id, "") session = self.client.session - session["oidc_states"] = {"mock": {"nonce": "nonce"}} + session["oidc_states"] = { + "mock": { + "nonce": "nonce", + "config_class": "mozilla_django_oidc_db.OpenIDConnectConfig", + } + } session.save() callback_url = reverse("oidc_authentication_callback") @@ -196,12 +203,12 @@ def test_existing_email_updates_regular_user( @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.verify_token") @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.get_token") @patch( - "mozilla_django_oidc_db.mixins.OpenIDConnectConfig.get_solo", + "mozilla_django_oidc_db.models.OpenIDConnectConfig.get_solo", return_value=OpenIDConnectConfig( id=1, enabled=True, make_users_staff=False, - claim_mapping={"first_name": "first_name"}, + claim_mapping={"first_name": ["first_name"]}, ), ) @patch( @@ -228,7 +235,12 @@ def test_existing_oidc_id_updates_regular_user( oidc_id="some_username", first_name="Foo", login_type=LoginTypeChoices.oidc ) session = self.client.session - session["oidc_states"] = {"mock": {"nonce": "nonce"}} + session["oidc_states"] = { + "mock": { + "nonce": "nonce", + "config_class": "mozilla_django_oidc_db.OpenIDConnectConfig", + } + } session.save() callback_url = reverse("oidc_authentication_callback") @@ -254,7 +266,7 @@ def test_existing_oidc_id_updates_regular_user( @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.verify_token") @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.get_token") @patch( - "mozilla_django_oidc_db.mixins.OpenIDConnectConfig.get_solo", + "mozilla_django_oidc_db.models.OpenIDConnectConfig.get_solo", return_value=OpenIDConnectConfig(id=1, enabled=True), ) @patch( @@ -281,7 +293,12 @@ def test_existing_case_sensitive_email_updates_user( ) self.assertEqual(user.oidc_id, "") session = self.client.session - session["oidc_states"] = {"mock": {"nonce": "nonce"}} + session["oidc_states"] = { + "mock": { + "nonce": "nonce", + "config_class": "mozilla_django_oidc_db.OpenIDConnectConfig", + } + } session.save() callback_url = reverse("oidc_authentication_callback") @@ -309,7 +326,7 @@ def test_existing_case_sensitive_email_updates_user( @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.verify_token") @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.get_token") @patch( - "mozilla_django_oidc_db.mixins.OpenIDConnectConfig.get_solo", + "mozilla_django_oidc_db.models.OpenIDConnectConfig.get_solo", return_value=OpenIDConnectConfig(id=1, enabled=True, make_users_staff=True), ) @patch( @@ -332,7 +349,12 @@ def test_new_admin_user_is_created_when_new_email( } UserFactory.create(email="existing_user@example.com") session = self.client.session - session["oidc_states"] = {"mock": {"nonce": "nonce"}} + session["oidc_states"] = { + "mock": { + "nonce": "nonce", + "config_class": "mozilla_django_oidc_db.OpenIDConnectConfig", + } + } session.save() callback_url = reverse("oidc_authentication_callback") @@ -358,7 +380,7 @@ def test_new_admin_user_is_created_when_new_email( @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.verify_token") @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.get_token") @patch( - "mozilla_django_oidc_db.mixins.OpenIDConnectConfig.get_solo", + "mozilla_django_oidc_db.models.OpenIDConnectConfig.get_solo", return_value=OpenIDConnectConfig(id=1, enabled=True, make_users_staff=False), ) @patch( @@ -381,7 +403,12 @@ def test_new_regular_user_is_created_when_new_email( } UserFactory.create(email="existing_user@example.com") session = self.client.session - session["oidc_states"] = {"mock": {"nonce": "nonce"}} + session["oidc_states"] = { + "mock": { + "nonce": "nonce", + "config_class": "mozilla_django_oidc_db.OpenIDConnectConfig", + } + } session.save() callback_url = reverse("oidc_authentication_callback") @@ -412,7 +439,7 @@ def test_error_page_direct_access_forbidden(self): @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.verify_token") @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.get_token") @patch( - "mozilla_django_oidc_db.mixins.OpenIDConnectConfig.get_solo", + "mozilla_django_oidc_db.models.OpenIDConnectConfig.get_solo", return_value=OpenIDConnectConfig(id=1, enabled=True), ) @patch( @@ -443,7 +470,12 @@ def test_error_first_cleared_after_succesful_login( self.assertEqual(response.status_code, 200) with self.subTest("after succesful login"): - session["oidc_states"] = {"mock": {"nonce": "nonce"}} + session["oidc_states"] = { + "mock": { + "nonce": "nonce", + "config_class": "mozilla_django_oidc_db.OpenIDConnectConfig", + } + } session.save() callback_url = reverse("oidc_authentication_callback") @@ -476,10 +508,8 @@ def setUpClass(cls): @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.verify_token") @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.get_token") @patch( - "digid_eherkenning_oidc_generics.models.OpenIDConnectDigiDConfig.get_solo", - return_value=OpenIDConnectDigiDConfig( - id=1, enabled=True, identifier_claim_name="sub" - ), + "open_inwoner.accounts.models.OpenIDDigiDConfig.get_solo", + return_value=OpenIDDigiDConfig(id=1, enabled=True, bsn_claim=["sub"]), ) def test_existing_bsn_creates_no_new_user( self, @@ -505,7 +535,12 @@ def test_existing_bsn_creates_no_new_user( ) self.assertEqual(user.oidc_id, "") session = self.client.session - session["oidc_states"] = {"mock": {"nonce": "nonce"}} + session["oidc_states"] = { + "mock": { + "nonce": "nonce", + "config_class": "accounts.OpenIDDigiDConfig", + } + } session.save() callback_url = reverse("digid_oidc:callback") @@ -537,10 +572,8 @@ def test_existing_bsn_creates_no_new_user( @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.verify_token") @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.get_token") @patch( - "digid_eherkenning_oidc_generics.models.OpenIDConnectDigiDConfig.get_solo", - return_value=OpenIDConnectDigiDConfig( - id=1, enabled=True, identifier_claim_name="sub" - ), + "open_inwoner.accounts.models.OpenIDDigiDConfig.get_solo", + return_value=OpenIDDigiDConfig(id=1, enabled=True, bsn_claim=["sub"]), ) def test_new_user_is_created_when_new_bsn( self, @@ -555,7 +588,12 @@ def test_new_user_is_created_when_new_bsn( mock_get_userinfo.return_value = {"sub": "000000000"} DigidUserFactory.create(bsn="123456782", email="existing_user@example.com") session = self.client.session - session["oidc_states"] = {"mock": {"nonce": "nonce"}} + session["oidc_states"] = { + "mock": { + "nonce": "nonce", + "config_class": "accounts.OpenIDDigiDConfig", + } + } session.save() callback_url = reverse("digid_oidc:callback") @@ -580,8 +618,8 @@ def test_new_user_is_created_when_new_bsn( self.assertEqual(new_user.login_type, LoginTypeChoices.digid) @patch( - "digid_eherkenning_oidc_generics.models.OpenIDConnectDigiDConfig.get_solo", - return_value=OpenIDConnectDigiDConfig( + "open_inwoner.accounts.models.OpenIDDigiDConfig.get_solo", + return_value=OpenIDDigiDConfig( id=1, enabled=True, oidc_op_logout_endpoint="http://localhost:8080/logout" ), ) @@ -592,7 +630,12 @@ def test_logout(self, mock_get_solo): ) self.client.force_login(user) session = self.client.session - session["oidc_states"] = {"mock": {"nonce": "nonce"}} + session["oidc_states"] = { + "mock": { + "nonce": "nonce", + "config_class": "accounts.OpenIDDigiDConfig", + } + } session["oidc_id_token"] = "foo" session.save() logout_url = reverse("digid_oidc:logout") @@ -601,18 +644,12 @@ def test_logout(self, mock_get_solo): # enter the logout flow with requests_mock.Mocker() as m: - logout_endpoint_url = str( - furl("http://localhost:8080/logout").set( - { - "id_token_hint": "foo", - } - ) - ) - m.get(logout_endpoint_url) + m.post("http://localhost:8080/logout") logout_response = self.client.get(logout_url) self.assertEqual(len(m.request_history), 1) - self.assertEqual(m.request_history[0].url, logout_endpoint_url) + self.assertEqual(m.request_history[0].url, "http://localhost:8080/logout") + self.assertEqual(m.request_history[0].body, "id_token_hint=foo") self.assertRedirects( logout_response, reverse("login"), fetch_redirect_response=False @@ -633,8 +670,8 @@ def test_error_page_direct_access(self): @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.verify_token") @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.get_token") @patch( - "digid_eherkenning_oidc_generics.models.OpenIDConnectDigiDConfig.get_solo", - return_value=OpenIDConnectDigiDConfig(id=1, enabled=True), + "open_inwoner.accounts.models.OpenIDDigiDConfig.get_solo", + return_value=OpenIDDigiDConfig(id=1, enabled=True), ) def test_error_first_cleared_after_succesful_login( self, @@ -660,7 +697,12 @@ def test_error_first_cleared_after_succesful_login( self.assertEqual(response.status_code, 200) with self.subTest("after succesful login"): - session["oidc_states"] = {"mock": {"nonce": "nonce"}} + session["oidc_states"] = { + "mock": { + "nonce": "nonce", + "config_class": "accounts.OpenIDDigiDConfig", + } + } session.save() callback_url = reverse("digid_oidc:callback") @@ -683,12 +725,8 @@ def test_error_first_cleared_after_succesful_login( @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.verify_token") @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.get_token") @patch( - "digid_eherkenning_oidc_generics.models.OpenIDConnectDigiDConfig.get_solo", - return_value=OpenIDConnectDigiDConfig( - id=1, - enabled=True, - error_message_mapping={"some mapped message": "Some Error"}, - ), + "open_inwoner.accounts.models.OpenIDDigiDConfig.get_solo", + return_value=OpenIDDigiDConfig(id=1, enabled=True), ) def test_login_error_message_mapped_in_config( self, @@ -704,7 +742,12 @@ def test_login_error_message_mapped_in_config( } session = self.client.session - session["oidc_states"] = {"mock": {"nonce": "nonce"}} + session["oidc_states"] = { + "mock": { + "nonce": "nonce", + "config_class": "accounts.OpenIDDigiDConfig", + } + } session.save() callback_url = reverse("digid_oidc:callback") @@ -713,7 +756,7 @@ def test_login_error_message_mapped_in_config( callback_url, { "error": "access_denied", - "error_description": "some mapped message", + "error_description": "The user cancelled", "state": "mock", }, ) @@ -732,15 +775,15 @@ def test_login_error_message_mapped_in_config( doc = PQ(login_response.content) error_msg = doc.find(".notification__content").text() - self.assertEqual(error_msg, "Some Error") + self.assertEqual(error_msg, "Je hebt het inloggen met DigiD geannuleerd.") @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.get_userinfo") @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.store_tokens") @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.verify_token") @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.get_token") @patch( - "digid_eherkenning_oidc_generics.models.OpenIDConnectDigiDConfig.get_solo", - return_value=OpenIDConnectDigiDConfig(id=1, enabled=True), + "open_inwoner.accounts.models.OpenIDDigiDConfig.get_solo", + return_value=OpenIDDigiDConfig(id=1, enabled=True), ) def test_login_error_message_not_mapped_in_config( self, @@ -756,7 +799,12 @@ def test_login_error_message_not_mapped_in_config( } session = self.client.session - session["oidc_states"] = {"mock": {"nonce": "nonce"}} + session["oidc_states"] = { + "mock": { + "nonce": "nonce", + "config_class": "accounts.OpenIDDigiDConfig", + } + } session.save() callback_url = reverse("digid_oidc:callback") @@ -765,7 +813,7 @@ def test_login_error_message_not_mapped_in_config( callback_url, { "error": "access_denied", - "error_description": "some unmapped message", + "error_description": "Some generic error", "state": "mock", }, ) @@ -791,8 +839,8 @@ def test_login_error_message_not_mapped_in_config( @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.verify_token") @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.get_token") @patch( - "digid_eherkenning_oidc_generics.models.OpenIDConnectDigiDConfig.get_solo", - return_value=OpenIDConnectDigiDConfig(id=1, enabled=True), + "open_inwoner.accounts.models.OpenIDDigiDConfig.get_solo", + return_value=OpenIDDigiDConfig(id=1, enabled=True), ) def test_login_validation_error( self, @@ -809,7 +857,12 @@ def test_login_validation_error( } session = self.client.session - session["oidc_states"] = {"mock": {"nonce": "nonce"}} + session["oidc_states"] = { + "mock": { + "nonce": "nonce", + "config_class": "accounts.OpenIDDigiDConfig", + } + } session.save() callback_url = reverse("digid_oidc:callback") @@ -839,8 +892,8 @@ def test_login_validation_error( @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.verify_token") @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.get_token") @patch( - "digid_eherkenning_oidc_generics.models.OpenIDConnectDigiDConfig.get_solo", - return_value=OpenIDConnectDigiDConfig( + "open_inwoner.accounts.models.OpenIDDigiDConfig.get_solo", + return_value=OpenIDDigiDConfig( id=1, enabled=True, oidc_op_authorization_endpoint="http://idp.local/auth" ), ) @@ -909,8 +962,8 @@ def test_redirect_after_login_with_registration( @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.verify_token") @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.get_token") @patch( - "digid_eherkenning_oidc_generics.models.OpenIDConnectDigiDConfig.get_solo", - return_value=OpenIDConnectDigiDConfig( + "open_inwoner.accounts.models.OpenIDDigiDConfig.get_solo", + return_value=OpenIDDigiDConfig( id=1, enabled=True, oidc_op_authorization_endpoint="http://idp.local/auth" ), ) @@ -969,9 +1022,9 @@ def setUpClass(cls): @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.verify_token") @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.get_token") @patch( - "digid_eherkenning_oidc_generics.models.OpenIDConnectEHerkenningConfig.get_solo", - return_value=OpenIDConnectEHerkenningConfig( - id=1, enabled=True, identifier_claim_name="sub" + "open_inwoner.accounts.models.OpenIDEHerkenningConfig.get_solo", + return_value=OpenIDEHerkenningConfig( + id=1, enabled=True, legal_subject_claim=["sub"] ), ) def test_existing_kvk_creates_no_new_user( @@ -1005,7 +1058,12 @@ def test_existing_kvk_creates_no_new_user( ) self.assertEqual(user.oidc_id, "") session = self.client.session - session["oidc_states"] = {"mock": {"nonce": "nonce"}} + session["oidc_states"] = { + "mock": { + "nonce": "nonce", + "config_class": "accounts.OpenIDEHerkenningConfig", + } + } session.save() callback_url = reverse("eherkenning_oidc:callback") @@ -1037,9 +1095,9 @@ def test_existing_kvk_creates_no_new_user( @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.verify_token") @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.get_token") @patch( - "digid_eherkenning_oidc_generics.models.OpenIDConnectEHerkenningConfig.get_solo", - return_value=OpenIDConnectEHerkenningConfig( - id=1, enabled=True, identifier_claim_name="sub" + "open_inwoner.accounts.models.OpenIDEHerkenningConfig.get_solo", + return_value=OpenIDEHerkenningConfig( + id=1, enabled=True, legal_subject_claim=["sub"] ), ) def test_new_user_is_created_when_new_kvk( @@ -1058,7 +1116,12 @@ def test_new_user_is_created_when_new_kvk( kvk="12345678", rsin="123456789", email="existing_user@example.com" ) session = self.client.session - session["oidc_states"] = {"mock": {"nonce": "nonce"}} + session["oidc_states"] = { + "mock": { + "nonce": "nonce", + "config_class": "accounts.OpenIDEHerkenningConfig", + } + } session.save() callback_url = reverse("eherkenning_oidc:callback") @@ -1084,9 +1147,12 @@ def test_new_user_is_created_when_new_kvk( self.assertEqual(new_user.login_type, LoginTypeChoices.eherkenning) @patch( - "digid_eherkenning_oidc_generics.models.OpenIDConnectEHerkenningConfig.get_solo", - return_value=OpenIDConnectEHerkenningConfig( - id=1, enabled=True, oidc_op_logout_endpoint="http://localhost:8080/logout" + "open_inwoner.accounts.models.OpenIDEHerkenningConfig.get_solo", + return_value=OpenIDEHerkenningConfig( + id=1, + enabled=True, + legal_subject_claim=["kvk"], + oidc_op_logout_endpoint="http://localhost:8080/logout", ), ) def test_logout(self, mock_get_solo): @@ -1096,7 +1162,12 @@ def test_logout(self, mock_get_solo): ) self.client.force_login(user) session = self.client.session - session["oidc_states"] = {"mock": {"nonce": "nonce"}} + session["oidc_states"] = { + "mock": { + "nonce": "nonce", + "config_class": "accounts.OpenIDEHerkenningConfig", + } + } session["oidc_id_token"] = "foo" session[KVK_BRANCH_SESSION_VARIABLE] = None session.save() @@ -1106,18 +1177,12 @@ def test_logout(self, mock_get_solo): # enter the logout flow with requests_mock.Mocker() as m: - logout_endpoint_url = str( - furl("http://localhost:8080/logout").set( - { - "id_token_hint": "foo", - } - ) - ) - m.get(logout_endpoint_url) - logout_response = self.client.get(logout_url, follow=False) + m.post("http://localhost:8080/logout") + logout_response = self.client.get(logout_url) self.assertEqual(len(m.request_history), 1) - self.assertEqual(m.request_history[0].url, logout_endpoint_url) + self.assertEqual(m.request_history[0].url, "http://localhost:8080/logout") + self.assertEqual(m.request_history[0].body, "id_token_hint=foo") self.assertRedirects( logout_response, reverse("login"), fetch_redirect_response=False @@ -1157,8 +1222,10 @@ def test_logout(self, mock_get_solo): autospec=True, ) @patch( - "digid_eherkenning_oidc_generics.models.OpenIDConnectEHerkenningConfig.get_solo", - return_value=OpenIDConnectEHerkenningConfig(id=1, enabled=True), + "open_inwoner.accounts.models.OpenIDEHerkenningConfig.get_solo", + return_value=OpenIDEHerkenningConfig( + id=1, enabled=True, legal_subject_claim=["kvk"] + ), autospec=True, ) def test_error_first_cleared_after_succesful_login( @@ -1188,7 +1255,12 @@ def test_error_first_cleared_after_succesful_login( self.assertEqual(response.status_code, 200) with self.subTest("after succesful login"): - session["oidc_states"] = {"mock": {"nonce": "nonce"}} + session["oidc_states"] = { + "mock": { + "nonce": "nonce", + "config_class": "accounts.OpenIDEHerkenningConfig", + } + } session.save() callback_url = reverse("eherkenning_oidc:callback") @@ -1211,11 +1283,9 @@ def test_error_first_cleared_after_succesful_login( @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.verify_token") @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.get_token") @patch( - "digid_eherkenning_oidc_generics.models.OpenIDConnectEHerkenningConfig.get_solo", - return_value=OpenIDConnectEHerkenningConfig( - id=1, - enabled=True, - error_message_mapping={"some mapped message": "Some Error"}, + "open_inwoner.accounts.models.OpenIDEHerkenningConfig.get_solo", + return_value=OpenIDEHerkenningConfig( + id=1, enabled=True, legal_subject_claim=["kvk"] ), ) def test_login_error_message_mapped_in_config( @@ -1232,7 +1302,12 @@ def test_login_error_message_mapped_in_config( } session = self.client.session - session["oidc_states"] = {"mock": {"nonce": "nonce"}} + session["oidc_states"] = { + "mock": { + "nonce": "nonce", + "config_class": "accounts.OpenIDEHerkenningConfig", + } + } session.save() callback_url = reverse("eherkenning_oidc:callback") @@ -1241,7 +1316,7 @@ def test_login_error_message_mapped_in_config( callback_url, { "error": "access_denied", - "error_description": "some mapped message", + "error_description": "The user cancelled", "state": "mock", }, ) @@ -1260,15 +1335,17 @@ def test_login_error_message_mapped_in_config( doc = PQ(login_response.content) error_msg = doc.find(".notification__content").text() - self.assertEqual(error_msg, "Some Error") + self.assertEqual(error_msg, "Je hebt het inloggen met eHerkenning geannuleerd.") @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.get_userinfo") @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.store_tokens") @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.verify_token") @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.get_token") @patch( - "digid_eherkenning_oidc_generics.models.OpenIDConnectEHerkenningConfig.get_solo", - return_value=OpenIDConnectEHerkenningConfig(id=1, enabled=True), + "open_inwoner.accounts.models.OpenIDEHerkenningConfig.get_solo", + return_value=OpenIDEHerkenningConfig( + id=1, enabled=True, legal_subject_claim=["kvk"] + ), ) def test_login_error_message_not_mapped_in_config( self, @@ -1284,7 +1361,12 @@ def test_login_error_message_not_mapped_in_config( } session = self.client.session - session["oidc_states"] = {"mock": {"nonce": "nonce"}} + session["oidc_states"] = { + "mock": { + "nonce": "nonce", + "config_class": "accounts.OpenIDEHerkenningConfig", + } + } session.save() callback_url = reverse("eherkenning_oidc:callback") @@ -1293,7 +1375,7 @@ def test_login_error_message_not_mapped_in_config( callback_url, { "error": "access_denied", - "error_description": "some unmapped message", + "error_description": "Some generic error", "state": "mock", }, ) @@ -1319,8 +1401,10 @@ def test_login_error_message_not_mapped_in_config( @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.verify_token") @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.get_token") @patch( - "digid_eherkenning_oidc_generics.models.OpenIDConnectEHerkenningConfig.get_solo", - return_value=OpenIDConnectEHerkenningConfig(id=1, enabled=True), + "open_inwoner.accounts.models.OpenIDEHerkenningConfig.get_solo", + return_value=OpenIDEHerkenningConfig( + id=1, enabled=True, legal_subject_claim=["kvk"] + ), ) def test_login_validation_error( self, @@ -1337,7 +1421,12 @@ def test_login_validation_error( } session = self.client.session - session["oidc_states"] = {"mock": {"nonce": "nonce"}} + session["oidc_states"] = { + "mock": { + "nonce": "nonce", + "config_class": "accounts.OpenIDEHerkenningConfig", + } + } session.save() callback_url = reverse("eherkenning_oidc:callback") @@ -1385,9 +1474,9 @@ def test_login_validation_error( autospec=True, ) @patch( - "digid_eherkenning_oidc_generics.models.OpenIDConnectEHerkenningConfig.get_solo", - return_value=OpenIDConnectEHerkenningConfig( - id=1, enabled=True, identifier_claim_name="sub" + "open_inwoner.accounts.models.OpenIDEHerkenningConfig.get_solo", + return_value=OpenIDEHerkenningConfig( + id=1, enabled=True, legal_subject_claim=["sub"] ), autospec=True, ) @@ -1411,7 +1500,12 @@ def test_login_as_eenmanszaak_blocked( mock_get_userinfo.return_value = {"sub": "00000000"} eHerkenningUserFactory.create(kvk="12345678", email="existing_user@example.com") session = self.client.session - session["oidc_states"] = {"mock": {"nonce": "nonce"}} + session["oidc_states"] = { + "mock": { + "nonce": "nonce", + "config_class": "accounts.OpenIDEHerkenningConfig", + } + } session.save() callback_url = reverse("eherkenning_oidc:callback") @@ -1462,9 +1556,12 @@ def test_login_as_eenmanszaak_blocked( autospec=True, ) @patch( - "digid_eherkenning_oidc_generics.models.OpenIDConnectEHerkenningConfig.get_solo", - return_value=OpenIDConnectEHerkenningConfig( - id=1, enabled=True, oidc_op_authorization_endpoint="http://idp.local/auth" + "open_inwoner.accounts.models.OpenIDEHerkenningConfig.get_solo", + return_value=OpenIDEHerkenningConfig( + id=1, + enabled=True, + legal_subject_claim=["kvk"], + oidc_op_authorization_endpoint="http://idp.local/auth", ), autospec=True, ) @@ -1577,9 +1674,12 @@ def test_redirect_after_login_with_registration_and_branch_selection( autospec=True, ) @patch( - "digid_eherkenning_oidc_generics.models.OpenIDConnectEHerkenningConfig.get_solo", - return_value=OpenIDConnectEHerkenningConfig( - id=1, enabled=True, oidc_op_authorization_endpoint="http://idp.local/auth" + "open_inwoner.accounts.models.OpenIDEHerkenningConfig.get_solo", + return_value=OpenIDEHerkenningConfig( + id=1, + enabled=True, + legal_subject_claim=["kvk"], + oidc_op_authorization_endpoint="http://idp.local/auth", ), autospec=True, ) @@ -1657,9 +1757,12 @@ def test_redirect_after_login_no_registration_with_branch_selection( @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.verify_token") @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.get_token") @patch( - "digid_eherkenning_oidc_generics.models.OpenIDConnectEHerkenningConfig.get_solo", - return_value=OpenIDConnectEHerkenningConfig( - id=1, enabled=True, oidc_op_authorization_endpoint="http://idp.local/auth" + "open_inwoner.accounts.models.OpenIDEHerkenningConfig.get_solo", + return_value=OpenIDEHerkenningConfig( + id=1, + enabled=True, + legal_subject_claim=["kvk"], + oidc_op_authorization_endpoint="http://idp.local/auth", ), ) def test_redirect_after_login_no_registration_and_no_branch_selection( diff --git a/src/open_inwoner/accounts/tests/test_profile_views.py b/src/open_inwoner/accounts/tests/test_profile_views.py index 2f10eace8a..cc80ac8fae 100644 --- a/src/open_inwoner/accounts/tests/test_profile_views.py +++ b/src/open_inwoner/accounts/tests/test_profile_views.py @@ -95,7 +95,7 @@ def test_show_correct_logout_button_for_login_type_default(self): self.assertEqual(logout_link.attr("href"), reverse("logout")) - @patch("digid_eherkenning_oidc_generics.models.OpenIDConnectDigiDConfig.get_solo") + @patch("open_inwoner.accounts.models.OpenIDDigiDConfig.get_solo") def test_show_correct_logout_button_for_login_type_digid(self, mock_solo): for oidc_enabled in [True, False]: with self.subTest(oidc_enabled=oidc_enabled): @@ -114,9 +114,7 @@ def test_show_correct_logout_button_for_login_type_digid(self, mock_solo): self.assertEqual(logout_link.attr("href"), logout_url) - @patch( - "digid_eherkenning_oidc_generics.models.OpenIDConnectEHerkenningConfig.get_solo" - ) + @patch("open_inwoner.accounts.models.OpenIDEHerkenningConfig.get_solo") def test_show_correct_logout_button_for_login_type_eherkenning(self, mock_solo): for oidc_enabled in [True, False]: with self.subTest(oidc_enabled=oidc_enabled): diff --git a/src/open_inwoner/accounts/views/__init__.py b/src/open_inwoner/accounts/views/__init__.py index 8517315ec7..3d8ee25b02 100644 --- a/src/open_inwoner/accounts/views/__init__.py +++ b/src/open_inwoner/accounts/views/__init__.py @@ -13,11 +13,19 @@ CustomDigiDAssertionConsumerServiceView, CustomeHerkenningAssertionConsumerServiceMockView, CustomeHerkenningAssertionConsumerServiceView, - CustomEHerkenningOIDCAuthenticationCallbackView, LogPasswordChangeView, LogPasswordResetConfirmView, LogPasswordResetView, ) +from .auth_oidc import ( + OIDCFailureView, + digid_callback, + digid_init, + digid_logout, + eherkenning_callback, + eherkenning_init, + eherkenning_logout, +) from .contacts import ( ContactApprovalView, ContactCreateView, @@ -82,5 +90,12 @@ "UserAppointmentsView", "CustomRegistrationView", "NecessaryFieldsUserView", - "CustomEHerkenningOIDCAuthenticationCallbackView", + # OIDC + "OIDCFailureView", + "digid_init", + "digid_callback", + "digid_logout", + "eherkenning_init", + "eherkenning_callback", + "eherkenning_logout", ] diff --git a/src/open_inwoner/accounts/views/auth.py b/src/open_inwoner/accounts/views/auth.py index 3051c01457..c4c407046d 100644 --- a/src/open_inwoner/accounts/views/auth.py +++ b/src/open_inwoner/accounts/views/auth.py @@ -19,9 +19,6 @@ from digid_eherkenning.views.digid import DigiDAssertionConsumerServiceView from digid_eherkenning.views.eherkenning import eHerkenningAssertionConsumerServiceView -from digid_eherkenning_oidc_generics.views import ( - eHerkenningOIDCAuthenticationCallbackView, -) from eherkenning.mock import eherkenning_conf from eherkenning.mock.views.eherkenning import ( eHerkenningAssertionConsumerServiceMockView, @@ -225,10 +222,3 @@ def get_success_url(self): del session["invite_url"] return super().get_success_url() - - -class CustomEHerkenningOIDCAuthenticationCallbackView( - BlockEenmanszaakLoginMixin, eHerkenningOIDCAuthenticationCallbackView -): - def get_failure_url(self): - return settings.LOGIN_URL diff --git a/src/open_inwoner/accounts/views/auth_oidc.py b/src/open_inwoner/accounts/views/auth_oidc.py new file mode 100644 index 0000000000..25f078e7e7 --- /dev/null +++ b/src/open_inwoner/accounts/views/auth_oidc.py @@ -0,0 +1,143 @@ +import logging + +from django.conf import settings +from django.contrib import auth, messages +from django.core.exceptions import ValidationError +from django.db import IntegrityError, transaction +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 + +from digid_eherkenning.oidc.models import BaseConfig +from digid_eherkenning.oidc.views import OIDCAuthenticationCallbackView +from mozilla_django_oidc_db.utils import do_op_logout +from mozilla_django_oidc_db.views import _OIDC_ERROR_SESSION_KEY, OIDCInit + +from ..models import OpenIDDigiDConfig, OpenIDEHerkenningConfig +from .auth import BlockEenmanszaakLoginMixin + +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" +) + + +# XXX consider replacing this with mozilla_django_oidc_db.views.AdminLoginFailure? +# Or at least, make it consistent in the library. +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 CallbackView(OIDCAuthenticationCallbackView): + expect_django_user = True + + failure_url = reverse_lazy("oidc-error") + generic_error_msg = "" + error_message_mapping: dict[tuple[str, str], str] + + def get(self, request): + try: + with transaction.atomic(): + response = super().get(request) + except (IntegrityError, ValidationError) as exc: + logger.exception( + "Something went wrong while attempting to authenticate via OIDC", + exc_info=exc, + ) + request.session[_OIDC_ERROR_SESSION_KEY] = str(self.generic_error_msg) + response = self.login_failure() + else: + # Upstream library doesn't do any error handling by default. + if _OIDC_ERROR_SESSION_KEY in request.session: + del request.session[_OIDC_ERROR_SESSION_KEY] + + if error_label := self._map_error(request): + request.session[_OIDC_ERROR_SESSION_KEY] = error_label + + return response + + def _map_error(self, request) -> str: + if not (error := request.GET.get("error")): + return "" + + error_description = request.GET.get("error_description", "") + + # Look up the combination of error code and description in the mapping. + mapped_error = self.error_message_mapping.get((error, error_description)) + return mapped_error or str(self.generic_error_msg) + + +class OIDCLogoutView(View): + config_class: type[BaseConfig] | None = None + + def get_success_url(self): + return resolve_url(settings.LOGOUT_REDIRECT_URL) + + def get(self, request): + assert self.config_class is not None + + if id_token := request.session.get("oidc_id_token"): + config = self.config_class.get_solo() + do_op_logout(config, 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 DigiDOIDCAuthenticationCallbackView(CallbackView): + generic_error_msg = GENERIC_DIGID_ERROR_MSG + error_message_mapping = { + ("access_denied", "The user cancelled"): ( + "Je hebt het inloggen met DigiD geannuleerd." + ) + } + + +class EHerkenningOIDCAuthenticationCallbackView( + BlockEenmanszaakLoginMixin, + CallbackView, +): + generic_error_msg = GENERIC_EHERKENNING_ERROR_MSG + error_message_mapping = { + ("access_denied", "The user cancelled"): ( + "Je hebt het inloggen met eHerkenning geannuleerd." + ) + } + + def get_failure_url(self): + return settings.LOGIN_URL + + +digid_init = OIDCInit.as_view(config_class=OpenIDDigiDConfig) +digid_callback = DigiDOIDCAuthenticationCallbackView.as_view() +digid_logout = OIDCLogoutView.as_view(config_class=OpenIDDigiDConfig) + +eherkenning_init = OIDCInit.as_view(config_class=OpenIDEHerkenningConfig) +eherkenning_callback = EHerkenningOIDCAuthenticationCallbackView.as_view() +eherkenning_logout = OIDCLogoutView.as_view(config_class=OpenIDEHerkenningConfig) diff --git a/src/open_inwoner/accounts/views/registration.py b/src/open_inwoner/accounts/views/registration.py index ebd954092d..b24f233244 100644 --- a/src/open_inwoner/accounts/views/registration.py +++ b/src/open_inwoner/accounts/views/registration.py @@ -11,10 +11,6 @@ from django_registration.backends.one_step.views import RegistrationView from furl import furl -from digid_eherkenning_oidc_generics.models import ( - OpenIDConnectDigiDConfig, - OpenIDConnectEHerkenningConfig, -) from open_inwoner.accounts.choices import NotificationChannelChoice from open_inwoner.accounts.views.mixins import KlantenAPIMixin from open_inwoner.configurations.models import SiteConfiguration @@ -24,7 +20,7 @@ from ...utils.text import html_tag_wrap_format from ...utils.url import get_next_url_from from ..forms import CustomRegistrationForm, NecessaryUserForm -from ..models import Invite, User +from ..models import Invite, OpenIDDigiDConfig, OpenIDEHerkenningConfig, User class InviteMixin(CommonPageMixin): @@ -105,7 +101,7 @@ def get_context_data(self, **kwargs): ) try: - config = OpenIDConnectDigiDConfig.get_solo() + config = OpenIDDigiDConfig.get_solo() if config.enabled: digid_url = reverse("digid_oidc:init") else: @@ -117,7 +113,7 @@ def get_context_data(self, **kwargs): context["digid_url"] = "" try: - config = OpenIDConnectEHerkenningConfig.get_solo() + config = OpenIDEHerkenningConfig.get_solo() if config.enabled: eherkenning_url = reverse("eherkenning_oidc:init") else: @@ -214,12 +210,13 @@ def page_title(self): def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) text = _( - "Om door te gaan moet je jouw e-mailadres {email} bevestigen, we hebben je een e-mail gestuurd naar dit adres." + "Er is een e-mail verstuurd naar {email}. Klik op de link in de mail om uw e-mailadres te bevestigen." + "Heef u geen e-mail ontvangen? Verstuur deze dan nog een keer via onderstaande knop." ) ctx["verification_text"] = html_tag_wrap_format( text, "strong", email=self.request.user.email ) - ctx["button_text"] = _("Verificatie email nogmaals verzenden") + ctx["button_text"] = _("Verstuur de e-mail opnieuw") return ctx diff --git a/src/open_inwoner/conf/app/setup_configuration.py b/src/open_inwoner/conf/app/setup_configuration.py index c82a885a82..478a93daad 100644 --- a/src/open_inwoner/conf/app/setup_configuration.py +++ b/src/open_inwoner/conf/app/setup_configuration.py @@ -212,8 +212,8 @@ # Authentication configuration variables # NOTE variables are namespaced with `DIGID_OIDC`, but some model field names also have `oidc_...` in them -DIGID_OIDC_CONFIG_ENABLE = config("DIGID_OIDC_CONFIG_ENABLE", False) -DIGID_OIDC_IDENTIFIER_CLAIM_NAME = config("DIGID_OIDC_IDENTIFIER_CLAIM_NAME", None) +DIGID_OIDC_CONFIG_ENABLE = config("DIGID_OIDC_CONFIG_ENABLE", True) +DIGID_OIDC_BSN_CLAIM = config("DIGID_OIDC_BSN_CLAIM", None) DIGID_OIDC_OIDC_RP_CLIENT_ID = config("DIGID_OIDC_OIDC_RP_CLIENT_ID", None) DIGID_OIDC_OIDC_RP_CLIENT_SECRET = config("DIGID_OIDC_OIDC_RP_CLIENT_SECRET", None) DIGID_OIDC_OIDC_RP_SIGN_ALGO = config("DIGID_OIDC_OIDC_RP_SIGN_ALGO", None) @@ -230,17 +230,15 @@ DIGID_OIDC_OIDC_RP_IDP_SIGN_KEY = config("DIGID_OIDC_OIDC_RP_IDP_SIGN_KEY", None) DIGID_OIDC_USERINFO_CLAIMS_SOURCE = config("DIGID_OIDC_USERINFO_CLAIMS_SOURCE", None) DIGID_OIDC_OIDC_OP_LOGOUT_ENDPOINT = config("DIGID_OIDC_OIDC_OP_LOGOUT_ENDPOINT", None) -DIGID_OIDC_ERROR_MESSAGE_MAPPING = config("DIGID_OIDC_ERROR_MESSAGE_MAPPING", None) DIGID_OIDC_OIDC_KEYCLOAK_IDP_HINT = config("DIGID_OIDC_OIDC_KEYCLOAK_IDP_HINT", None) DIGID_OIDC_OIDC_USE_NONCE = config("DIGID_OIDC_OIDC_USE_NONCE", None) DIGID_OIDC_OIDC_NONCE_SIZE = config("DIGID_OIDC_OIDC_NONCE_SIZE", None) DIGID_OIDC_OIDC_STATE_SIZE = config("DIGID_OIDC_OIDC_STATE_SIZE", None) -DIGID_OIDC_OIDC_EXEMPT_URLS = config("DIGID_OIDC_OIDC_EXEMPT_URLS", None) # NOTE variables are namespaced with `EHERKENNING_OIDC`, but some model field names also have `oidc_...` in them -EHERKENNING_OIDC_CONFIG_ENABLE = config("EHERKENNING_OIDC_CONFIG_ENABLE", False) -EHERKENNING_OIDC_IDENTIFIER_CLAIM_NAME = config( - "EHERKENNING_OIDC_IDENTIFIER_CLAIM_NAME", None +EHERKENNING_OIDC_CONFIG_ENABLE = config("EHERKENNING_OIDC_CONFIG_ENABLE", True) +EHERKENNING_OIDC_LEGAL_SUBJECT_CLAIM = config( + "EHERKENNING_OIDC_LEGAL_SUBJECT_CLAIM", None ) EHERKENNING_OIDC_OIDC_RP_CLIENT_ID = config("EHERKENNING_OIDC_OIDC_RP_CLIENT_ID", None) EHERKENNING_OIDC_OIDC_RP_CLIENT_SECRET = config( @@ -274,16 +272,12 @@ EHERKENNING_OIDC_OIDC_OP_LOGOUT_ENDPOINT = config( "EHERKENNING_OIDC_OIDC_OP_LOGOUT_ENDPOINT", None ) -EHERKENNING_OIDC_ERROR_MESSAGE_MAPPING = config( - "EHERKENNING_OIDC_ERROR_MESSAGE_MAPPING", None -) EHERKENNING_OIDC_OIDC_KEYCLOAK_IDP_HINT = config( "EHERKENNING_OIDC_OIDC_KEYCLOAK_IDP_HINT", None ) EHERKENNING_OIDC_OIDC_USE_NONCE = config("EHERKENNING_OIDC_OIDC_USE_NONCE", None) EHERKENNING_OIDC_OIDC_NONCE_SIZE = config("EHERKENNING_OIDC_OIDC_NONCE_SIZE", None) EHERKENNING_OIDC_OIDC_STATE_SIZE = config("EHERKENNING_OIDC_OIDC_STATE_SIZE", None) -EHERKENNING_OIDC_OIDC_EXEMPT_URLS = config("EHERKENNING_OIDC_OIDC_EXEMPT_URLS", None) # NOTE variables are namespaced with `ADMIN_OIDC`, but some model field names also have `oidc_...` in them ADMIN_OIDC_CONFIG_ENABLE = config("ADMIN_OIDC_CONFIG_ENABLE", default=False) @@ -303,6 +297,7 @@ ADMIN_OIDC_OIDC_OP_USER_ENDPOINT = config("ADMIN_OIDC_OIDC_OP_USER_ENDPOINT", None) ADMIN_OIDC_USERNAME_CLAIM = config("ADMIN_OIDC_USERNAME_CLAIM", None) ADMIN_OIDC_GROUPS_CLAIM = config("ADMIN_OIDC_GROUPS_CLAIM", None) +# XXX: this needs to be provided as a Mapping[str, list[str]] now instead of Mapping[str, str]! ADMIN_OIDC_CLAIM_MAPPING = config("ADMIN_OIDC_CLAIM_MAPPING", None) ADMIN_OIDC_SYNC_GROUPS = config("ADMIN_OIDC_SYNC_GROUPS", None) ADMIN_OIDC_SYNC_GROUPS_GLOB_PATTERN = config( @@ -314,7 +309,6 @@ ADMIN_OIDC_OIDC_USE_NONCE = config("ADMIN_OIDC_OIDC_USE_NONCE", None) ADMIN_OIDC_OIDC_NONCE_SIZE = config("ADMIN_OIDC_OIDC_NONCE_SIZE", None) ADMIN_OIDC_OIDC_STATE_SIZE = config("ADMIN_OIDC_OIDC_STATE_SIZE", None) -ADMIN_OIDC_OIDC_EXEMPT_URLS = config("ADMIN_OIDC_OIDC_EXEMPT_URLS", None) ADMIN_OIDC_USERINFO_CLAIMS_SOURCE = config("ADMIN_OIDC_USERINFO_CLAIMS_SOURCE", None) # diff --git a/src/open_inwoner/conf/base.py b/src/open_inwoner/conf/base.py index 50c455841c..21cf5f9513 100644 --- a/src/open_inwoner/conf/base.py +++ b/src/open_inwoner/conf/base.py @@ -175,6 +175,7 @@ "sniplates", "digid_eherkenning", "eherkenning", + "digid_eherkenning.oidc", "localflavor", "easy_thumbnails", # used by filer "image_cropping", @@ -195,7 +196,6 @@ "cspreports", "mozilla_django_oidc", "mozilla_django_oidc_db", - "digid_eherkenning_oidc_generics", "sessionprofile", "openformsclient", "django_htmx", @@ -496,10 +496,10 @@ AUTHENTICATION_BACKENDS = [ "open_inwoner.accounts.backends.CustomAxesBackend", "open_inwoner.accounts.backends.UserModelEmailBackend", + "django.contrib.auth.backends.ModelBackend", "digid_eherkenning.backends.DigiDBackend", "eherkenning.backends.eHerkenningBackend", - "digid_eherkenning_oidc_generics.backends.OIDCAuthenticationDigiDBackend", - "digid_eherkenning_oidc_generics.backends.OIDCAuthenticationEHerkenningBackend", + "open_inwoner.accounts.backends.DigiDEHerkenningOIDCBackend", "open_inwoner.accounts.backends.CustomOIDCBackend", ] @@ -754,7 +754,7 @@ # SENTRY - error monitoring # SENTRY_DSN = config("SENTRY_DSN", None) -RELEASE = "v1.21.0" # get_current_version() +RELEASE = "v1.22.0" # get_current_version() PRIVATE_MEDIA_ROOT = os.path.join(BASE_DIR, "private_media") FILER_ROOT = os.path.join(BASE_DIR, "media", "filer") diff --git a/src/open_inwoner/conf/ci.py b/src/open_inwoner/conf/ci.py index c4f288bb89..13254f7c65 100644 --- a/src/open_inwoner/conf/ci.py +++ b/src/open_inwoner/conf/ci.py @@ -36,8 +36,7 @@ # mock login like dev.py "digid_eherkenning.mock.backends.DigiDBackend", "eherkenning.mock.backends.eHerkenningBackend", - "digid_eherkenning_oidc_generics.backends.OIDCAuthenticationDigiDBackend", - "digid_eherkenning_oidc_generics.backends.OIDCAuthenticationEHerkenningBackend", + "open_inwoner.accounts.backends.DigiDEHerkenningOIDCBackend", "open_inwoner.accounts.backends.CustomOIDCBackend", ] diff --git a/src/open_inwoner/conf/fixtures/django-admin-index.json b/src/open_inwoner/conf/fixtures/django-admin-index.json index 3baa74d121..f279e26f2a 100644 --- a/src/open_inwoner/conf/fixtures/django-admin-index.json +++ b/src/open_inwoner/conf/fixtures/django-admin-index.json @@ -429,12 +429,12 @@ "eherkenningconfiguration" ], [ - "digid_eherkenning_oidc_generics", - "openidconnectdigidconfig" + "accounts", + "openiddigidconfig" ], [ - "digid_eherkenning_oidc_generics", - "openidconnecteherkenningconfig" + "accounts", + "openideherkenningconfig" ], [ "haalcentraal", diff --git a/src/open_inwoner/conf/production.py b/src/open_inwoner/conf/production.py index fb9d69bb5d..44163a88fe 100644 --- a/src/open_inwoner/conf/production.py +++ b/src/open_inwoner/conf/production.py @@ -16,8 +16,7 @@ "open_inwoner.accounts.backends.CustomAxesBackend", "open_inwoner.accounts.backends.UserModelEmailBackend", "django.contrib.auth.backends.ModelBackend", - "digid_eherkenning_oidc_generics.backends.OIDCAuthenticationDigiDBackend", - "digid_eherkenning_oidc_generics.backends.OIDCAuthenticationEHerkenningBackend", + "open_inwoner.accounts.backends.DigiDEHerkenningOIDCBackend", "open_inwoner.accounts.backends.CustomOIDCBackend", ] diff --git a/src/open_inwoner/configurations/bootstrap/auth.py b/src/open_inwoner/configurations/bootstrap/auth.py index e2fd44bef7..954390cffa 100644 --- a/src/open_inwoner/configurations/bootstrap/auth.py +++ b/src/open_inwoner/configurations/bootstrap/auth.py @@ -9,6 +9,8 @@ EherkenningConfigurationAdmin, ) from digid_eherkenning.models import DigidConfiguration, EherkenningConfiguration +from digid_eherkenning.oidc.admin import admin_modelform_factory +from django_jsonform.forms.fields import JSONFormField from django_setup_configuration.config_settings import ConfigSettings from django_setup_configuration.configuration import BaseConfigurationStep from django_setup_configuration.exceptions import ConfigurationRunFailed @@ -16,19 +18,36 @@ from mozilla_django_oidc_db.models import OpenIDConnectConfig from simple_certmanager.models import Certificate -from digid_eherkenning_oidc_generics.admin import ( - OpenIDConnectDigiDConfigForm, - OpenIDConnectEHerkenningConfigForm, -) -from digid_eherkenning_oidc_generics.models import ( - OpenIDConnectDigiDConfig, - OpenIDConnectEHerkenningConfig, -) +from open_inwoner.accounts.models import OpenIDDigiDConfig, OpenIDEHerkenningConfig from open_inwoner.configurations.models import SiteConfiguration from .utils import convert_setting_to_model_field_name, log_form_errors +class LOAValueMappingField(JSONFormField): + def to_python(self, value): + value = super().to_python(value) + # super class treats [] as empty (not wrong), but converts it to None, which + # doesn't pass the schema validation + if value is None: + value = [] + return value + + +def formfield_callback(model_field, **kwargs): + if model_field.name == "loa_value_mapping": + kwargs["form_class"] = LOAValueMappingField + return model_field.formfield(**kwargs) + + +OpenIDDigiDConfigForm = admin_modelform_factory( + OpenIDDigiDConfig, formfield_callback=formfield_callback +) +OpenIDEHerkenningConfigForm = admin_modelform_factory( + OpenIDEHerkenningConfig, formfield_callback=formfield_callback +) + + # # DigiD OIDC # @@ -41,16 +60,14 @@ class DigiDOIDCConfigurationStep(BaseConfigurationStep): config_settings = ConfigSettings( enable_setting="DIGID_OIDC_CONFIG_ENABLE", namespace="DIGID_OIDC", - models=[OpenIDConnectDigiDConfig], + models=[OpenIDDigiDConfig], required_settings=[ "DIGID_OIDC_OIDC_RP_CLIENT_ID", "DIGID_OIDC_OIDC_RP_CLIENT_SECRET", ], optional_settings=[ "DIGID_OIDC_ENABLED", - "DIGID_OIDC_ERROR_MESSAGE_MAPPING", - "DIGID_OIDC_IDENTIFIER_CLAIM_NAME", - "DIGID_OIDC_OIDC_EXEMPT_URLS", + "DIGID_OIDC_BSN_CLAIM", "DIGID_OIDC_OIDC_KEYCLOAK_IDP_HINT", "DIGID_OIDC_OIDC_NONCE_SIZE", "DIGID_OIDC_OIDC_OP_AUTHORIZATION_ENDPOINT", @@ -72,18 +89,18 @@ def is_enabled(self): return getattr(settings, self.config_settings.enable_setting, False) def is_configured(self) -> bool: - return OpenIDConnectDigiDConfig.get_solo().enabled + return OpenIDDigiDConfig.get_solo().enabled def configure(self): if not self.is_enabled(): return - config = OpenIDConnectDigiDConfig.get_solo() + config = OpenIDDigiDConfig.get_solo() # Use the model defaults form_data = { field.name: getattr(config, field.name) - for field in OpenIDConnectDigiDConfig._meta.fields + for field in OpenIDDigiDConfig._meta.fields } # Only override field values with settings if they are defined @@ -101,12 +118,8 @@ def configure(self): form_data["enabled"] = True - # Saving the form with the default error_message_mapping `{}` causes the save to fail - if not form_data["error_message_mapping"]: - del form_data["error_message_mapping"] - # Use the admin form to apply validation and fetch URLs from the discovery endpoint - form = OpenIDConnectDigiDConfigForm(data=form_data) + form = OpenIDDigiDConfigForm(data=form_data) if not form.is_valid(): raise ConfigurationRunFailed( f"Something went wrong while saving configuration: {form.errors}" @@ -133,7 +146,7 @@ class eHerkenningOIDCConfigurationStep(BaseConfigurationStep): config_settings = ConfigSettings( enable_setting="EHERKENNING_OIDC_CONFIG_ENABLE", namespace="EHERKENNING_OIDC", - models=[OpenIDConnectEHerkenningConfig], + models=[OpenIDEHerkenningConfig], update_fields=True, required_settings=[ "EHERKENNING_OIDC_OIDC_RP_CLIENT_ID", @@ -141,7 +154,7 @@ class eHerkenningOIDCConfigurationStep(BaseConfigurationStep): ], optional_settings=[ "EHERKENNING_OIDC_ENABLED", - "EHERKENNING_OIDC_IDENTIFIER_CLAIM_NAME", + "EHERKENNING_OIDC_LEGAL_SUBJECT_CLAIM", "EHERKENNING_OIDC_OIDC_RP_SCOPES_LIST", "EHERKENNING_OIDC_OIDC_RP_SIGN_ALGO", "EHERKENNING_OIDC_OIDC_RP_IDP_SIGN_KEY", @@ -152,28 +165,26 @@ class eHerkenningOIDCConfigurationStep(BaseConfigurationStep): "EHERKENNING_OIDC_OIDC_OP_USER_ENDPOINT", "EHERKENNING_OIDC_OIDC_OP_LOGOUT_ENDPOINT", "EHERKENNING_OIDC_USERINFO_CLAIMS_SOURCE", - "EHERKENNING_OIDC_ERROR_MESSAGE_MAPPING", "EHERKENNING_OIDC_OIDC_KEYCLOAK_IDP_HINT", "EHERKENNING_OIDC_OIDC_USE_NONCE", "EHERKENNING_OIDC_OIDC_NONCE_SIZE", "EHERKENNING_OIDC_OIDC_STATE_SIZE", - "EHERKENNING_OIDC_OIDC_EXEMPT_URLS", ], ) def is_configured(self) -> bool: - return OpenIDConnectEHerkenningConfig.get_solo().enabled + return OpenIDEHerkenningConfig.get_solo().enabled def configure(self): if not getattr(settings, self.config_settings.enable_setting, None): return - config = OpenIDConnectEHerkenningConfig.get_solo() + config = OpenIDEHerkenningConfig.get_solo() # Use the model defaults form_data = { field.name: getattr(config, field.name) - for field in OpenIDConnectEHerkenningConfig._meta.fields + for field in OpenIDEHerkenningConfig._meta.fields } # Only override field values with settings if they are defined @@ -191,12 +202,8 @@ def configure(self): form_data["enabled"] = True - # Saving the form with the default error_message_mapping `{}` causes the save to fail - if not form_data["error_message_mapping"]: - del form_data["error_message_mapping"] - # Use the admin form to apply validation and fetch URLs from the discovery endpoint - form = OpenIDConnectEHerkenningConfigForm(data=form_data) + form = OpenIDEHerkenningConfigForm(data=form_data) if not form.is_valid(): raise ConfigurationRunFailed( f"Something went wrong while saving configuration: {form.errors}" @@ -235,7 +242,6 @@ class AdminOIDCConfigurationStep(BaseConfigurationStep): "ADMIN_OIDC_CLAIM_MAPPING", "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", diff --git a/src/open_inwoner/configurations/tests/bootstrap/test_setup_auth_config.py b/src/open_inwoner/configurations/tests/bootstrap/test_setup_auth_config.py index 025390d9db..6e501a8599 100644 --- a/src/open_inwoner/configurations/tests/bootstrap/test_setup_auth_config.py +++ b/src/open_inwoner/configurations/tests/bootstrap/test_setup_auth_config.py @@ -23,10 +23,7 @@ from privates.test import temp_private_root from simple_certmanager.constants import CertificateTypes -from digid_eherkenning_oidc_generics.models import ( - OpenIDConnectDigiDConfig, - OpenIDConnectEHerkenningConfig, -) +from open_inwoner.accounts.models import OpenIDDigiDConfig, OpenIDEHerkenningConfig from open_inwoner.utils.test import ClearCachesMixin from ...bootstrap.auth import ( @@ -67,7 +64,7 @@ DIGID_OIDC_CONFIG_ENABLE=True, DIGID_OIDC_OIDC_RP_CLIENT_ID="client-id", DIGID_OIDC_OIDC_RP_CLIENT_SECRET="secret", - DIGID_OIDC_IDENTIFIER_CLAIM_NAME="claim_name", + DIGID_OIDC_BSN_CLAIM=["claim_name"], DIGID_OIDC_OIDC_RP_SCOPES_LIST=["openid", "bsn", "extra_scope"], DIGID_OIDC_OIDC_RP_SIGN_ALGO="RS256", DIGID_OIDC_OIDC_RP_IDP_SIGN_KEY="key", @@ -78,23 +75,21 @@ DIGID_OIDC_OIDC_OP_USER_ENDPOINT=f"{IDENTITY_PROVIDER}protocol/openid-connect/userinfo", DIGID_OIDC_OIDC_OP_LOGOUT_ENDPOINT=f"{IDENTITY_PROVIDER}protocol/openid-connect/logout", DIGID_OIDC_USERINFO_CLAIMS_SOURCE=UserInformationClaimsSources.id_token, - DIGID_OIDC_ERROR_MESSAGE_MAPPING={"some_error": "Some readable error"}, DIGID_OIDC_OIDC_KEYCLOAK_IDP_HINT="parameter", DIGID_OIDC_OIDC_USE_NONCE=False, DIGID_OIDC_OIDC_NONCE_SIZE=64, DIGID_OIDC_OIDC_STATE_SIZE=64, - DIGID_OIDC_OIDC_EXEMPT_URLS=["/foo"], ) class DigiDOIDCConfigurationTest(ClearCachesMixin, TestCase): def test_configure(self): DigiDOIDCConfigurationStep().configure() - config = OpenIDConnectDigiDConfig.get_solo() + config = OpenIDDigiDConfig.get_solo() self.assertTrue(config.enabled) self.assertEqual(config.oidc_rp_client_id, "client-id") self.assertEqual(config.oidc_rp_client_secret, "secret") - self.assertEqual(config.identifier_claim_name, "claim_name") + self.assertEqual(config.bsn_claim, ["claim_name"]) self.assertEqual(config.oidc_rp_scopes_list, ["openid", "bsn", "extra_scope"]) self.assertEqual(config.oidc_rp_sign_algo, "RS256") self.assertEqual(config.oidc_rp_idp_sign_key, "key") @@ -122,37 +117,31 @@ def test_configure(self): self.assertEqual( config.userinfo_claims_source, UserInformationClaimsSources.id_token ) - self.assertEqual( - config.error_message_mapping, {"some_error": "Some readable error"} - ) self.assertEqual(config.oidc_keycloak_idp_hint, "parameter") self.assertEqual(config.oidc_use_nonce, False) self.assertEqual(config.oidc_nonce_size, 64) self.assertEqual(config.oidc_state_size, 64) - self.assertEqual(config.oidc_exempt_urls, ["/foo"]) @override_settings( - DIGID_OIDC_IDENTIFIER_CLAIM_NAME=None, + DIGID_OIDC_BSN_CLAIM=None, DIGID_OIDC_OIDC_RP_SCOPES_LIST=None, DIGID_OIDC_OIDC_RP_SIGN_ALGO=None, DIGID_OIDC_OIDC_RP_IDP_SIGN_KEY=None, DIGID_OIDC_USERINFO_CLAIMS_SOURCE=None, - DIGID_OIDC_ERROR_MESSAGE_MAPPING=None, DIGID_OIDC_OIDC_KEYCLOAK_IDP_HINT=None, DIGID_OIDC_OIDC_USE_NONCE=None, DIGID_OIDC_OIDC_NONCE_SIZE=None, DIGID_OIDC_OIDC_STATE_SIZE=None, - DIGID_OIDC_OIDC_EXEMPT_URLS=None, ) def test_configure_use_defaults(self): DigiDOIDCConfigurationStep().configure() - config = OpenIDConnectDigiDConfig.get_solo() + config = OpenIDDigiDConfig.get_solo() self.assertTrue(config.enabled) self.assertEqual(config.oidc_rp_client_id, "client-id") self.assertEqual(config.oidc_rp_client_secret, "secret") - self.assertEqual(config.identifier_claim_name, "bsn") + self.assertEqual(config.bsn_claim, ["bsn"]) self.assertEqual(config.oidc_rp_scopes_list, ["openid", "bsn"]) self.assertEqual(config.oidc_rp_sign_algo, "HS256") self.assertEqual(config.oidc_rp_idp_sign_key, "") @@ -181,12 +170,10 @@ def test_configure_use_defaults(self): config.userinfo_claims_source, UserInformationClaimsSources.userinfo_endpoint, ) - self.assertEqual(config.error_message_mapping, {}) self.assertEqual(config.oidc_keycloak_idp_hint, "") self.assertEqual(config.oidc_use_nonce, True) self.assertEqual(config.oidc_nonce_size, 32) self.assertEqual(config.oidc_state_size, 32) - self.assertEqual(config.oidc_exempt_urls, []) @override_settings( DIGID_OIDC_OIDC_OP_DISCOVERY_ENDPOINT=IDENTITY_PROVIDER, @@ -205,7 +192,7 @@ def test_configure_use_discovery_endpoint(self, m): DigiDOIDCConfigurationStep().configure() - config = OpenIDConnectDigiDConfig.get_solo() + config = OpenIDDigiDConfig.get_solo() self.assertTrue(config.enabled) self.assertEqual(config.oidc_op_discovery_endpoint, IDENTITY_PROVIDER) @@ -257,7 +244,7 @@ def test_configure_failure(self, m): with self.assertRaises(ConfigurationRunFailed): DigiDOIDCConfigurationStep().configure() - self.assertFalse(OpenIDConnectDigiDConfig.get_solo().enabled) + self.assertFalse(OpenIDDigiDConfig.get_solo().enabled) @skip("Testing config for DigiD OIDC is not implemented yet") @requests_mock.Mocker() @@ -291,7 +278,7 @@ def test_disable_digid_oidc_config(self): EHERKENNING_OIDC_CONFIG_ENABLE=True, EHERKENNING_OIDC_OIDC_RP_CLIENT_ID="client-id", EHERKENNING_OIDC_OIDC_RP_CLIENT_SECRET="secret", - EHERKENNING_OIDC_IDENTIFIER_CLAIM_NAME="claim_name", + EHERKENNING_OIDC_LEGAL_SUBJECT_CLAIM=["claim_name"], EHERKENNING_OIDC_OIDC_RP_SCOPES_LIST=["openid", "kvk", "extra_scope"], EHERKENNING_OIDC_OIDC_RP_SIGN_ALGO="RS256", EHERKENNING_OIDC_OIDC_RP_IDP_SIGN_KEY="key", @@ -302,23 +289,21 @@ def test_disable_digid_oidc_config(self): EHERKENNING_OIDC_OIDC_OP_USER_ENDPOINT=f"{IDENTITY_PROVIDER}protocol/openid-connect/userinfo", EHERKENNING_OIDC_OIDC_OP_LOGOUT_ENDPOINT=f"{IDENTITY_PROVIDER}protocol/openid-connect/logout", EHERKENNING_OIDC_USERINFO_CLAIMS_SOURCE=UserInformationClaimsSources.id_token, - EHERKENNING_OIDC_ERROR_MESSAGE_MAPPING={"some_error": "Some readable error"}, EHERKENNING_OIDC_OIDC_KEYCLOAK_IDP_HINT="parameter", EHERKENNING_OIDC_OIDC_USE_NONCE=False, EHERKENNING_OIDC_OIDC_NONCE_SIZE=64, EHERKENNING_OIDC_OIDC_STATE_SIZE=64, - EHERKENNING_OIDC_OIDC_EXEMPT_URLS=["/foo"], ) class eHerkenningOIDCConfigurationTest(ClearCachesMixin, TestCase): def test_configure(self): eHerkenningOIDCConfigurationStep().configure() - config = OpenIDConnectEHerkenningConfig.get_solo() + config = OpenIDEHerkenningConfig.get_solo() self.assertTrue(config.enabled) self.assertEqual(config.oidc_rp_client_id, "client-id") self.assertEqual(config.oidc_rp_client_secret, "secret") - self.assertEqual(config.identifier_claim_name, "claim_name") + self.assertEqual(config.legal_subject_claim, ["claim_name"]) self.assertEqual(config.oidc_rp_scopes_list, ["openid", "kvk", "extra_scope"]) self.assertEqual(config.oidc_rp_sign_algo, "RS256") self.assertEqual(config.oidc_rp_idp_sign_key, "key") @@ -346,37 +331,33 @@ def test_configure(self): self.assertEqual( config.userinfo_claims_source, UserInformationClaimsSources.id_token ) - self.assertEqual( - config.error_message_mapping, {"some_error": "Some readable error"} - ) self.assertEqual(config.oidc_keycloak_idp_hint, "parameter") self.assertEqual(config.oidc_use_nonce, False) self.assertEqual(config.oidc_nonce_size, 64) self.assertEqual(config.oidc_state_size, 64) - self.assertEqual(config.oidc_exempt_urls, ["/foo"]) @override_settings( - EHERKENNING_OIDC_IDENTIFIER_CLAIM_NAME=None, + EHERKENNING_OIDC_LEGAL_SUBJECT_CLAIM=None, EHERKENNING_OIDC_OIDC_RP_SCOPES_LIST=None, EHERKENNING_OIDC_OIDC_RP_SIGN_ALGO=None, EHERKENNING_OIDC_OIDC_RP_IDP_SIGN_KEY=None, EHERKENNING_OIDC_USERINFO_CLAIMS_SOURCE=None, - EHERKENNING_OIDC_ERROR_MESSAGE_MAPPING=None, EHERKENNING_OIDC_OIDC_KEYCLOAK_IDP_HINT=None, EHERKENNING_OIDC_OIDC_USE_NONCE=None, EHERKENNING_OIDC_OIDC_NONCE_SIZE=None, EHERKENNING_OIDC_OIDC_STATE_SIZE=None, - EHERKENNING_OIDC_OIDC_EXEMPT_URLS=None, ) def test_configure_use_defaults(self): eHerkenningOIDCConfigurationStep().configure() - config = OpenIDConnectEHerkenningConfig.get_solo() + config = OpenIDEHerkenningConfig.get_solo() self.assertTrue(config.enabled) self.assertEqual(config.oidc_rp_client_id, "client-id") self.assertEqual(config.oidc_rp_client_secret, "secret") - self.assertEqual(config.identifier_claim_name, "kvk") + self.assertEqual( + config.legal_subject_claim, ["urn:etoegang:core:LegalSubjectID"] + ) self.assertEqual(config.oidc_rp_scopes_list, ["openid", "kvk"]) self.assertEqual(config.oidc_rp_sign_algo, "HS256") self.assertEqual(config.oidc_rp_idp_sign_key, "") @@ -405,12 +386,10 @@ def test_configure_use_defaults(self): config.userinfo_claims_source, UserInformationClaimsSources.userinfo_endpoint, ) - self.assertEqual(config.error_message_mapping, {}) self.assertEqual(config.oidc_keycloak_idp_hint, "") self.assertEqual(config.oidc_use_nonce, True) self.assertEqual(config.oidc_nonce_size, 32) self.assertEqual(config.oidc_state_size, 32) - self.assertEqual(config.oidc_exempt_urls, []) @override_settings( EHERKENNING_OIDC_OIDC_OP_DISCOVERY_ENDPOINT=IDENTITY_PROVIDER, @@ -429,7 +408,7 @@ def test_configure_use_discovery_endpoint(self, m): eHerkenningOIDCConfigurationStep().configure() - config = OpenIDConnectEHerkenningConfig.get_solo() + config = OpenIDEHerkenningConfig.get_solo() self.assertTrue(config.enabled) self.assertEqual(config.oidc_op_discovery_endpoint, IDENTITY_PROVIDER) @@ -481,7 +460,7 @@ def test_configure_failure(self, m): with self.assertRaises(ConfigurationRunFailed): eHerkenningOIDCConfigurationStep().configure() - self.assertFalse(OpenIDConnectEHerkenningConfig.get_solo().enabled) + self.assertFalse(OpenIDEHerkenningConfig.get_solo().enabled) @skip("Testing config for DigiD OIDC is not implemented yet") @requests_mock.Mocker() @@ -523,9 +502,9 @@ def test_disable_eherkenning_oidc_config(self): ADMIN_OIDC_OIDC_OP_AUTHORIZATION_ENDPOINT=f"{IDENTITY_PROVIDER}protocol/openid-connect/auth", ADMIN_OIDC_OIDC_OP_TOKEN_ENDPOINT=f"{IDENTITY_PROVIDER}protocol/openid-connect/token", ADMIN_OIDC_OIDC_OP_USER_ENDPOINT=f"{IDENTITY_PROVIDER}protocol/openid-connect/userinfo", - ADMIN_OIDC_USERNAME_CLAIM="claim_name", - ADMIN_OIDC_GROUPS_CLAIM="groups_claim_name", - ADMIN_OIDC_CLAIM_MAPPING={"first_name": "given_name"}, + ADMIN_OIDC_USERNAME_CLAIM=["claim_name"], + ADMIN_OIDC_GROUPS_CLAIM=["groups_claim_name"], + ADMIN_OIDC_CLAIM_MAPPING={"first_name": ["given_name"]}, ADMIN_OIDC_SYNC_GROUPS=False, ADMIN_OIDC_SYNC_GROUPS_GLOB_PATTERN="local.groups.*", ADMIN_OIDC_DEFAULT_GROUPS=["Admins", "Read-only"], @@ -534,7 +513,6 @@ def test_disable_eherkenning_oidc_config(self): ADMIN_OIDC_OIDC_USE_NONCE=False, ADMIN_OIDC_OIDC_NONCE_SIZE=48, ADMIN_OIDC_OIDC_STATE_SIZE=48, - ADMIN_OIDC_OIDC_EXEMPT_URLS=["http://testserver/some-endpoint"], ADMIN_OIDC_USERINFO_CLAIMS_SOURCE=UserInformationClaimsSources.id_token, ) class AdminOIDCConfigurationTest(ClearCachesMixin, TestCase): @@ -568,9 +546,9 @@ def test_configure(self): config.oidc_op_user_endpoint, f"{IDENTITY_PROVIDER}protocol/openid-connect/userinfo", ) - self.assertEqual(config.username_claim, "claim_name") - self.assertEqual(config.groups_claim, "groups_claim_name") - self.assertEqual(config.claim_mapping, {"first_name": "given_name"}) + self.assertEqual(config.username_claim, ["claim_name"]) + self.assertEqual(config.groups_claim, ["groups_claim_name"]) + self.assertEqual(config.claim_mapping, {"first_name": ["given_name"]}) self.assertEqual(config.sync_groups, False) self.assertEqual(config.sync_groups_glob_pattern, "local.groups.*") self.assertEqual( @@ -582,7 +560,6 @@ def test_configure(self): self.assertEqual(config.oidc_use_nonce, False) self.assertEqual(config.oidc_nonce_size, 48) self.assertEqual(config.oidc_state_size, 48) - self.assertEqual(config.oidc_exempt_urls, ["http://testserver/some-endpoint"]) self.assertEqual( config.userinfo_claims_source, UserInformationClaimsSources.id_token ) @@ -599,7 +576,6 @@ def test_configure(self): ADMIN_OIDC_OIDC_USE_NONCE=None, ADMIN_OIDC_OIDC_NONCE_SIZE=None, ADMIN_OIDC_OIDC_STATE_SIZE=None, - ADMIN_OIDC_OIDC_EXEMPT_URLS=None, ADMIN_OIDC_USERINFO_CLAIMS_SOURCE=None, ) def test_configure_use_defaults(self): @@ -630,11 +606,11 @@ def test_configure_use_defaults(self): config.oidc_op_user_endpoint, f"{IDENTITY_PROVIDER}protocol/openid-connect/userinfo", ) - self.assertEqual(config.username_claim, "sub") - self.assertEqual(config.groups_claim, "groups_claim_name") + self.assertEqual(config.username_claim, ["sub"]) + self.assertEqual(config.groups_claim, ["groups_claim_name"]) self.assertEqual( config.claim_mapping, - {"last_name": "family_name", "first_name": "given_name"}, + {"last_name": ["family_name"], "first_name": ["given_name"]}, ) self.assertEqual(config.sync_groups, True) self.assertEqual(config.sync_groups_glob_pattern, "*") @@ -647,7 +623,6 @@ def test_configure_use_defaults(self): self.assertEqual(config.oidc_use_nonce, True) self.assertEqual(config.oidc_nonce_size, 32) self.assertEqual(config.oidc_state_size, 32) - self.assertEqual(config.oidc_exempt_urls, []) self.assertEqual( config.userinfo_claims_source, UserInformationClaimsSources.userinfo_endpoint, diff --git a/src/open_inwoner/configurations/validators.py b/src/open_inwoner/configurations/validators.py index 47a0fae66e..d6d218f3ef 100644 --- a/src/open_inwoner/configurations/validators.py +++ b/src/open_inwoner/configurations/validators.py @@ -1,7 +1,7 @@ from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ -from mozilla_django_oidc_db.mixins import OpenIDConnectConfig +from mozilla_django_oidc_db.models import OpenIDConnectConfig from .choices import OpenIDDisplayChoices diff --git a/src/open_inwoner/openklant/tests/test_contactform.py b/src/open_inwoner/openklant/tests/test_contactform.py index 045c085d12..04d134a5dc 100644 --- a/src/open_inwoner/openklant/tests/test_contactform.py +++ b/src/open_inwoner/openklant/tests/test_contactform.py @@ -405,6 +405,63 @@ def test_submit_and_register_anon_via_api_without_klant( self.assertTimelineLog("registered contactmoment by API") mock_send_confirm.assert_called_once_with("foo@example.com", subject.subject) + def test_submit_and_register_anon_via_api_without_klant_does_not_send_empty_email_or_telephone( + self, m, mock_send_confirm, mock_captcha + ): + config = OpenKlantConfig.get_solo() + config.register_contact_moment = True + config.register_bronorganisatie_rsin = "123456789" + config.register_type = "Melding" + config.register_channel = "contactformulier" + config.register_employee_id = "FooVonBar" + config.save() + + MockAPICreateData.setUpServices() + data = MockAPICreateData() + data.install_mocks_anon_without_klant(m) + + subject = ContactFormSubjectFactory( + config=config, + subject="Aanvraag document", + subject_code="afdeling-xyz", + ) + + for contact_details in ( + {"phonenumber": "+31612345678", "email": ""}, + {"phonenumber": "", "email": "foo@example.com"}, + ): + with self.subTest(): + m.reset_mock() + response = self.app.get(self.url) + form = response.forms["contactmoment-form"] + form["subject"].select(text=subject.subject) + form["first_name"] = "Foo" + form["infix"] = "de" + form["last_name"] = "Bar" + form["question"] = "foobar" + form["phonenumber"] = contact_details["phonenumber"] + form["email"] = contact_details["email"] + + response = form.submit().follow() + + contactmoment_create_data = data.matchers[1].request_history[0].json() + contactgegevens = contactmoment_create_data["contactgegevens"] + + if contact_details["email"]: + self.assertEqual( + contactgegevens["emailadres"], contact_details["email"] + ) + else: + self.assertNotIn("emailadres", contactgegevens.keys()) + + if contact_details["phonenumber"]: + self.assertEqual( + contactgegevens["telefoonnummer"], + contact_details["phonenumber"], + ) + else: + self.assertNotIn("telefoonnummer", contactgegevens.keys()) + def test_register_bsn_user_via_api_without_id( self, m, mock_send_confirm, mock_captcha ): diff --git a/src/open_inwoner/openklant/views/contactform.py b/src/open_inwoner/openklant/views/contactform.py index 4f10653965..108b9a2a6b 100644 --- a/src/open_inwoner/openklant/views/contactform.py +++ b/src/open_inwoner/openklant/views/contactform.py @@ -234,10 +234,16 @@ def register_by_api(self, form, config: OpenKlantConfig) -> tuple[bool, str]: } if not self.request.user.is_authenticated: - data["contactgegevens"] = { - "emailadres": form.cleaned_data["email"], - "telefoonnummer": form.cleaned_data["phonenumber"], - } + # Ensure we don't send an empty (and thus invalid) email or phonenumber + contactgegevens = {} + if form.cleaned_data["email"]: + contactgegevens["emailadres"] = form.cleaned_data["email"] + + if form.cleaned_data["phonenumber"]: + contactgegevens["telefoonnummer"] = form.cleaned_data["phonenumber"] + + if contactgegevens: + data["contactgegevens"] = contactgegevens if employee_id := config.register_employee_id: data["medewerkerIdentificatie"] = {"identificatie": employee_id} diff --git a/src/open_inwoner/urls.py b/src/open_inwoner/urls.py index 0f7c034905..fb6f179271 100644 --- a/src/open_inwoner/urls.py +++ b/src/open_inwoner/urls.py @@ -11,7 +11,6 @@ from maykin_2fa.urls import urlpatterns, webauthn_urlpatterns from mozilla_django_oidc_db.views import AdminLoginFailure -from digid_eherkenning_oidc_generics.views import OIDCFailureView from open_inwoner.accounts.forms import CustomRegistrationForm from open_inwoner.accounts.views import ( AddPhoneNumberWizardView, @@ -24,6 +23,7 @@ LogPasswordChangeView, LogPasswordResetConfirmView, LogPasswordResetView, + OIDCFailureView, PasswordResetView, ResendTokenView, VerifyTokenView, @@ -114,21 +114,12 @@ ), path("contactformulier/", ContactFormView.as_view(), name="contactform"), path("oidc/", include("mozilla_django_oidc.urls")), - path( - "digid-oidc/", - include( - "digid_eherkenning_oidc_generics.digid_urls", - ), - ), - path( - "eherkenning-oidc/", - include("open_inwoner.accounts.eherkenning_urls"), - ), + path("digid-oidc/", include("open_inwoner.accounts.digid_urls")), + path("eherkenning-oidc/", include("open_inwoner.accounts.eherkenning_urls")), path("login/failure/", OIDCFailureView.as_view(), name="oidc-error"), path("faq/", FAQView.as_view(), name="general_faq"), path("apimock/", include("open_inwoner.apimock.urls")), path("kvk/", include("open_inwoner.kvk.urls")), - # TODO move search to products cms app? path("", include("open_inwoner.search.urls", namespace="search")), re_path(r"^", include("cms.urls")), ]