diff --git a/docs/configuration/authentication/index.rst b/docs/configuration/authentication/index.rst index f11ba1502b..f0ebaeba23 100644 --- a/docs/configuration/authentication/index.rst +++ b/docs/configuration/authentication/index.rst @@ -10,6 +10,6 @@ Authentication plugins digid eherkenning_eidas oidc_digid - oidc_digid_machtigen + oidc_machtigen oidc_eherkenning other diff --git a/docs/configuration/authentication/oidc_digid_machtigen.rst b/docs/configuration/authentication/oidc_digid_machtigen.rst deleted file mode 100644 index 6b31f33356..0000000000 --- a/docs/configuration/authentication/oidc_digid_machtigen.rst +++ /dev/null @@ -1,58 +0,0 @@ -.. _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 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 -* 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 het 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 zijn de volgende :ref:`gegevens ` 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`). diff --git a/docs/configuration/authentication/oidc_machtigen.rst b/docs/configuration/authentication/oidc_machtigen.rst new file mode 100644 index 0000000000..d9a3dcc2d3 --- /dev/null +++ b/docs/configuration/authentication/oidc_machtigen.rst @@ -0,0 +1,100 @@ +.. _configuration_authentication_oidc_machtigen: + +============================================================================= +OpenID Connect voor inloggen met DigiD Machtigen en eHerkenning bewindvoering +============================================================================= + +Open Formulieren ondersteunt `DigiD Machtigen`_ en eHerkenning bewindvoering login voor burgers via het OpenID Connect +protocol (OIDC). +Burgers kunnen inloggen op Open Formulieren met hun DigiD/eHerkenning account en een formulier invullen namens iemand +anders. In deze flow: + +* Een gebruiker klikt op de knop *Inloggen met DigiD Machtigen* of *Inloggen met eHerkenning bewindvoering* die op de startpagina van een formulier staat. +* De gebruiker wordt via de omgeving van de OpenID Connect provider (bijv. `Keycloak`_) naar DigiD/eHerkenning geleid, waar de gebruiker kan inloggen met *hun eigen* DigiD/eHerkenning inloggegevens. +* De gebruiker kan dan kiezen namens wie ze het formulier willen invullen. +* De gebruiker wordt daarna terug naar de OIDC omgeving gestuurd, 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 het 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 zijn de volgende :ref:`gegevens ` 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`). + +.. _configuration_oidc_eh_bewindvoering_appgroup: + +Configureren van OIDC voor eHerkenning bewindvoering +==================================================== + +De stappen hier zijn dezelfde als voor :ref:`configuration_oidc_digid_machtigen_appgroup`, maar de **Redirect URI** +is ``https://open-formulieren.gemeente.nl/eherkenning-bewindvoering-oidc/callback/`` (met het juiste domein in plaats van +``open-formulieren.gemeente.nl``). + +Aan het eind van dit proces moet u de volgende gegevens hebben: + +* OpenID connect client discovery endpoint, bijvoorbeeld ``https://keycloak-test.nl/auth/realms/zgw-publiek/`` +* Client ID, bijvoorbeeld ``a7d14516-8b20-418f-b34e-25f53c930948`` +* Client secret, bijvoorbeeld ``97d663a9-3624-4930-90c7-2b90635bd990`` +* Identity provider hint (optioneel) + +Configureren van OIDC in Open Formulieren +========================================= + +Om OIDC in Open-Formulieren te kunnen configureren zijn de volgende :ref:`gegevens ` nodig: + +* OpenID connect client discovery endpoint +* Client ID +* Client secret +* Identity provider hint (optioneel) + +Navigeer vervolgens in de admin naar **Configuratie** > **OpenID Connect configuration for eHerkenning bewindvoering**. + +#. 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 **Vertegenwoordigd bedrijf claim name** de standaardwaarde staan, tenzij de naam van het KvK veld van de vertegenwoordigde in de OIDC claims anders is dan ``aanvrager.kvk``. +#. Laat bij **Gemachtigde persoon claim name** de standaardwaarde staan, tenzij de naam van het ID 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. +#. Als u een Identity Provider hint heeft, dan vul het in. Voor Keycloak is dit nodig. + +Nu kan er een formulier aangemaakt worden met het authenticatie backend ``eHerkenning bewindvoering via OpenID Connect`` (zie :ref:`manual_forms_basics`). diff --git a/src/digid_eherkenning_oidc_generics/admin.py b/src/digid_eherkenning_oidc_generics/admin.py index 85e5bcdb64..c44c462604 100644 --- a/src/digid_eherkenning_oidc_generics/admin.py +++ b/src/digid_eherkenning_oidc_generics/admin.py @@ -6,11 +6,13 @@ from .forms import ( OpenIDConnectDigiDMachtigenConfigForm, + OpenIDConnectEHerkenningBewindvoeringConfigForm, OpenIDConnectEHerkenningConfigForm, OpenIDConnectPublicConfigForm, ) from .models import ( OpenIDConnectDigiDMachtigenConfig, + OpenIDConnectEHerkenningBewindvoeringConfig, OpenIDConnectEHerkenningConfig, OpenIDConnectPublicConfig, ) @@ -107,3 +109,52 @@ class OpenIDConnectConfigDigiDMachtigenAdmin(DynamicArrayMixin, SingletonModelAd ), (_("Keycloak specific settings"), {"fields": ("oidc_keycloak_idp_hint",)}), ) + + +@admin.register(OpenIDConnectEHerkenningBewindvoeringConfig) +class OpenIDConnectConfigEHerkenningBewindvoeringAdmin( + DynamicArrayMixin, SingletonModelAdmin +): + form = OpenIDConnectEHerkenningBewindvoeringConfigForm + + 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_company_claim_name", + "gemachtigde_person_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/eherkenning_bewindvoering_settings.py b/src/digid_eherkenning_oidc_generics/eherkenning_bewindvoering_settings.py new file mode 100644 index 0000000000..485a887e37 --- /dev/null +++ b/src/digid_eherkenning_oidc_generics/eherkenning_bewindvoering_settings.py @@ -0,0 +1,2 @@ +EHERKENNING_BEWINDVOERING_CUSTOM_OIDC_DB_PREFIX = "eherkenning_bewindvoering_oidc" +OIDC_AUTHENTICATION_CALLBACK_URL = "eherkenning_bewindvoering_oidc:callback" diff --git a/src/digid_eherkenning_oidc_generics/forms.py b/src/digid_eherkenning_oidc_generics/forms.py index d9b67ac521..29c2ed0fc0 100644 --- a/src/digid_eherkenning_oidc_generics/forms.py +++ b/src/digid_eherkenning_oidc_generics/forms.py @@ -10,6 +10,7 @@ from .models import ( OpenIDConnectDigiDMachtigenConfig, + OpenIDConnectEHerkenningBewindvoeringConfig, OpenIDConnectEHerkenningConfig, OpenIDConnectPublicConfig, ) @@ -70,3 +71,11 @@ class OpenIDConnectDigiDMachtigenConfigForm(OpenIDConnectBaseConfigForm): class Meta: model = OpenIDConnectDigiDMachtigenConfig fields = "__all__" + + +class OpenIDConnectEHerkenningBewindvoeringConfigForm(OpenIDConnectBaseConfigForm): + plugin_identifier = "eherkenning_bewindvoering_oidc" + + class Meta: + model = OpenIDConnectEHerkenningBewindvoeringConfig + fields = "__all__" diff --git a/src/digid_eherkenning_oidc_generics/migrations/0005_auto_20220426_1552.py b/src/digid_eherkenning_oidc_generics/migrations/0005_auto_20220426_1552.py new file mode 100644 index 0000000000..17bc9aa531 --- /dev/null +++ b/src/digid_eherkenning_oidc_generics/migrations/0005_auto_20220426_1552.py @@ -0,0 +1,204 @@ +# Generated by Django 3.2.13 on 2022-04-26 13:52 + +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", "0004_auto_20220425_1801"), + ] + + operations = [ + migrations.CreateModel( + name="OpenIDConnectEHerkenningBewindvoeringConfig", + 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_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_better_admin_arrayfield.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", + ), + ), + ( + "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_company_claim_name", + models.CharField( + default="aanvrager.kvk", + help_text="Name of the claim in which the KVK of the company being represented is stored", + max_length=50, + verbose_name="vertegenwoordigde company claim name", + ), + ), + ( + "gemachtigde_person_claim_name", + models.CharField( + default="gemachtigde.pseudoID", + help_text="Name of the claim in which the ID of the person representing a company is stored", + max_length=50, + verbose_name="gemachtigde person 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 eHerkenning Bewindvoering", + }, + bases=(mozilla_django_oidc_db.models.CachingMixin, models.Model), + ), + ] diff --git a/src/digid_eherkenning_oidc_generics/migrations/0006_auto_20220428_0949.py b/src/digid_eherkenning_oidc_generics/migrations/0006_auto_20220428_0949.py new file mode 100644 index 0000000000..99b4583435 --- /dev/null +++ b/src/digid_eherkenning_oidc_generics/migrations/0006_auto_20220428_0949.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.13 on 2022-04-28 07:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("digid_eherkenning_oidc_generics", "0005_auto_20220426_1552"), + ] + + operations = [ + migrations.AlterField( + model_name="openidconnecteherkenningbewindvoeringconfig", + name="oidc_op_jwks_endpoint", + field=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", + ), + ), + migrations.AlterField( + model_name="openidconnecteherkenningbewindvoeringconfig", + name="oidc_rp_idp_sign_key", + field=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", + ), + ), + ] diff --git a/src/digid_eherkenning_oidc_generics/mixins.py b/src/digid_eherkenning_oidc_generics/mixins.py index 378f85dd2b..2f281a91bb 100644 --- a/src/digid_eherkenning_oidc_generics/mixins.py +++ b/src/digid_eherkenning_oidc_generics/mixins.py @@ -1,12 +1,29 @@ +import logging +from copy import deepcopy + +from django.contrib.auth.models import AnonymousUser +from django.core.exceptions import SuspiciousOperation + +from glom import PathAccessError, glom from mozilla_django_oidc_db.mixins import SoloConfigMixin as _SoloConfigMixin -from . import digid_machtigen_settings, digid_settings, eherkenning_settings +from digid_eherkenning_oidc_generics.utils import obfuscate_claim + +from . import ( + digid_machtigen_settings, + digid_settings, + eherkenning_bewindvoering_settings, + eherkenning_settings, +) from .models import ( OpenIDConnectDigiDMachtigenConfig, + OpenIDConnectEHerkenningBewindvoeringConfig, OpenIDConnectEHerkenningConfig, OpenIDConnectPublicConfig, ) +logger = logging.getLogger(__name__) + class SoloConfigMixin(_SoloConfigMixin): config_class = "" @@ -31,3 +48,58 @@ class SoloConfigEHerkenningMixin(SoloConfigMixin): class SoloConfigDigiDMachtigenMixin(SoloConfigMixin): config_class = OpenIDConnectDigiDMachtigenConfig settings_attribute = digid_machtigen_settings + + +class SoloConfigEHerkenningBewindvoeringMixin(SoloConfigMixin): + config_class = OpenIDConnectEHerkenningBewindvoeringConfig + settings_attribute = eherkenning_bewindvoering_settings + + +class MachtigenBackendMixin: + 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: + self.request.session[self.session_key] = {} + for claim_name in self.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: + self.log_received_claims(claims) + + for expected_claim in self.claim_names: + try: + glom(claims, expected_claim) + except PathAccessError: + logger.error( + "`%s` not in OIDC claims, cannot proceed with eHerkenning bewindvoering authentication", + expected_claim, + ) + return False + + return True diff --git a/src/digid_eherkenning_oidc_generics/models.py b/src/digid_eherkenning_oidc_generics/models.py index eeadf25054..3517bfba8f 100644 --- a/src/digid_eherkenning_oidc_generics/models.py +++ b/src/digid_eherkenning_oidc_generics/models.py @@ -9,6 +9,9 @@ from .digid_machtigen_settings import DIGID_MACHTIGEN_CUSTOM_OIDC_DB_PREFIX from .digid_settings import DIGID_CUSTOM_OIDC_DB_PREFIX +from .eherkenning_bewindvoering_settings import ( + EHERKENNING_BEWINDVOERING_CUSTOM_OIDC_DB_PREFIX, +) from .eherkenning_settings import EHERKENNING_CUSTOM_OIDC_DB_PREFIX @@ -148,3 +151,39 @@ def custom_oidc_db_prefix(cls): class Meta: verbose_name = _("OpenID Connect configuration for eHerkenning") + + +class OpenIDConnectEHerkenningBewindvoeringConfig(OpenIDConnectBaseConfig): + vertegenwoordigde_company_claim_name = models.CharField( + verbose_name=_("vertegenwoordigde company claim name"), + default="aanvrager.kvk", + max_length=50, + help_text=_( + "Name of the claim in which the KVK of the company being represented is stored" + ), + ) + gemachtigde_person_claim_name = models.CharField( + verbose_name=_("gemachtigde person claim name"), + default="gemachtigde.pseudoID", + max_length=50, + help_text=_( + "Name of the claim in which the ID of the person representing a company 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 EHERKENNING_BEWINDVOERING_CUSTOM_OIDC_DB_PREFIX + + class Meta: + verbose_name = _("OpenID Connect configuration for eHerkenning Bewindvoering") diff --git a/src/openapi.yaml b/src/openapi.yaml index 65309492cd..80a6ce4cb4 100644 --- a/src/openapi.yaml +++ b/src/openapi.yaml @@ -3244,6 +3244,7 @@ components: - digid_oidc - eherkenning_oidc - digid_machtigen_oidc + - eherkenning_bewindvoering_oidc type: string BlankEnum: enum: diff --git a/src/openforms/authentication/contrib/digid_eherkenning_oidc/backends.py b/src/openforms/authentication/contrib/digid_eherkenning_oidc/backends.py index b515b6880c..a87789a5bc 100644 --- a/src/openforms/authentication/contrib/digid_eherkenning_oidc/backends.py +++ b/src/openforms/authentication/contrib/digid_eherkenning_oidc/backends.py @@ -1,22 +1,18 @@ import logging -from copy import deepcopy - -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 ( + MachtigenBackendMixin, SoloConfigDigiDMachtigenMixin, SoloConfigDigiDMixin, + SoloConfigEHerkenningBewindvoeringMixin, SoloConfigEHerkenningMixin, ) -from digid_eherkenning_oidc_generics.utils import obfuscate_claim from .constants import ( DIGID_MACHTIGEN_OIDC_AUTH_SESSION_KEY, DIGID_OIDC_AUTH_SESSION_KEY, + EHERKENNING_BEWINDVOERING_OIDC_AUTH_SESSION_KEY, EHERKENNING_OIDC_AUTH_SESSION_KEY, ) @@ -42,64 +38,28 @@ class OIDCAuthenticationEHerkenningBackend( class OIDCAuthenticationDigiDMachtigenBackend( - SoloConfigDigiDMachtigenMixin, OIDCAuthenticationBackend + MachtigenBackendMixin, 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 = [ + @property + def claim_names(self): + return [ 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) +class OIDCAuthenticationEHerkenningBewindvoeringBackend( + MachtigenBackendMixin, + SoloConfigEHerkenningBewindvoeringMixin, + OIDCAuthenticationBackend, +): + session_key = EHERKENNING_BEWINDVOERING_OIDC_AUTH_SESSION_KEY - def verify_claims(self, claims: dict) -> bool: - expected_claim_names = [ - self.config.vertegenwoordigde_claim_name, - self.config.gemachtigde_claim_name, + @property + def claim_names(self): + return [ + self.config.vertegenwoordigde_company_claim_name, + self.config.gemachtigde_person_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 af3cb390cb..ce6ead1307 100644 --- a/src/openforms/authentication/contrib/digid_eherkenning_oidc/constants.py +++ b/src/openforms/authentication/contrib/digid_eherkenning_oidc/constants.py @@ -1,3 +1,6 @@ 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" +EHERKENNING_BEWINDVOERING_OIDC_AUTH_SESSION_KEY = ( + "eherkenning_bewindvoering_oidc:machtigen" +) diff --git a/src/openforms/authentication/contrib/digid_eherkenning_oidc/eherkenning_bewindvoering_urls.py b/src/openforms/authentication/contrib/digid_eherkenning_oidc/eherkenning_bewindvoering_urls.py new file mode 100644 index 0000000000..12ad7c6984 --- /dev/null +++ b/src/openforms/authentication/contrib/digid_eherkenning_oidc/eherkenning_bewindvoering_urls.py @@ -0,0 +1,24 @@ +from django.urls import path + +from mozilla_django_oidc.urls import urlpatterns + +from .views import ( + EHerkenningBewindvoeringOIDCAuthenticationCallbackView, + EHerkenningBewindvoeringOIDCAuthenticationRequestView, +) + +app_name = "eherkenning_bewindvoering_oidc" + + +urlpatterns = [ + path( + "callback/", + EHerkenningBewindvoeringOIDCAuthenticationCallbackView.as_view(), + name="callback", + ), + path( + "authenticate/", + EHerkenningBewindvoeringOIDCAuthenticationRequestView.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 35b6fbab9f..ae3fefd9aa 100644 --- a/src/openforms/authentication/contrib/digid_eherkenning_oidc/plugin.py +++ b/src/openforms/authentication/contrib/digid_eherkenning_oidc/plugin.py @@ -9,6 +9,7 @@ from digid_eherkenning_oidc_generics.models import ( OpenIDConnectDigiDMachtigenConfig, + OpenIDConnectEHerkenningBewindvoeringConfig, OpenIDConnectEHerkenningConfig, OpenIDConnectPublicConfig, ) @@ -25,6 +26,7 @@ from .constants import ( DIGID_MACHTIGEN_OIDC_AUTH_SESSION_KEY, DIGID_OIDC_AUTH_SESSION_KEY, + EHERKENNING_BEWINDVOERING_OIDC_AUTH_SESSION_KEY, EHERKENNING_OIDC_AUTH_SESSION_KEY, ) @@ -163,3 +165,31 @@ def get_label(self) -> str: def get_logo(self, request) -> Optional[LoginLogo]: return LoginLogo(title=self.get_label(), **get_digid_logo(request)) + + +@register("eherkenning_bewindvoering_oidc") +class EHerkenningBewindvoeringOIDCAuthentication(OIDCAuthentication): + verbose_name = _("eHerkenning bewindvoering via OpenID Connect") + provides_auth = AuthAttribute.kvk + init_url = "eherkenning_bewindvoering_oidc:init" + session_key = EHERKENNING_BEWINDVOERING_OIDC_AUTH_SESSION_KEY + config_class = OpenIDConnectEHerkenningBewindvoeringConfig + + 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 = self.config_class.get_solo() + request.session[FORM_AUTH_SESSION_KEY] = { + "plugin": self.identifier, + "attribute": self.provides_auth, + "value": claim[config.vertegenwoordigde_company_claim_name], + "machtigen": request.session[ + EHERKENNING_BEWINDVOERING_OIDC_AUTH_SESSION_KEY + ], + } + + def get_label(self) -> str: + return "eHerkenning bewindvoering" + + def get_logo(self, request) -> Optional[LoginLogo]: + return LoginLogo(title=self.get_label(), **get_eherkenning_logo(request)) diff --git a/src/openforms/authentication/contrib/digid_eherkenning_oidc/tests/eherkenning_bewindvoering/__init__.py b/src/openforms/authentication/contrib/digid_eherkenning_oidc/tests/eherkenning_bewindvoering/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/openforms/authentication/contrib/digid_eherkenning_oidc/tests/eherkenning_bewindvoering/test_auth_plugin.py b/src/openforms/authentication/contrib/digid_eherkenning_oidc/tests/eherkenning_bewindvoering/test_auth_plugin.py new file mode 100644 index 0000000000..3c91d490a0 --- /dev/null +++ b/src/openforms/authentication/contrib/digid_eherkenning_oidc/tests/eherkenning_bewindvoering/test_auth_plugin.py @@ -0,0 +1,60 @@ +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 EHerkenningBewindvoeringOIDCAuthPluginEndpointTests(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_eherkenning_bewindvoering_oidc_enabled(self, mock_get_solo): + mock_get_solo.return_value = GlobalConfiguration( + plugin_configuration={ + "authentication": { + "eherkenning_bewindvoering_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("eherkenning_bewindvoering_oidc", plugin_names) + + @patch("openforms.plugins.registry.GlobalConfiguration.get_solo") + def test_plugin_list_eherkenning_bewindvoering_oidc_not_enabled( + self, mock_get_solo + ): + mock_get_solo.return_value = GlobalConfiguration( + plugin_configuration={ + "authentication": { + "eherkenning_bewindvoering_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("eherkenning_bewindvoering_oidc", plugin_names) diff --git a/src/openforms/authentication/contrib/digid_eherkenning_oidc/tests/eherkenning_bewindvoering/test_auth_procedure.py b/src/openforms/authentication/contrib/digid_eherkenning_oidc/tests/eherkenning_bewindvoering/test_auth_procedure.py new file mode 100644 index 0000000000..b6b94e05a9 --- /dev/null +++ b/src/openforms/authentication/contrib/digid_eherkenning_oidc/tests/eherkenning_bewindvoering/test_auth_procedure.py @@ -0,0 +1,360 @@ +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, + OpenIDConnectEHerkenningBewindvoeringConfig, + 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"], + 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.OpenIDConnectEHerkenningBewindvoeringConfig.get_solo", + return_value=OpenIDConnectEHerkenningBewindvoeringConfig(**default_config), +) +class EHerkenningBewindvoeringOIDCTests(TestCase): + @classmethod + def setUpTestData(cls): + cls.form = FormFactory.create( + generate_minimal_setup=True, + authentication_backends=["eherkenning_bewindvoering_oidc"], + ) + + def test_redirect_to_eherkenning_bewindvoering_oidc(self, m_get_solo): + login_url = reverse( + "authentication:start", + kwargs={ + "slug": self.form.slug, + "plugin_id": "eherkenning_bewindvoering_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("eherkenning_bewindvoering_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": "eherkenning_bewindvoering_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") + self.assertEqual(query_params["client_id"], "testclient") + self.assertEqual( + query_params["redirect_uri"], + f"http://testserver{reverse('eherkenning_bewindvoering_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": "eherkenning_bewindvoering_oidc", + }, + ), + ) + self.assertEqual(query_params["next"], form_url) + + def test_redirect_to_eherkenning_bewindvoering_oidc_internal_server_error( + self, m_get_solo + ): + login_url = reverse( + "authentication:start", + kwargs={ + "slug": self.form.slug, + "plugin_id": "eherkenning_bewindvoering_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"], "eherkenning_bewindvoering_oidc" + ) + + def test_redirect_to_eherkenning_bewindvoering_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": "eherkenning_bewindvoering_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.OIDCAuthenticationEHerkenningBewindvoeringBackend.verify_claims", + return_value=False, + ): + response = self.client.get( + reverse("eherkenning_bewindvoering_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], + "eherkenning_bewindvoering_oidc", + ) + + @override_settings(CORS_ALLOW_ALL_ORIGINS=False, CORS_ALLOWED_ORIGINS=[]) + def test_redirect_to_disallowed_domain(self, m_get_solo): + login_url = reverse( + "eherkenning_bewindvoering_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"], + 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": "eherkenning_bewindvoering_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("eherkenning_bewindvoering_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": "eherkenning_bewindvoering_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") + self.assertEqual(query_params["client_id"], "testclient") + self.assertEqual( + query_params["redirect_uri"], + f"http://testserver{reverse('eherkenning_bewindvoering_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"], + 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": "eherkenning_bewindvoering_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("eherkenning_bewindvoering_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": "eherkenning_bewindvoering_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") + self.assertEqual(query_params["client_id"], "testclient") + self.assertEqual( + query_params["redirect_uri"], + f"http://testserver{reverse('eherkenning_bewindvoering_oidc:oidc_authentication_callback')}", + ) + self.assertEqual(query_params["kc_idp_hint"], "oidc-digid-machtigen") diff --git a/src/openforms/authentication/contrib/digid_eherkenning_oidc/views.py b/src/openforms/authentication/contrib/digid_eherkenning_oidc/views.py index f4068f25a5..58ac0094bb 100644 --- a/src/openforms/authentication/contrib/digid_eherkenning_oidc/views.py +++ b/src/openforms/authentication/contrib/digid_eherkenning_oidc/views.py @@ -10,6 +10,7 @@ from digid_eherkenning_oidc_generics.mixins import ( SoloConfigDigiDMachtigenMixin, SoloConfigDigiDMixin, + SoloConfigEHerkenningBewindvoeringMixin, SoloConfigEHerkenningMixin, ) from digid_eherkenning_oidc_generics.views import ( @@ -22,6 +23,7 @@ OIDCAuthenticationDigiDBackend, OIDCAuthenticationDigiDMachtigenBackend, OIDCAuthenticationEHerkenningBackend, + OIDCAuthenticationEHerkenningBewindvoeringBackend, ) logger = logging.getLogger(__name__) @@ -107,3 +109,16 @@ class DigiDMachtigenOIDCAuthenticationCallbackView( ): plugin_identifier = "digid_machtigen_oidc" auth_backend_class = OIDCAuthenticationDigiDMachtigenBackend + + +class EHerkenningBewindvoeringOIDCAuthenticationRequestView( + SoloConfigEHerkenningBewindvoeringMixin, OIDCAuthenticationRequestView +): + plugin_identifier = "eherkenning_bewindvoering_oidc" + + +class EHerkenningBewindvoeringOIDCAuthenticationCallbackView( + SoloConfigEHerkenningBewindvoeringMixin, OIDCAuthenticationCallbackView +): + plugin_identifier = "eherkenning_bewindvoering_oidc" + auth_backend_class = OIDCAuthenticationEHerkenningBewindvoeringBackend diff --git a/src/openforms/fixtures/default_admin_index.json b/src/openforms/fixtures/default_admin_index.json index e4b4ae6ec7..6d17d57670 100644 --- a/src/openforms/fixtures/default_admin_index.json +++ b/src/openforms/fixtures/default_admin_index.json @@ -162,6 +162,10 @@ "digid_eherkenning_oidc_generics", "openidconnectdigidmachtigenconfig" ], + [ + "digid_eherkenning_oidc_generics", + "openidconnecteherkenningbewindvoeringconfig" + ], [ "multidomain", "domain" diff --git a/src/openforms/urls.py b/src/openforms/urls.py index 8a11ecbdc6..4324a77383 100644 --- a/src/openforms/urls.py +++ b/src/openforms/urls.py @@ -91,6 +91,12 @@ "openforms.authentication.contrib.digid_eherkenning_oidc.digid_machtigen_urls", ), ), + path( + "eherkenning-bewindvoering-oidc/", + include( + "openforms.authentication.contrib.digid_eherkenning_oidc.eherkenning_bewindvoering_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")),