From cbcc8cda589b32eda703d05b1f479ee4e28c5d03 Mon Sep 17 00:00:00 2001 From: SilviaAmAm Date: Thu, 31 Mar 2022 12:22:18 +0200 Subject: [PATCH 1/9] :construction: [#1471] Update OIDC module generics --- .../backends.py | 10 ++++-- .../migrations/0002_auto_20220331_1221.py | 33 +++++++++++++++++++ src/digid_eherkenning_oidc_generics/models.py | 16 +++++++++ 3 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 src/digid_eherkenning_oidc_generics/migrations/0002_auto_20220331_1221.py diff --git a/src/digid_eherkenning_oidc_generics/backends.py b/src/digid_eherkenning_oidc_generics/backends.py index fa21c3b449..3daabb2e11 100644 --- a/src/digid_eherkenning_oidc_generics/backends.py +++ b/src/digid_eherkenning_oidc_generics/backends.py @@ -16,6 +16,11 @@ class OIDCAuthenticationBackend(_OIDCAuthenticationBackend): session_key = "" claim_name_field = "identifier_claim_name" + def extract_claims(self, payload): + self.request.session[self.session_key] = payload[ + self.get_settings(self.claim_name_field) + ] + def get_or_create_user(self, access_token, id_token, payload): user_info = self.get_userinfo(access_token, id_token, payload) claims_verified = self.verify_claims(user_info) @@ -23,9 +28,8 @@ def get_or_create_user(self, access_token, id_token, payload): msg = "Claims verification failed" raise SuspiciousOperation(msg) - self.request.session[self.session_key] = payload[ - self.get_settings(self.claim_name_field) - ] + self.extract_claims(payload) + user = AnonymousUser() user.is_active = True return user diff --git a/src/digid_eherkenning_oidc_generics/migrations/0002_auto_20220331_1221.py b/src/digid_eherkenning_oidc_generics/migrations/0002_auto_20220331_1221.py new file mode 100644 index 0000000000..3f1e37ce82 --- /dev/null +++ b/src/digid_eherkenning_oidc_generics/migrations/0002_auto_20220331_1221.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.12 on 2022-03-31 10:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("digid_eherkenning_oidc_generics", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="openidconnectpublicconfig", + name="gemachtigde_claim_name", + field=models.CharField( + default="gemachtigde.bsn", + help_text="Name of the claim in which the BSN of the person representing someone else is stored", + max_length=50, + verbose_name="gemachtigde claim name", + ), + ), + migrations.AddField( + model_name="openidconnectpublicconfig", + name="vertegenwoordigde_claim_name", + field=models.CharField( + default="aanvrager.bsn", + help_text="Name of the claim in which the BSN of the person being represented is stored", + max_length=50, + verbose_name="vertegenwoordigde claim name", + ), + ), + ] diff --git a/src/digid_eherkenning_oidc_generics/models.py b/src/digid_eherkenning_oidc_generics/models.py index 8eb76d6671..b1587717ca 100644 --- a/src/digid_eherkenning_oidc_generics/models.py +++ b/src/digid_eherkenning_oidc_generics/models.py @@ -74,6 +74,22 @@ class OpenIDConnectPublicConfig(OpenIDConnectBaseConfig): "These scopes are hardcoded and must be supported by the identity provider" ), ) + vertegenwoordigde_claim_name = models.CharField( + verbose_name=_("vertegenwoordigde claim name"), + default="aanvrager.bsn", + max_length=50, + help_text=_( + "Name of the claim in which the BSN of the person being represented is stored" + ), + ) + gemachtigde_claim_name = models.CharField( + verbose_name=_("gemachtigde claim name"), + default="gemachtigde.bsn", + max_length=50, + help_text=_( + "Name of the claim in which the BSN of the person representing someone else is stored" + ), + ) @classproperty def custom_oidc_db_prefix(cls): From 6c614e3a49e6dbd7af24da0934e0da2ca7b3391b Mon Sep 17 00:00:00 2001 From: SilviaAmAm Date: Thu, 31 Mar 2022 12:26:27 +0200 Subject: [PATCH 2/9] :construction: [#1471] Add DigiD machtigen plugin --- .../digid_eherkenning_oidc/backends.py | 62 ++++++++++++++++++- .../digid_eherkenning_oidc/constants.py | 1 + .../digid_machtigen_urls.py | 24 +++++++ .../contrib/digid_eherkenning_oidc/plugin.py | 43 ++++++++++--- .../contrib/digid_eherkenning_oidc/views.py | 14 +++++ src/openforms/authentication/utils.py | 3 +- src/openforms/urls.py | 6 ++ 7 files changed, 143 insertions(+), 10 deletions(-) create mode 100644 src/openforms/authentication/contrib/digid_eherkenning_oidc/digid_machtigen_urls.py diff --git a/src/openforms/authentication/contrib/digid_eherkenning_oidc/backends.py b/src/openforms/authentication/contrib/digid_eherkenning_oidc/backends.py index 30ec03b87f..6bf48452ee 100644 --- a/src/openforms/authentication/contrib/digid_eherkenning_oidc/backends.py +++ b/src/openforms/authentication/contrib/digid_eherkenning_oidc/backends.py @@ -1,12 +1,20 @@ import logging +from copy import deepcopy + +from glom import PathAccessError, glom from digid_eherkenning_oidc_generics.backends import OIDCAuthenticationBackend from digid_eherkenning_oidc_generics.mixins import ( SoloConfigDigiDMixin, SoloConfigEHerkenningMixin, ) +from digid_eherkenning_oidc_generics.utils import obfuscate_claim -from .constants import DIGID_OIDC_AUTH_SESSION_KEY, EHERKENNING_OIDC_AUTH_SESSION_KEY +from .constants import ( + DIGID_MACHTIGEN_OIDC_AUTH_SESSION_KEY, + DIGID_OIDC_AUTH_SESSION_KEY, + EHERKENNING_OIDC_AUTH_SESSION_KEY, +) logger = logging.getLogger(__name__) @@ -27,3 +35,55 @@ class OIDCAuthenticationEHerkenningBackend( """ session_key = EHERKENNING_OIDC_AUTH_SESSION_KEY + + +class OIDCAuthenticationDigiDMachtigenBackend( + SoloConfigDigiDMixin, OIDCAuthenticationBackend +): + session_key = DIGID_MACHTIGEN_OIDC_AUTH_SESSION_KEY + + def extract_claims(self, payload: dict) -> None: + claim_names = [ + self.config.vertegenwoordigde_claim_name, + self.config.gemachtigde_claim_name, + ] + + self.request.session[self.session_key] = {} + for claim_name in claim_names: + self.request.session[self.session_key][claim_name] = glom( + payload, claim_name + ) + + def log_received_claims(self, claims: dict): + copied_claims = deepcopy(claims) + + def _obfuscate_claims_values(claims_to_obfuscate: dict) -> dict: + for key, value in claims_to_obfuscate.items(): + if isinstance(value, dict): + _obfuscate_claims_values(value) + else: + claims_to_obfuscate[key] = obfuscate_claim(value) + return claims_to_obfuscate + + obfuscated_claims = _obfuscate_claims_values(copied_claims) + logger.debug("OIDC claims received: %s", obfuscated_claims) + + def verify_claims(self, claims: dict) -> bool: + expected_claim_names = [ + self.config.vertegenwoordigde_claim_name, + self.config.gemachtigde_claim_name, + ] + + self.log_received_claims(claims) + + for expected_claim in expected_claim_names: + try: + glom(claims, expected_claim) + except PathAccessError: + logger.error( + "`%s` not in OIDC claims, cannot proceed with DigiD Machtigen authentication", + expected_claim, + ) + return False + + return True diff --git a/src/openforms/authentication/contrib/digid_eherkenning_oidc/constants.py b/src/openforms/authentication/contrib/digid_eherkenning_oidc/constants.py index a2f917db49..af3cb390cb 100644 --- a/src/openforms/authentication/contrib/digid_eherkenning_oidc/constants.py +++ b/src/openforms/authentication/contrib/digid_eherkenning_oidc/constants.py @@ -1,2 +1,3 @@ DIGID_OIDC_AUTH_SESSION_KEY = "digid_oidc:bsn" EHERKENNING_OIDC_AUTH_SESSION_KEY = "eherkenning_oidc:kvk" +DIGID_MACHTIGEN_OIDC_AUTH_SESSION_KEY = "digid_machtigen_oidc:machtigen" diff --git a/src/openforms/authentication/contrib/digid_eherkenning_oidc/digid_machtigen_urls.py b/src/openforms/authentication/contrib/digid_eherkenning_oidc/digid_machtigen_urls.py new file mode 100644 index 0000000000..66fb122364 --- /dev/null +++ b/src/openforms/authentication/contrib/digid_eherkenning_oidc/digid_machtigen_urls.py @@ -0,0 +1,24 @@ +from django.urls import path + +from mozilla_django_oidc.urls import urlpatterns + +from .views import ( + DigiDMachtigenOIDCAuthenticationCallbackView, + DigiDMachtigenOIDCAuthenticationRequestView, +) + +app_name = "digid_machtigen_oidc" + + +urlpatterns = [ + path( + "callback/", + DigiDMachtigenOIDCAuthenticationCallbackView.as_view(), + name="callback", + ), + path( + "authenticate/", + DigiDMachtigenOIDCAuthenticationRequestView.as_view(), + name="init", + ), +] + urlpatterns diff --git a/src/openforms/authentication/contrib/digid_eherkenning_oidc/plugin.py b/src/openforms/authentication/contrib/digid_eherkenning_oidc/plugin.py index 1313b16357..ca296a7a58 100644 --- a/src/openforms/authentication/contrib/digid_eherkenning_oidc/plugin.py +++ b/src/openforms/authentication/contrib/digid_eherkenning_oidc/plugin.py @@ -21,7 +21,11 @@ from ...constants import CO_SIGN_PARAMETER, FORM_AUTH_SESSION_KEY, AuthAttribute from ...exceptions import InvalidCoSignData from ...registry import register -from .constants import DIGID_OIDC_AUTH_SESSION_KEY, EHERKENNING_OIDC_AUTH_SESSION_KEY +from .constants import ( + DIGID_MACHTIGEN_OIDC_AUTH_SESSION_KEY, + DIGID_OIDC_AUTH_SESSION_KEY, + EHERKENNING_OIDC_AUTH_SESSION_KEY, +) class OIDCAuthentication(BasePlugin): @@ -59,6 +63,15 @@ def handle_co_sign( "fields": {}, } + def add_claims_to_sessions_if_not_cosigning(self, claim, request): + # set the session auth key only if we're not co-signing + if claim and CO_SIGN_PARAMETER not in request.GET: + request.session[FORM_AUTH_SESSION_KEY] = { + "plugin": self.identifier, + "attribute": self.provides_auth, + "value": claim, + } + def handle_return(self, request, form): """ Redirect to form URL. @@ -69,13 +82,7 @@ def handle_return(self, request, form): claim = request.session.get(self.session_key) - # set the session auth key only if we're not co-signing - if claim and CO_SIGN_PARAMETER not in request.GET: - request.session[FORM_AUTH_SESSION_KEY] = { - "plugin": self.identifier, - "attribute": self.provides_auth, - "value": claim, - } + self.add_claims_to_sessions_if_not_cosigning(claim, request) return HttpResponseRedirect(form_url) @@ -129,3 +136,23 @@ def get_label(self) -> str: def get_logo(self, request) -> Optional[LoginLogo]: return LoginLogo(title=self.get_label(), **get_eherkenning_logo(request)) + + +@register("digid_machtigen_oidc") +class DigiDMachtigenOIDCAuthentication(OIDCAuthentication): + verbose_name = _("DigiD Machtigen via OpenID Connect") + provides_auth = AuthAttribute.bsn + init_url = "digid_machtigen_oidc:init" + session_key = DIGID_MACHTIGEN_OIDC_AUTH_SESSION_KEY + config_class = OpenIDConnectPublicConfig + + def add_claims_to_sessions_if_not_cosigning(self, claim, request): + # set the session auth key only if we're not co-signing + if claim and CO_SIGN_PARAMETER not in request.GET: + config = OpenIDConnectPublicConfig.get_solo() + request.session[FORM_AUTH_SESSION_KEY] = { + "plugin": self.identifier, + "attribute": self.provides_auth, + "value": claim[config.vertegenwoordigde_claim_name], + "machtigen": request.session[DIGID_MACHTIGEN_OIDC_AUTH_SESSION_KEY], + } diff --git a/src/openforms/authentication/contrib/digid_eherkenning_oidc/views.py b/src/openforms/authentication/contrib/digid_eherkenning_oidc/views.py index 63e5e4d0ce..39786926a2 100644 --- a/src/openforms/authentication/contrib/digid_eherkenning_oidc/views.py +++ b/src/openforms/authentication/contrib/digid_eherkenning_oidc/views.py @@ -19,6 +19,7 @@ from ...views import BACKEND_OUTAGE_RESPONSE_PARAMETER from .backends import ( OIDCAuthenticationDigiDBackend, + OIDCAuthenticationDigiDMachtigenBackend, OIDCAuthenticationEHerkenningBackend, ) @@ -92,3 +93,16 @@ class eHerkenningOIDCAuthenticationCallbackView( ): plugin_identifier = "eherkenning_oidc" auth_backend_class = OIDCAuthenticationEHerkenningBackend + + +class DigiDMachtigenOIDCAuthenticationRequestView( + SoloConfigDigiDMixin, OIDCAuthenticationRequestView +): + plugin_identifier = "digid_machtigen_oidc" + + +class DigiDMachtigenOIDCAuthenticationCallbackView( + SoloConfigDigiDMixin, OIDCAuthenticationCallbackView +): + plugin_identifier = "digid_machtigen_oidc" + auth_backend_class = OIDCAuthenticationDigiDMachtigenBackend diff --git a/src/openforms/authentication/utils.py b/src/openforms/authentication/utils.py index 6189d34d64..3da8e10dc1 100644 --- a/src/openforms/authentication/utils.py +++ b/src/openforms/authentication/utils.py @@ -1,4 +1,4 @@ -from typing import Literal, TypedDict +from typing import Literal, Optional, TypedDict from openforms.submissions.models import Submission @@ -13,6 +13,7 @@ class FormAuth(TypedDict): AuthAttribute.pseudo, ] value: str + machtigen: Optional[dict] def store_auth_details(submission: Submission, form_auth: FormAuth) -> None: diff --git a/src/openforms/urls.py b/src/openforms/urls.py index aefb8b9de1..8a11ecbdc6 100644 --- a/src/openforms/urls.py +++ b/src/openforms/urls.py @@ -85,6 +85,12 @@ "openforms.authentication.contrib.digid_eherkenning_oidc.eherkenning_urls", ), ), + path( + "digid-machtigen-oidc/", + include( + "openforms.authentication.contrib.digid_eherkenning_oidc.digid_machtigen_urls", + ), + ), path("payment/", include("openforms.payments.urls", namespace="payments")), # NOTE: we dont use the User creation feature so don't enable all the mock views path("digid/", include("openforms.authentication.contrib.digid.urls")), From 683a771772f20e4b7792828fd77170097eadb6f6 Mon Sep 17 00:00:00 2001 From: SilviaAmAm Date: Thu, 31 Mar 2022 15:46:52 +0200 Subject: [PATCH 3/9] :construction: [#1471] Splitting DigiD/DigiD machtigen config models --- src/digid_eherkenning_oidc_generics/admin.py | 59 ++++++- .../digid_machtigen_settings.py | 2 + src/digid_eherkenning_oidc_generics/forms.py | 14 +- .../migrations/0002_auto_20220331_1221.py | 33 ---- .../0002_openidconnectdigidmachtigenconfig.py | 167 ++++++++++++++++++ src/digid_eherkenning_oidc_generics/mixins.py | 32 ++-- src/digid_eherkenning_oidc_generics/models.py | 25 ++- .../digid_eherkenning_oidc/backends.py | 17 +- .../contrib/digid_eherkenning_oidc/plugin.py | 12 +- .../contrib/digid_eherkenning_oidc/views.py | 5 +- 10 files changed, 312 insertions(+), 54 deletions(-) create mode 100644 src/digid_eherkenning_oidc_generics/digid_machtigen_settings.py delete mode 100644 src/digid_eherkenning_oidc_generics/migrations/0002_auto_20220331_1221.py create mode 100644 src/digid_eherkenning_oidc_generics/migrations/0002_openidconnectdigidmachtigenconfig.py diff --git a/src/digid_eherkenning_oidc_generics/admin.py b/src/digid_eherkenning_oidc_generics/admin.py index 9c79342e22..85e5bcdb64 100644 --- a/src/digid_eherkenning_oidc_generics/admin.py +++ b/src/digid_eherkenning_oidc_generics/admin.py @@ -4,8 +4,16 @@ from django_better_admin_arrayfield.admin.mixins import DynamicArrayMixin from solo.admin import SingletonModelAdmin -from .forms import OpenIDConnectEHerkenningConfigForm, OpenIDConnectPublicConfigForm -from .models import OpenIDConnectEHerkenningConfig, OpenIDConnectPublicConfig +from .forms import ( + OpenIDConnectDigiDMachtigenConfigForm, + OpenIDConnectEHerkenningConfigForm, + OpenIDConnectPublicConfigForm, +) +from .models import ( + OpenIDConnectDigiDMachtigenConfig, + OpenIDConnectEHerkenningConfig, + OpenIDConnectPublicConfig, +) class OpenIDConnectConfigBaseAdmin(DynamicArrayMixin, SingletonModelAdmin): @@ -52,3 +60,50 @@ class OpenIDConnectConfigDigiDAdmin(OpenIDConnectConfigBaseAdmin): @admin.register(OpenIDConnectEHerkenningConfig) class OpenIDConnectConfigEHerkenningAdmin(OpenIDConnectConfigBaseAdmin): form = OpenIDConnectEHerkenningConfigForm + + +@admin.register(OpenIDConnectDigiDMachtigenConfig) +class OpenIDConnectConfigDigiDMachtigenAdmin(DynamicArrayMixin, SingletonModelAdmin): + form = OpenIDConnectDigiDMachtigenConfigForm + + fieldsets = ( + ( + _("Activation"), + {"fields": ("enabled",)}, + ), + ( + _("Common settings"), + { + "fields": ( + "oidc_rp_client_id", + "oidc_rp_client_secret", + "oidc_rp_scopes_list", + "oidc_rp_sign_algo", + "oidc_rp_idp_sign_key", + ) + }, + ), + ( + _("Attributes to extract from claim"), + { + "fields": ( + "vertegenwoordigde_claim_name", + "gemachtigde_claim_name", + ) + }, + ), + ( + _("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",)}), + ) diff --git a/src/digid_eherkenning_oidc_generics/digid_machtigen_settings.py b/src/digid_eherkenning_oidc_generics/digid_machtigen_settings.py new file mode 100644 index 0000000000..b998079bf5 --- /dev/null +++ b/src/digid_eherkenning_oidc_generics/digid_machtigen_settings.py @@ -0,0 +1,2 @@ +DIGID_MACHTIGEN_CUSTOM_OIDC_DB_PREFIX = "digid_machtigen_oidc" +OIDC_AUTHENTICATION_CALLBACK_URL = "digid_machtigen_oidc:callback" diff --git a/src/digid_eherkenning_oidc_generics/forms.py b/src/digid_eherkenning_oidc_generics/forms.py index 972b32f18d..d9b67ac521 100644 --- a/src/digid_eherkenning_oidc_generics/forms.py +++ b/src/digid_eherkenning_oidc_generics/forms.py @@ -8,7 +8,11 @@ from openforms.forms.models import Form -from .models import OpenIDConnectEHerkenningConfig, OpenIDConnectPublicConfig +from .models import ( + OpenIDConnectDigiDMachtigenConfig, + OpenIDConnectEHerkenningConfig, + OpenIDConnectPublicConfig, +) OIDC_MAPPING = deepcopy(_OIDC_MAPPING) @@ -58,3 +62,11 @@ class OpenIDConnectEHerkenningConfigForm(OpenIDConnectBaseConfigForm): class Meta: model = OpenIDConnectEHerkenningConfig fields = "__all__" + + +class OpenIDConnectDigiDMachtigenConfigForm(OpenIDConnectBaseConfigForm): + plugin_identifier = "digid_machtigen_oidc" + + class Meta: + model = OpenIDConnectDigiDMachtigenConfig + fields = "__all__" diff --git a/src/digid_eherkenning_oidc_generics/migrations/0002_auto_20220331_1221.py b/src/digid_eherkenning_oidc_generics/migrations/0002_auto_20220331_1221.py deleted file mode 100644 index 3f1e37ce82..0000000000 --- a/src/digid_eherkenning_oidc_generics/migrations/0002_auto_20220331_1221.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 3.2.12 on 2022-03-31 10:21 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("digid_eherkenning_oidc_generics", "0001_initial"), - ] - - operations = [ - migrations.AddField( - model_name="openidconnectpublicconfig", - name="gemachtigde_claim_name", - field=models.CharField( - default="gemachtigde.bsn", - help_text="Name of the claim in which the BSN of the person representing someone else is stored", - max_length=50, - verbose_name="gemachtigde claim name", - ), - ), - migrations.AddField( - model_name="openidconnectpublicconfig", - name="vertegenwoordigde_claim_name", - field=models.CharField( - default="aanvrager.bsn", - help_text="Name of the claim in which the BSN of the person being represented is stored", - max_length=50, - verbose_name="vertegenwoordigde claim name", - ), - ), - ] diff --git a/src/digid_eherkenning_oidc_generics/migrations/0002_openidconnectdigidmachtigenconfig.py b/src/digid_eherkenning_oidc_generics/migrations/0002_openidconnectdigidmachtigenconfig.py new file mode 100644 index 0000000000..76916dc3fd --- /dev/null +++ b/src/digid_eherkenning_oidc_generics/migrations/0002_openidconnectdigidmachtigenconfig.py @@ -0,0 +1,167 @@ +# Generated by Django 3.2.12 on 2022-03-31 12:45 + +import digid_eherkenning_oidc_generics.models +from django.db import migrations, models +import django_better_admin_arrayfield.models.fields +import mozilla_django_oidc_db.models + + +class Migration(migrations.Migration): + + dependencies = [ + ("digid_eherkenning_oidc_generics", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="OpenIDConnectDigiDMachtigenConfig", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "enabled", + models.BooleanField( + default=False, + help_text="Indicates whether OpenID Connect for authentication/authorization is enabled", + verbose_name="enable", + ), + ), + ( + "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_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", + ), + ), + ( + "vertegenwoordigde_claim_name", + models.CharField( + default="aanvrager.bsn", + help_text="Name of the claim in which the BSN of the person being represented is stored", + max_length=50, + verbose_name="vertegenwoordigde claim name", + ), + ), + ( + "gemachtigde_claim_name", + models.CharField( + default="gemachtigde.bsn", + help_text="Name of the claim in which the BSN of the person representing someone else is stored", + max_length=50, + verbose_name="gemachtigde claim name", + ), + ), + ( + "oidc_rp_scopes_list", + django_better_admin_arrayfield.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 Machtigen", + }, + bases=(mozilla_django_oidc_db.models.CachingMixin, models.Model), + ), + ] diff --git a/src/digid_eherkenning_oidc_generics/mixins.py b/src/digid_eherkenning_oidc_generics/mixins.py index f395c13e3f..9cb0de09af 100644 --- a/src/digid_eherkenning_oidc_generics/mixins.py +++ b/src/digid_eherkenning_oidc_generics/mixins.py @@ -1,24 +1,36 @@ -from mozilla_django_oidc_db.mixins import SoloConfigMixin +from mozilla_django_oidc_db.mixins import SoloConfigMixin as _SoloConfigMixin +import digid_eherkenning_oidc_generics.digid_machtigen_settings as digid_machtigen_settings import digid_eherkenning_oidc_generics.digid_settings as digid_settings import digid_eherkenning_oidc_generics.eherkenning_settings as eherkenning_settings -from .models import OpenIDConnectEHerkenningConfig, OpenIDConnectPublicConfig +from .models import ( + OpenIDConnectDigiDMachtigenConfig, + OpenIDConnectEHerkenningConfig, + OpenIDConnectPublicConfig, +) -class SoloConfigDigiDMixin(SoloConfigMixin): - config_class = OpenIDConnectPublicConfig +class SoloConfigMixin(_SoloConfigMixin): + config_class = "" + settings_attribute = None def get_settings(self, attr, *args): - if hasattr(digid_settings, attr): - return getattr(digid_settings, attr) + if hasattr(self.settings_attribute, attr): + return getattr(self.settings_attribute, attr) return super().get_settings(attr, *args) +class SoloConfigDigiDMixin(SoloConfigMixin): + config_class = OpenIDConnectPublicConfig + settings_attribute = digid_settings + + class SoloConfigEHerkenningMixin(SoloConfigMixin): config_class = OpenIDConnectEHerkenningConfig + settings_attribute = eherkenning_settings - def get_settings(self, attr, *args): - if hasattr(eherkenning_settings, attr): - return getattr(eherkenning_settings, attr) - return super().get_settings(attr, *args) + +class SoloConfigDigiDMachtigenMixin(SoloConfigMixin): + config_class = OpenIDConnectDigiDMachtigenConfig + settings_attribute = digid_machtigen_settings diff --git a/src/digid_eherkenning_oidc_generics/models.py b/src/digid_eherkenning_oidc_generics/models.py index b1587717ca..eeadf25054 100644 --- a/src/digid_eherkenning_oidc_generics/models.py +++ b/src/digid_eherkenning_oidc_generics/models.py @@ -7,6 +7,7 @@ from openforms.authentication.constants import AuthAttribute +from .digid_machtigen_settings import DIGID_MACHTIGEN_CUSTOM_OIDC_DB_PREFIX from .digid_settings import DIGID_CUSTOM_OIDC_DB_PREFIX from .eherkenning_settings import EHERKENNING_CUSTOM_OIDC_DB_PREFIX @@ -74,6 +75,16 @@ class OpenIDConnectPublicConfig(OpenIDConnectBaseConfig): "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 OpenIDConnectDigiDMachtigenConfig(OpenIDConnectBaseConfig): vertegenwoordigde_claim_name = models.CharField( verbose_name=_("vertegenwoordigde claim name"), default="aanvrager.bsn", @@ -90,13 +101,23 @@ class OpenIDConnectPublicConfig(OpenIDConnectBaseConfig): "Name of the claim in which the BSN of the person representing someone else is stored" ), ) + 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 + return DIGID_MACHTIGEN_CUSTOM_OIDC_DB_PREFIX class Meta: - verbose_name = _("OpenID Connect configuration for DigiD") + verbose_name = _("OpenID Connect configuration for DigiD Machtigen") class OpenIDConnectEHerkenningConfig(OpenIDConnectBaseConfig): diff --git a/src/openforms/authentication/contrib/digid_eherkenning_oidc/backends.py b/src/openforms/authentication/contrib/digid_eherkenning_oidc/backends.py index 6bf48452ee..e49d89d5c3 100644 --- a/src/openforms/authentication/contrib/digid_eherkenning_oidc/backends.py +++ b/src/openforms/authentication/contrib/digid_eherkenning_oidc/backends.py @@ -2,9 +2,12 @@ from copy import deepcopy from glom import PathAccessError, glom +from django.contrib.auth.models import AnonymousUser +from django.core.exceptions import SuspiciousOperation from digid_eherkenning_oidc_generics.backends import OIDCAuthenticationBackend from digid_eherkenning_oidc_generics.mixins import ( + SoloConfigDigiDMachtigenMixin, SoloConfigDigiDMixin, SoloConfigEHerkenningMixin, ) @@ -38,10 +41,22 @@ class OIDCAuthenticationEHerkenningBackend( class OIDCAuthenticationDigiDMachtigenBackend( - SoloConfigDigiDMixin, OIDCAuthenticationBackend + SoloConfigDigiDMachtigenMixin, OIDCAuthenticationBackend ): session_key = DIGID_MACHTIGEN_OIDC_AUTH_SESSION_KEY + def get_or_create_user(self, access_token, id_token, payload): + claims_verified = self.verify_claims(payload) + if not claims_verified: + msg = "Claims verification failed" + raise SuspiciousOperation(msg) + + self.extract_claims(payload) + + user = AnonymousUser() + user.is_active = True + return user + def extract_claims(self, payload: dict) -> None: claim_names = [ self.config.vertegenwoordigde_claim_name, diff --git a/src/openforms/authentication/contrib/digid_eherkenning_oidc/plugin.py b/src/openforms/authentication/contrib/digid_eherkenning_oidc/plugin.py index ca296a7a58..1f45fc5c50 100644 --- a/src/openforms/authentication/contrib/digid_eherkenning_oidc/plugin.py +++ b/src/openforms/authentication/contrib/digid_eherkenning_oidc/plugin.py @@ -9,7 +9,7 @@ from digid_eherkenning_oidc_generics.models import ( OpenIDConnectEHerkenningConfig, - OpenIDConnectPublicConfig, + OpenIDConnectPublicConfig, OpenIDConnectDigiDMachtigenConfig, ) from openforms.contrib.digid_eherkenning.utils import ( get_digid_logo, @@ -144,15 +144,21 @@ class DigiDMachtigenOIDCAuthentication(OIDCAuthentication): provides_auth = AuthAttribute.bsn init_url = "digid_machtigen_oidc:init" session_key = DIGID_MACHTIGEN_OIDC_AUTH_SESSION_KEY - config_class = OpenIDConnectPublicConfig + config_class = OpenIDConnectDigiDMachtigenConfig def add_claims_to_sessions_if_not_cosigning(self, claim, request): # set the session auth key only if we're not co-signing if claim and CO_SIGN_PARAMETER not in request.GET: - config = OpenIDConnectPublicConfig.get_solo() + config = OpenIDConnectDigiDMachtigenConfig.get_solo() request.session[FORM_AUTH_SESSION_KEY] = { "plugin": self.identifier, "attribute": self.provides_auth, "value": claim[config.vertegenwoordigde_claim_name], "machtigen": request.session[DIGID_MACHTIGEN_OIDC_AUTH_SESSION_KEY], } + + def get_label(self) -> str: + return "DigiD Machtigen" + + def get_logo(self, request) -> Optional[LoginLogo]: + return LoginLogo(title=self.get_label(), **get_digid_logo(request)) diff --git a/src/openforms/authentication/contrib/digid_eherkenning_oidc/views.py b/src/openforms/authentication/contrib/digid_eherkenning_oidc/views.py index 39786926a2..f4068f25a5 100644 --- a/src/openforms/authentication/contrib/digid_eherkenning_oidc/views.py +++ b/src/openforms/authentication/contrib/digid_eherkenning_oidc/views.py @@ -8,6 +8,7 @@ from mozilla_django_oidc.views import get_next_url from digid_eherkenning_oidc_generics.mixins import ( + SoloConfigDigiDMachtigenMixin, SoloConfigDigiDMixin, SoloConfigEHerkenningMixin, ) @@ -96,13 +97,13 @@ class eHerkenningOIDCAuthenticationCallbackView( class DigiDMachtigenOIDCAuthenticationRequestView( - SoloConfigDigiDMixin, OIDCAuthenticationRequestView + SoloConfigDigiDMachtigenMixin, OIDCAuthenticationRequestView ): plugin_identifier = "digid_machtigen_oidc" class DigiDMachtigenOIDCAuthenticationCallbackView( - SoloConfigDigiDMixin, OIDCAuthenticationCallbackView + SoloConfigDigiDMachtigenMixin, OIDCAuthenticationCallbackView ): plugin_identifier = "digid_machtigen_oidc" auth_backend_class = OIDCAuthenticationDigiDMachtigenBackend From cb2340cf27f143857aebb53a8ffa66333f38fb4f Mon Sep 17 00:00:00 2001 From: SilviaAmAm Date: Thu, 31 Mar 2022 15:47:36 +0200 Subject: [PATCH 4/9] :white_check_mark: [#1471] DigiD machtigen tests --- .../tests/digid/test_admin.py | 9 - .../tests/digid_machtigen/__init__.py | 0 .../tests/digid_machtigen/test_auth_plugin.py | 58 ++++ .../digid_machtigen/test_auth_procedure.py | 320 ++++++++++++++++++ 4 files changed, 378 insertions(+), 9 deletions(-) create mode 100644 src/openforms/authentication/contrib/digid_eherkenning_oidc/tests/digid_machtigen/__init__.py create mode 100644 src/openforms/authentication/contrib/digid_eherkenning_oidc/tests/digid_machtigen/test_auth_plugin.py create mode 100644 src/openforms/authentication/contrib/digid_eherkenning_oidc/tests/digid_machtigen/test_auth_procedure.py diff --git a/src/openforms/authentication/contrib/digid_eherkenning_oidc/tests/digid/test_admin.py b/src/openforms/authentication/contrib/digid_eherkenning_oidc/tests/digid/test_admin.py index dd96bd8ded..aa69b5c9b0 100644 --- a/src/openforms/authentication/contrib/digid_eherkenning_oidc/tests/digid/test_admin.py +++ b/src/openforms/authentication/contrib/digid_eherkenning_oidc/tests/digid/test_admin.py @@ -30,15 +30,6 @@ def setUp(self): self.user = SuperUserFactory.create(app=self.app) self.app.set_user(self.user) - global_config = GlobalConfiguration.get_solo() - global_config.enable_react_form = False - global_config.save() - - def _cleanup(): - GlobalConfiguration.get_solo().delete() - - self.addCleanup(_cleanup) - def test_digid_oidc_disable_allowed(self): # Patching `get_solo()` doesn't seem to work when retrieving the change_form config = OpenIDConnectPublicConfig(**default_config) diff --git a/src/openforms/authentication/contrib/digid_eherkenning_oidc/tests/digid_machtigen/__init__.py b/src/openforms/authentication/contrib/digid_eherkenning_oidc/tests/digid_machtigen/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/openforms/authentication/contrib/digid_eherkenning_oidc/tests/digid_machtigen/test_auth_plugin.py b/src/openforms/authentication/contrib/digid_eherkenning_oidc/tests/digid_machtigen/test_auth_plugin.py new file mode 100644 index 0000000000..029201180b --- /dev/null +++ b/src/openforms/authentication/contrib/digid_eherkenning_oidc/tests/digid_machtigen/test_auth_plugin.py @@ -0,0 +1,58 @@ +from unittest.mock import patch + +from django.urls import reverse + +from rest_framework import status +from rest_framework.test import APITestCase + +from openforms.accounts.tests.factories import UserFactory +from openforms.config.models import GlobalConfiguration + + +class DigiDMachtigenOIDCAuthPluginEndpointTests(APITestCase): + @classmethod + def setUpTestData(cls): + super().setUpTestData() + + cls.user = UserFactory.create(is_staff=True) + + def setUp(self): + super().setUp() + + self.client.force_authenticate(user=self.user) + + @patch("openforms.plugins.registry.GlobalConfiguration.get_solo") + def test_plugin_list_digid_machtigen_oidc_enabled(self, mock_get_solo): + mock_get_solo.return_value = GlobalConfiguration( + plugin_configuration={ + "authentication": { + "digid_machtigen_oidc": {"enabled": True}, + }, + } + ) + + endpoint = reverse("api:authentication-plugin-list") + + response = self.client.get(endpoint) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + plugin_names = [p["id"] for p in response.data] + self.assertIn("digid_machtigen_oidc", plugin_names) + + @patch("openforms.plugins.registry.GlobalConfiguration.get_solo") + def test_plugin_list_digid_machtigen_oidc_not_enabled(self, mock_get_solo): + mock_get_solo.return_value = GlobalConfiguration( + plugin_configuration={ + "authentication": { + "digid_machtigen_oidc": {"enabled": False}, + }, + } + ) + + endpoint = reverse("api:authentication-plugin-list") + + response = self.client.get(endpoint) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + plugin_names = [p["id"] for p in response.data] + self.assertNotIn("digid_machtigen_oidc", plugin_names) diff --git a/src/openforms/authentication/contrib/digid_eherkenning_oidc/tests/digid_machtigen/test_auth_procedure.py b/src/openforms/authentication/contrib/digid_eherkenning_oidc/tests/digid_machtigen/test_auth_procedure.py new file mode 100644 index 0000000000..03bb6e1624 --- /dev/null +++ b/src/openforms/authentication/contrib/digid_eherkenning_oidc/tests/digid_machtigen/test_auth_procedure.py @@ -0,0 +1,320 @@ +from unittest.mock import patch + +from django.test import TestCase, override_settings +from django.urls import reverse + +import requests_mock +from furl import furl +from rest_framework import status + +from digid_eherkenning_oidc_generics.models import ( + OpenIDConnectDigiDMachtigenConfig, + OpenIDConnectPublicConfig, +) +from openforms.authentication.views import BACKEND_OUTAGE_RESPONSE_PARAMETER +from openforms.forms.tests.factories import FormFactory + +default_config = dict( + enabled=True, + oidc_rp_client_id="testclient", + oidc_rp_client_secret="secret", + oidc_rp_sign_algo="RS256", + oidc_rp_scopes_list=["openid", "bsn"], + oidc_op_jwks_endpoint="http://provider.com/auth/realms/master/protocol/openid-connect/certs", + oidc_op_authorization_endpoint="http://provider.com/auth/realms/master/protocol/openid-connect/auth", + oidc_op_token_endpoint="http://provider.com/auth/realms/master/protocol/openid-connect/token", + oidc_op_user_endpoint="http://provider.com/auth/realms/master/protocol/openid-connect/userinfo", +) + + +@override_settings(CORS_ALLOW_ALL_ORIGINS=True, IS_HTTPS=True) +@patch( + "digid_eherkenning_oidc_generics.models.OpenIDConnectDigiDMachtigenConfig.get_solo", + return_value=OpenIDConnectDigiDMachtigenConfig(**default_config), +) +class DigiDMachtigenOIDCTests(TestCase): + @classmethod + def setUpTestData(cls): + cls.form = FormFactory.create( + generate_minimal_setup=True, + authentication_backends=["digid_machtigen_oidc"], + ) + + def test_redirect_to_digid_machtigen_oidc(self, m_get_solo): + login_url = reverse( + "authentication:start", + kwargs={"slug": self.form.slug, "plugin_id": "digid_machtigen_oidc"}, + ) + + form_path = reverse("core:form-detail", kwargs={"slug": self.form.slug}) + form_url = furl(f"http://testserver{form_path}").set({"_start": "1"}).url + start_url = furl(login_url).set({"next": form_url}) + response = self.client.get(start_url) + + self.assertEqual(status.HTTP_302_FOUND, response.status_code) + + parsed = furl(response.url) + query_params = parsed.query.params + + self.assertEqual(parsed.host, "testserver") + self.assertEqual( + parsed.path, reverse("digid_machtigen_oidc:oidc_authentication_init") + ) + + parsed = furl(query_params["next"]) + query_params = parsed.query.params + + self.assertEqual( + parsed.path, + reverse( + "authentication:return", + kwargs={"slug": self.form.slug, "plugin_id": "digid_machtigen_oidc"}, + ), + ) + self.assertEqual(query_params["next"], form_url) + + with requests_mock.Mocker() as m: + m.head( + "http://provider.com/auth/realms/master/protocol/openid-connect/auth", + status_code=200, + ) + response = self.client.get(response.url) + + self.assertEqual(status.HTTP_302_FOUND, response.status_code) + + parsed = furl(response.url) + query_params = parsed.query.params + + self.assertEqual(parsed.host, "provider.com") + self.assertEqual( + parsed.path, "/auth/realms/master/protocol/openid-connect/auth" + ) + self.assertEqual(query_params["scope"], "openid bsn") + self.assertEqual(query_params["client_id"], "testclient") + self.assertEqual( + query_params["redirect_uri"], + f"http://testserver{reverse('digid_machtigen_oidc:oidc_authentication_callback')}", + ) + + parsed = furl(self.client.session["oidc_login_next"]) + query_params = parsed.query.params + + self.assertEqual( + parsed.path, + reverse( + "authentication:return", + kwargs={"slug": self.form.slug, "plugin_id": "digid_machtigen_oidc"}, + ), + ) + self.assertEqual(query_params["next"], form_url) + + def test_redirect_to_digid_machtigen_oidc_internal_server_error(self, m_get_solo): + login_url = reverse( + "authentication:start", + kwargs={"slug": self.form.slug, "plugin_id": "digid_machtigen_oidc"}, + ) + + form_path = reverse("core:form-detail", kwargs={"slug": self.form.slug}) + form_url = str(furl(f"http://testserver{form_path}").set({"_start": "1"})) + start_url = furl(login_url).set({"next": form_url}) + response = self.client.get(start_url) + + self.assertEqual(status.HTTP_302_FOUND, response.status_code) + + with requests_mock.Mocker() as m: + m.head( + "http://provider.com/auth/realms/master/protocol/openid-connect/auth", + status_code=500, + ) + response = self.client.get(response.url) + + parsed = furl(response.url) + query_params = parsed.query.params + + self.assertEqual(parsed.host, "testserver") + self.assertEqual(parsed.path, form_path) + self.assertEqual(query_params["of-auth-problem"], "digid_machtigen_oidc") + + def test_redirect_to_digid_machtigen_oidc_callback_error(self, m_get_solo): + form_path = reverse("core:form-detail", kwargs={"slug": self.form.slug}) + form_url = f"http://testserver{form_path}" + redirect_form_url = furl(form_url).set({"_start": "1"}) + redirect_url = furl( + reverse( + "authentication:return", + kwargs={"slug": self.form.slug, "plugin_id": "digid_machtigen_oidc"}, + ) + ).set({"next": redirect_form_url}) + + session = self.client.session + session["oidc_login_next"] = redirect_url.url + session.save() + + with patch( + "openforms.authentication.contrib.digid_eherkenning_oidc.backends.OIDCAuthenticationDigiDMachtigenBackend.verify_claims", + return_value=False, + ): + response = self.client.get(reverse("digid_machtigen_oidc:callback")) + + self.assertEqual(response.status_code, status.HTTP_302_FOUND) + + parsed = furl(response.url) + query_params = parsed.query.params + + self.assertEqual(parsed.path, form_path) + self.assertEqual(query_params["_start"], "1") + self.assertEqual( + query_params[BACKEND_OUTAGE_RESPONSE_PARAMETER], "digid_machtigen_oidc" + ) + + @override_settings(CORS_ALLOW_ALL_ORIGINS=False, CORS_ALLOWED_ORIGINS=[]) + def test_redirect_to_disallowed_domain(self, m_get_solo): + login_url = reverse( + "digid_machtigen_oidc:oidc_authentication_init", + ) + + form_url = "http://example.com" + start_url = furl(login_url).set({"next": form_url}) + response = self.client.get(start_url) + + self.assertEqual(status.HTTP_400_BAD_REQUEST, response.status_code) + + @override_settings( + CORS_ALLOW_ALL_ORIGINS=False, CORS_ALLOWED_ORIGINS=["http://example.com"] + ) + def test_redirect_to_allowed_domain(self, m_get_solo): + m_get_solo.return_value = OpenIDConnectDigiDMachtigenConfig( + enabled=True, + oidc_rp_client_id="testclient", + oidc_rp_client_secret="secret", + oidc_rp_sign_algo="RS256", + oidc_rp_scopes_list=["openid", "bsn"], + oidc_op_jwks_endpoint="http://provider.com/auth/realms/master/protocol/openid-connect/certs", + oidc_op_authorization_endpoint="http://provider.com/auth/realms/master/protocol/openid-connect/auth", + oidc_op_token_endpoint="http://provider.com/auth/realms/master/protocol/openid-connect/token", + oidc_op_user_endpoint="http://provider.com/auth/realms/master/protocol/openid-connect/userinfo", + ) + + login_url = reverse( + "authentication:start", + kwargs={"slug": self.form.slug, "plugin_id": "digid_machtigen_oidc"}, + ) + + form_url = "http://example.com" + start_url = furl(login_url).set({"next": form_url}) + response = self.client.get(start_url) + + self.assertEqual(status.HTTP_302_FOUND, response.status_code) + + parsed = furl(response.url) + query_params = parsed.query.params + + self.assertEqual(parsed.host, "testserver") + self.assertEqual( + parsed.path, reverse("digid_machtigen_oidc:oidc_authentication_init") + ) + + parsed = furl(query_params["next"]) + query_params = parsed.query.params + + self.assertEqual( + parsed.path, + reverse( + "authentication:return", + kwargs={"slug": self.form.slug, "plugin_id": "digid_machtigen_oidc"}, + ), + ) + self.assertEqual(query_params["next"], form_url) + + with requests_mock.Mocker() as m: + m.head( + "http://provider.com/auth/realms/master/protocol/openid-connect/auth", + status_code=200, + ) + response = self.client.get(response.url) + + self.assertEqual(status.HTTP_302_FOUND, response.status_code) + + parsed = furl(response.url) + query_params = parsed.query.params + + self.assertEqual(parsed.host, "provider.com") + self.assertEqual( + parsed.path, "/auth/realms/master/protocol/openid-connect/auth" + ) + self.assertEqual(query_params["scope"], "openid bsn") + self.assertEqual(query_params["client_id"], "testclient") + self.assertEqual( + query_params["redirect_uri"], + f"http://testserver{reverse('digid_machtigen_oidc:oidc_authentication_callback')}", + ) + + def test_redirect_with_keycloak_identity_provider_hint(self, m_get_solo): + m_get_solo.return_value = OpenIDConnectPublicConfig( + enabled=True, + oidc_rp_client_id="testclient", + oidc_rp_client_secret="secret", + oidc_rp_sign_algo="RS256", + oidc_rp_scopes_list=["openid", "bsn"], + oidc_op_jwks_endpoint="http://provider.com/auth/realms/master/protocol/openid-connect/certs", + oidc_op_authorization_endpoint="http://provider.com/auth/realms/master/protocol/openid-connect/auth", + oidc_op_token_endpoint="http://provider.com/auth/realms/master/protocol/openid-connect/token", + oidc_op_user_endpoint="http://provider.com/auth/realms/master/protocol/openid-connect/userinfo", + oidc_keycloak_idp_hint="oidc-digid-machtigen", + ) + + login_url = reverse( + "authentication:start", + kwargs={"slug": self.form.slug, "plugin_id": "digid_machtigen_oidc"}, + ) + + form_path = reverse("core:form-detail", kwargs={"slug": self.form.slug}) + form_url = str(furl(f"http://testserver{form_path}").set({"_start": "1"})) + start_url = furl(login_url).set({"next": form_url}) + response = self.client.get(start_url) + + self.assertEqual(status.HTTP_302_FOUND, response.status_code) + + parsed = furl(response.url) + query_params = parsed.query.params + + self.assertEqual(parsed.host, "testserver") + self.assertEqual( + parsed.path, reverse("digid_machtigen_oidc:oidc_authentication_init") + ) + + parsed = furl(query_params["next"]) + query_params = parsed.query.params + + self.assertEqual( + parsed.path, + reverse( + "authentication:return", + kwargs={"slug": self.form.slug, "plugin_id": "digid_machtigen_oidc"}, + ), + ) + self.assertEqual(query_params["next"], form_url) + + with requests_mock.Mocker() as m: + m.head( + "http://provider.com/auth/realms/master/protocol/openid-connect/auth", + status_code=200, + ) + response = self.client.get(response.url) + + self.assertEqual(status.HTTP_302_FOUND, response.status_code) + + parsed = furl(response.url) + query_params = parsed.query.params + + self.assertEqual(parsed.host, "provider.com") + self.assertEqual( + parsed.path, "/auth/realms/master/protocol/openid-connect/auth" + ) + self.assertEqual(query_params["scope"], "openid bsn") + self.assertEqual(query_params["client_id"], "testclient") + self.assertEqual( + query_params["redirect_uri"], + f"http://testserver{reverse('digid_machtigen_oidc:oidc_authentication_callback')}", + ) + self.assertEqual(query_params["kc_idp_hint"], "oidc-digid-machtigen") From 63650f26cfc469531dfc5f5de14589aca45894de Mon Sep 17 00:00:00 2001 From: SilviaAmAm Date: Thu, 31 Mar 2022 15:58:42 +0200 Subject: [PATCH 5/9] :lipstick: [#1471] Update admin index --- src/openforms/fixtures/default_admin_index.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/openforms/fixtures/default_admin_index.json b/src/openforms/fixtures/default_admin_index.json index 786ece93e5..e4b4ae6ec7 100644 --- a/src/openforms/fixtures/default_admin_index.json +++ b/src/openforms/fixtures/default_admin_index.json @@ -158,6 +158,10 @@ "digid_eherkenning_oidc_generics", "openidconnecteherkenningconfig" ], + [ + "digid_eherkenning_oidc_generics", + "openidconnectdigidmachtigenconfig" + ], [ "multidomain", "domain" From 7bf75855aa1190a87d6642055dc138eb986fb0ae Mon Sep 17 00:00:00 2001 From: SilviaAmAm Date: Thu, 31 Mar 2022 16:01:01 +0200 Subject: [PATCH 6/9] :sparkles: [#1471] Updated OAS --- src/openapi.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/openapi.yaml b/src/openapi.yaml index 8ff725c55a..81684b8fa7 100644 --- a/src/openapi.yaml +++ b/src/openapi.yaml @@ -3203,6 +3203,7 @@ components: - eidas - digid_oidc - eherkenning_oidc + - digid_machtigen_oidc type: string BlankEnum: enum: From b125b50a440116de90bb9fb5f89a1f9fc8f1f907 Mon Sep 17 00:00:00 2001 From: SilviaAmAm Date: Thu, 7 Apr 2022 10:49:30 +0200 Subject: [PATCH 7/9] :memo: [#1471] Document config Digid machtigen --- docs/configuration/authentication/index.rst | 1 + .../authentication/oidc_digid_machtigen.rst | 60 +++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 docs/configuration/authentication/oidc_digid_machtigen.rst diff --git a/docs/configuration/authentication/index.rst b/docs/configuration/authentication/index.rst index 545e9286b1..f11ba1502b 100644 --- a/docs/configuration/authentication/index.rst +++ b/docs/configuration/authentication/index.rst @@ -10,5 +10,6 @@ Authentication plugins digid eherkenning_eidas oidc_digid + oidc_digid_machtigen oidc_eherkenning other diff --git a/docs/configuration/authentication/oidc_digid_machtigen.rst b/docs/configuration/authentication/oidc_digid_machtigen.rst new file mode 100644 index 0000000000..c5990602c7 --- /dev/null +++ b/docs/configuration/authentication/oidc_digid_machtigen.rst @@ -0,0 +1,60 @@ +.. _configuration_authentication_oidc_digid_machtigen: + +================================================ +OpenID Connect voor inloggen met DigiD Machtigen +================================================ + +Open Formulieren ondersteunt `DigiD Machtigen`_ login voor burgers via het OpenID Connect protocol (OIDC). +Burgers kunnen inloggen op Open Formulieren met hun DigiD account en een formulier invullen namens iemand +anders. In deze flow: + +* Een gebruiker klikt op de knop *Inloggen met DigiD Machtigen* die op de start pagina van een formulier staat. +* De gebruiker wordt via de omgeving van de OpenID Connect provider (bijv. `Keycloak`_) naar DigiD geleid, waar de gebruiker kan inloggen met *zijn/haar eigen* DigiD inlog gegevens. +* De gebruiker kan dan kiezen namens wie hij/zij het formulier wilt invullen. +* DigiD stuurt de gebruiker terug naar de OIDC omgeving, die op zijn beurt de gebruiker weer terugstuurt naar Open Formulieren +* De gebruiker kan verder met het invullen van het formulier + +.. _DigiD Machtigen: https://machtigen.digid.nl/ +.. _Keycloak: https://www.keycloak.org/ + +.. _configuration_oidc_digid_machtigen_appgroup: + +Configureren van OIDC voor DigiD Machtigen +========================================== + +De stappen hier zijn dezelfde als voor :ref:`configuration_oidc_digid_appgroup`, maar de **Redirect URI** +is ``https://open-formulieren.gemeente.nl/digid-oidc-machtigen/callback/`` (met de juiste domein in plaats van +``open-formulieren.gemeente.nl``). + +Aan het eind van dit proces moet u de volgende gegevens hebben: + +* Server adres, bijvoorbeeld ``login.gemeente.nl`` +* Client ID, bijvoorbeeld ``a7d14516-8b20-418f-b34e-25f53c930948`` +* Client secret, bijvoorbeeld ``97d663a9-3624-4930-90c7-2b90635bd990`` + +Configureren van OIDC in Open Formulieren +========================================= + +Om OIDC in Open-Formulieren te kunnen configureren, de volgende :ref:`gegevens ` zijn nodig: + +* Server adres +* Client ID +* Client secret + +Navigeer vervolgens in de admin naar **Configuratie** > **OpenID Connect configuration for DigiD Machtigen**. + +#. Vink *Enable* aan om OIDC in te schakelen. +#. Vul bij **OpenID Connect client ID** het Client ID in, bijvoorbeeld ``a7d14516-8b20-418f-b34e-25f53c930948``. +#. Vul bij **OpenID Connect secret** het Client secret in, bijvoobeeld ``97d663a9-3624-4930-90c7-2b90635bd990``. +#. Vul bij **OpenID Connect scopes** ``openid``. +#. Vul bij **OpenID sign algorithm** ``RS256`` in. +#. Laat **Sign key** leeg. +#. Laat bij **Vertegenwoordigde claim name** de standaardwaarde staan, tenzij de naam van het BSN veld van de vertegenwoordigde +#. in de OIDC claims anders is dan ``aanvrager.bsn``. +#. Laat bij **Gemachtigde claim name** de standaardwaarde staan, tenzij de naam van het BSN veld van de gemachtigde +#. in de OIDC claims anders is dan ``gemachtigde.bsn``. + +De endpoints die ingesteld moeten worden zijn dezelfde als voor DigiD. U kunt de stappen in :ref:`configuration_oidc_digid_appgroup` +volgen om die te configureren. + +Nu kan er een formulier aangemaakt worden met het authenticatie backend ``DigiD Machtigen via OpenID Connect`` (zie :ref:`manual_forms_basics`). From 835676eb6b056ca5ddf2c6951c1cc5ab87a4ea9d Mon Sep 17 00:00:00 2001 From: SilviaAmAm Date: Thu, 7 Apr 2022 10:59:25 +0200 Subject: [PATCH 8/9] :rotating_light: Formatting --- .../authentication/contrib/digid_eherkenning_oidc/backends.py | 3 ++- .../authentication/contrib/digid_eherkenning_oidc/plugin.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/openforms/authentication/contrib/digid_eherkenning_oidc/backends.py b/src/openforms/authentication/contrib/digid_eherkenning_oidc/backends.py index e49d89d5c3..b515b6880c 100644 --- a/src/openforms/authentication/contrib/digid_eherkenning_oidc/backends.py +++ b/src/openforms/authentication/contrib/digid_eherkenning_oidc/backends.py @@ -1,10 +1,11 @@ import logging from copy import deepcopy -from glom import PathAccessError, glom from django.contrib.auth.models import AnonymousUser from django.core.exceptions import SuspiciousOperation +from glom import PathAccessError, glom + from digid_eherkenning_oidc_generics.backends import OIDCAuthenticationBackend from digid_eherkenning_oidc_generics.mixins import ( SoloConfigDigiDMachtigenMixin, diff --git a/src/openforms/authentication/contrib/digid_eherkenning_oidc/plugin.py b/src/openforms/authentication/contrib/digid_eherkenning_oidc/plugin.py index 1f45fc5c50..35b6fbab9f 100644 --- a/src/openforms/authentication/contrib/digid_eherkenning_oidc/plugin.py +++ b/src/openforms/authentication/contrib/digid_eherkenning_oidc/plugin.py @@ -8,8 +8,9 @@ from rest_framework.reverse import reverse from digid_eherkenning_oidc_generics.models import ( + OpenIDConnectDigiDMachtigenConfig, OpenIDConnectEHerkenningConfig, - OpenIDConnectPublicConfig, OpenIDConnectDigiDMachtigenConfig, + OpenIDConnectPublicConfig, ) from openforms.contrib.digid_eherkenning.utils import ( get_digid_logo, From 718ef9aa1a3f54c4ec80b39ad15a69a0344ed2b4 Mon Sep 17 00:00:00 2001 From: SilviaAmAm Date: Tue, 19 Apr 2022 16:30:52 +0200 Subject: [PATCH 9/9] :ok_hand: [#1471] PR feedback --- .../authentication/oidc_digid_machtigen.rst | 12 +++++------- src/digid_eherkenning_oidc_generics/mixins.py | 5 +---- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/docs/configuration/authentication/oidc_digid_machtigen.rst b/docs/configuration/authentication/oidc_digid_machtigen.rst index c5990602c7..6b31f33356 100644 --- a/docs/configuration/authentication/oidc_digid_machtigen.rst +++ b/docs/configuration/authentication/oidc_digid_machtigen.rst @@ -8,7 +8,7 @@ Open Formulieren ondersteunt `DigiD Machtigen`_ login voor burgers via het OpenI Burgers kunnen inloggen op Open Formulieren met hun DigiD account en een formulier invullen namens iemand anders. In deze flow: -* Een gebruiker klikt op de knop *Inloggen met DigiD Machtigen* die op de start pagina van een formulier staat. +* Een gebruiker klikt op de knop *Inloggen met DigiD Machtigen* die op de startpagina van een formulier staat. * De gebruiker wordt via de omgeving van de OpenID Connect provider (bijv. `Keycloak`_) naar DigiD geleid, waar de gebruiker kan inloggen met *zijn/haar eigen* DigiD inlog gegevens. * De gebruiker kan dan kiezen namens wie hij/zij het formulier wilt invullen. * DigiD stuurt de gebruiker terug naar de OIDC omgeving, die op zijn beurt de gebruiker weer terugstuurt naar Open Formulieren @@ -23,7 +23,7 @@ Configureren van OIDC voor DigiD Machtigen ========================================== De stappen hier zijn dezelfde als voor :ref:`configuration_oidc_digid_appgroup`, maar de **Redirect URI** -is ``https://open-formulieren.gemeente.nl/digid-oidc-machtigen/callback/`` (met de juiste domein in plaats van +is ``https://open-formulieren.gemeente.nl/digid-oidc-machtigen/callback/`` (met het juiste domein in plaats van ``open-formulieren.gemeente.nl``). Aan het eind van dit proces moet u de volgende gegevens hebben: @@ -35,7 +35,7 @@ Aan het eind van dit proces moet u de volgende gegevens hebben: Configureren van OIDC in Open Formulieren ========================================= -Om OIDC in Open-Formulieren te kunnen configureren, de volgende :ref:`gegevens ` zijn nodig: +Om OIDC in Open-Formulieren te kunnen configureren zijn de volgende :ref:`gegevens ` nodig: * Server adres * Client ID @@ -49,10 +49,8 @@ Navigeer vervolgens in de admin naar **Configuratie** > **OpenID Connect configu #. Vul bij **OpenID Connect scopes** ``openid``. #. Vul bij **OpenID sign algorithm** ``RS256`` in. #. Laat **Sign key** leeg. -#. Laat bij **Vertegenwoordigde claim name** de standaardwaarde staan, tenzij de naam van het BSN veld van de vertegenwoordigde -#. in de OIDC claims anders is dan ``aanvrager.bsn``. -#. Laat bij **Gemachtigde claim name** de standaardwaarde staan, tenzij de naam van het BSN veld van de gemachtigde -#. in de OIDC claims anders is dan ``gemachtigde.bsn``. +#. Laat bij **Vertegenwoordigde claim name** de standaardwaarde staan, tenzij de naam van het BSN veld van de vertegenwoordigde in de OIDC claims anders is dan ``aanvrager.bsn``. +#. Laat bij **Gemachtigde claim name** de standaardwaarde staan, tenzij de naam van het BSN veld van de gemachtigde in de OIDC claims anders is dan ``gemachtigde.bsn``. De endpoints die ingesteld moeten worden zijn dezelfde als voor DigiD. U kunt de stappen in :ref:`configuration_oidc_digid_appgroup` volgen om die te configureren. diff --git a/src/digid_eherkenning_oidc_generics/mixins.py b/src/digid_eherkenning_oidc_generics/mixins.py index 9cb0de09af..378f85dd2b 100644 --- a/src/digid_eherkenning_oidc_generics/mixins.py +++ b/src/digid_eherkenning_oidc_generics/mixins.py @@ -1,9 +1,6 @@ from mozilla_django_oidc_db.mixins import SoloConfigMixin as _SoloConfigMixin -import digid_eherkenning_oidc_generics.digid_machtigen_settings as digid_machtigen_settings -import digid_eherkenning_oidc_generics.digid_settings as digid_settings -import digid_eherkenning_oidc_generics.eherkenning_settings as eherkenning_settings - +from . import digid_machtigen_settings, digid_settings, eherkenning_settings from .models import ( OpenIDConnectDigiDMachtigenConfig, OpenIDConnectEHerkenningConfig,