From b2fe8717668d8f1fe8cd51025199641e018ecb7a Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Mon, 15 Apr 2024 11:07:49 +0200 Subject: [PATCH 1/9] :sparkles: [#2263] Setup config for DigiD/eHerkenning via OIDC task: https://taiga.maykinmedia.nl/project/open-inwoner/task/2263 --- .../conf/app/setup_configuration.py | 70 ++++++++ .../configurations/bootstrap/auth.py | 159 ++++++++++++++++++ 2 files changed, 229 insertions(+) create mode 100644 src/open_inwoner/configurations/bootstrap/auth.py diff --git a/src/open_inwoner/conf/app/setup_configuration.py b/src/open_inwoner/conf/app/setup_configuration.py index 50546ac95b..999d485fa8 100644 --- a/src/open_inwoner/conf/app/setup_configuration.py +++ b/src/open_inwoner/conf/app/setup_configuration.py @@ -10,6 +10,8 @@ "open_inwoner.configurations.bootstrap.kic.ContactmomentenAPIConfigurationStep", "open_inwoner.configurations.bootstrap.kic.KICAPIsConfigurationStep", "open_inwoner.configurations.bootstrap.siteconfig.SiteConfigurationStep", + "open_inwoner.configurations.bootstrap.auth.DigiDOIDCConfigurationStep", + "open_inwoner.configurations.bootstrap.auth.eHerkenningOIDCConfigurationStep", ] OIP_ORGANIZATION = config("OIP_ORGANIZATION", "") @@ -183,3 +185,71 @@ SITE_DISPLAY_SOCIAL = config("SITE_DISPLAY_SOCIAL", None) SITE_THEME_STYLESHEET = config("SITE_THEME_STYLESHEET", None) SITE_EHERKENNING_ENABLED = config("SITE_EHERKENNING_ENABLED", None) + + +# Authentication configuration variables +# NOTE variables are namespaced with `DIGID_OIDC`, but some model field names also have `oidc_...` in them +DIGID_OIDC_ENABLE = config("DIGID_OIDC_ENABLE", True) +DIGID_OIDC_IDENTIFIER_CLAIM_NAME = config("DIGID_OIDC_IDENTIFIER_CLAIM_NAME", None) +DIGID_OIDC_OIDC_RP_CLIENT_ID = config("DIGID_OIDC_OIDC_RP_CLIENT_ID", None) +DIGID_OIDC_OIDC_RP_CLIENT_SECRET = config("DIGID_OIDC_OIDC_RP_CLIENT_SECRET", None) +DIGID_OIDC_OIDC_RP_SIGN_ALGO = config("DIGID_OIDC_OIDC_RP_SIGN_ALGO", None) +DIGID_OIDC_OIDC_RP_SCOPES_LIST = config("DIGID_OIDC_OIDC_RP_SCOPES_LIST", None) +DIGID_OIDC_OIDC_OP_DISCOVERY_ENDPOINT = config( + "DIGID_OIDC_OIDC_OP_DISCOVERY_ENDPOINT", None +) +DIGID_OIDC_OIDC_OP_JWKS_ENDPOINT = config("DIGID_OIDC_OIDC_OP_JWKS_ENDPOINT", None) +DIGID_OIDC_OIDC_OP_AUTHORIZATION_ENDPOINT = config( + "DIGID_OIDC_OIDC_OP_AUTHORIZATION_ENDPOINT", None +) +DIGID_OIDC_OIDC_OP_TOKEN_ENDPOINT = config("DIGID_OIDC_OIDC_OP_TOKEN_ENDPOINT", None) +DIGID_OIDC_OIDC_OP_USER_ENDPOINT = config("DIGID_OIDC_OIDC_OP_USER_ENDPOINT", None) +DIGID_OIDC_OIDC_RP_IDP_SIGN_KEY = config("DIGID_OIDC_OIDC_RP_IDP_SIGN_KEY", None) +DIGID_OIDC_USERINFO_CLAIMS_SOURCE = config("DIGID_OIDC_USERINFO_CLAIMS_SOURCE", None) +DIGID_OIDC_OIDC_OP_LOGOUT_ENDPOINT = config("DIGID_OIDC_OIDC_OP_LOGOUT_ENDPOINT", None) +DIGID_OIDC_ERROR_MESSAGE_MAPPING = config("DIGID_OIDC_ERROR_MESSAGE_MAPPING", None) +DIGID_OIDC_OIDC_KEYCLOAK_IDP_HINT = config("DIGID_OIDC_OIDC_KEYCLOAK_IDP_HINT", None) + +# NOTE variables are namespaced with `EHERKENNING_OIDC`, but some model field names also have `oidc_...` in them +EHERKENNING_OIDC_ENABLE = config("EHERKENNING_OIDC_ENABLE", True) +EHERKENNING_OIDC_IDENTIFIER_CLAIM_NAME = config( + "EHERKENNING_OIDC_IDENTIFIER_CLAIM_NAME", None +) +EHERKENNING_OIDC_OIDC_RP_CLIENT_ID = config("EHERKENNING_OIDC_OIDC_RP_CLIENT_ID", None) +EHERKENNING_OIDC_OIDC_RP_CLIENT_SECRET = config( + "EHERKENNING_OIDC_OIDC_RP_CLIENT_SECRET", None +) +EHERKENNING_OIDC_OIDC_RP_SIGN_ALGO = config("EHERKENNING_OIDC_OIDC_RP_SIGN_ALGO", None) +EHERKENNING_OIDC_OIDC_RP_SCOPES_LIST = config( + "EHERKENNING_OIDC_OIDC_RP_SCOPES_LIST", None +) +EHERKENNING_OIDC_OIDC_OP_DISCOVERY_ENDPOINT = config( + "EHERKENNING_OIDC_OIDC_OP_DISCOVERY_ENDPOINT", None +) +EHERKENNING_OIDC_OIDC_OP_JWKS_ENDPOINT = config( + "EHERKENNING_OIDC_OIDC_OP_JWKS_ENDPOINT", None +) +EHERKENNING_OIDC_OIDC_OP_AUTHORIZATION_ENDPOINT = config( + "EHERKENNING_OIDC_OIDC_OP_AUTHORIZATION_ENDPOINT", None +) +EHERKENNING_OIDC_OIDC_OP_TOKEN_ENDPOINT = config( + "EHERKENNING_OIDC_OIDC_OP_TOKEN_ENDPOINT", None +) +EHERKENNING_OIDC_OIDC_OP_USER_ENDPOINT = config( + "EHERKENNING_OIDC_OIDC_OP_USER_ENDPOINT", None +) +EHERKENNING_OIDC_OIDC_RP_IDP_SIGN_KEY = config( + "EHERKENNING_OIDC_OIDC_RP_IDP_SIGN_KEY", None +) +EHERKENNING_OIDC_USERINFO_CLAIMS_SOURCE = config( + "EHERKENNING_OIDC_USERINFO_CLAIMS_SOURCE", None +) +EHERKENNING_OIDC_OIDC_OP_LOGOUT_ENDPOINT = config( + "EHERKENNING_OIDC_OIDC_OP_LOGOUT_ENDPOINT", None +) +EHERKENNING_OIDC_ERROR_MESSAGE_MAPPING = config( + "EHERKENNING_OIDC_ERROR_MESSAGE_MAPPING", None +) +EHERKENNING_OIDC_OIDC_KEYCLOAK_IDP_HINT = config( + "EHERKENNING_OIDC_OIDC_KEYCLOAK_IDP_HINT", None +) diff --git a/src/open_inwoner/configurations/bootstrap/auth.py b/src/open_inwoner/configurations/bootstrap/auth.py new file mode 100644 index 0000000000..576e0f470f --- /dev/null +++ b/src/open_inwoner/configurations/bootstrap/auth.py @@ -0,0 +1,159 @@ +from django.conf import settings + +from django_setup_configuration.configuration import BaseConfigurationStep +from django_setup_configuration.exceptions import ConfigurationRunFailed + +from digid_eherkenning_oidc_generics.admin import ( + OpenIDConnectDigiDConfigForm, + OpenIDConnectEHerkenningConfigForm, +) +from digid_eherkenning_oidc_generics.models import ( + OpenIDConnectDigiDConfig, + OpenIDConnectEHerkenningConfig, +) + + +class DigiDOIDCConfigurationStep(BaseConfigurationStep): + """ + Configure DigiD authentication via OpenID Connect + """ + + verbose_name = "Configuration for DigiD via OpenID Connect" + required_settings = [ + "DIGID_OIDC_OIDC_RP_CLIENT_ID", + "DIGID_OIDC_OIDC_RP_CLIENT_SECRET", + # NOTE these are part of the model, but not actually part of the admin form + # "DIGID_OIDC_OIDC_USE_NONCE", + # "DIGID_OIDC_OIDC_NONCE_SIZE", + # "DIGID_OIDC_OIDC_STATE_SIZE", + # "DIGID_OIDC_OIDC_EXEMPT_URLS", + ] + all_settings = required_settings + [ + "DIGID_OIDC_IDENTIFIER_CLAIM_NAME", + "DIGID_OIDC_OIDC_RP_SCOPES_LIST", + "DIGID_OIDC_OIDC_RP_SIGN_ALGO", + "DIGID_OIDC_OIDC_RP_IDP_SIGN_KEY", + "DIGID_OIDC_OIDC_OP_DISCOVERY_ENDPOINT", + "DIGID_OIDC_OIDC_OP_JWKS_ENDPOINT", + "DIGID_OIDC_OIDC_OP_AUTHORIZATION_ENDPOINT", + "DIGID_OIDC_OIDC_OP_TOKEN_ENDPOINT", + "DIGID_OIDC_OIDC_OP_USER_ENDPOINT", + "DIGID_OIDC_OIDC_OP_LOGOUT_ENDPOINT", + "DIGID_OIDC_USERINFO_CLAIMS_SOURCE", + "DIGID_OIDC_ERROR_MESSAGE_MAPPING", + "DIGID_OIDC_OIDC_KEYCLOAK_IDP_HINT", + ] + enable_setting = "DIGID_OIDC_ENABLE" + + def is_configured(self) -> bool: + return OpenIDConnectDigiDConfig.get_solo().enabled + + def configure(self): + config = OpenIDConnectDigiDConfig.get_solo() + + # Use the model defaults + form_data = { + field.name: getattr(config, field.name) + for field in OpenIDConnectDigiDConfig._meta.fields + } + + # Only override field values with settings if they are defined + for setting in self.all_settings: + value = getattr(settings, setting, None) + if value is not None: + model_field_name = setting.split("DIGID_OIDC_")[1].lower() + form_data[model_field_name] = value + + form_data["enabled"] = True + + # Saving the form with the default error_message_mapping `{}` causes the save to fail + if not form_data["error_message_mapping"]: + del form_data["error_message_mapping"] + + # Use the admin for to apply validation and fetch URLs from the discovery endpoint + form = OpenIDConnectDigiDConfigForm(data=form_data) + if not form.is_valid(): + raise ConfigurationRunFailed( + f"Something went wrong while saving configuration: {form.errors}" + ) + + form.save() + + def test_configuration(self): + """ + TODO not sure if it is feasible (because there are different possible IdPs), + but it would be nice if we could test the login automatically + """ + + +class eHerkenningOIDCConfigurationStep(BaseConfigurationStep): + """ + Configure eHerkenning authentication via OpenID Connect + """ + + verbose_name = "Configuration for eHerkenning via OpenID Connect" + required_settings = [ + "EHERKENNING_OIDC_OIDC_RP_CLIENT_ID", + "EHERKENNING_OIDC_OIDC_RP_CLIENT_SECRET", + # NOTE these are part of the model, but not actually part of the admin form + # "EHERKENNING_OIDC_OIDC_USE_NONCE", + # "EHERKENNING_OIDC_OIDC_NONCE_SIZE", + # "EHERKENNING_OIDC_OIDC_STATE_SIZE", + # "EHERKENNING_OIDC_OIDC_EXEMPT_URLS", + ] + all_settings = required_settings + [ + "EHERKENNING_OIDC_IDENTIFIER_CLAIM_NAME", + "EHERKENNING_OIDC_OIDC_RP_SCOPES_LIST", + "EHERKENNING_OIDC_OIDC_RP_SIGN_ALGO", + "EHERKENNING_OIDC_OIDC_RP_IDP_SIGN_KEY", + "EHERKENNING_OIDC_OIDC_OP_DISCOVERY_ENDPOINT", + "EHERKENNING_OIDC_OIDC_OP_JWKS_ENDPOINT", + "EHERKENNING_OIDC_OIDC_OP_AUTHORIZATION_ENDPOINT", + "EHERKENNING_OIDC_OIDC_OP_TOKEN_ENDPOINT", + "EHERKENNING_OIDC_OIDC_OP_USER_ENDPOINT", + "EHERKENNING_OIDC_OIDC_OP_LOGOUT_ENDPOINT", + "EHERKENNING_OIDC_USERINFO_CLAIMS_SOURCE", + "EHERKENNING_OIDC_ERROR_MESSAGE_MAPPING", + "EHERKENNING_OIDC_OIDC_KEYCLOAK_IDP_HINT", + ] + enable_setting = "EHERKENNING_OIDC_ENABLE" + + def is_configured(self) -> bool: + return OpenIDConnectEHerkenningConfig.get_solo().enabled + + def configure(self): + config = OpenIDConnectEHerkenningConfig.get_solo() + + # Use the model defaults + form_data = { + field.name: getattr(config, field.name) + for field in OpenIDConnectEHerkenningConfig._meta.fields + } + + # Only override field values with settings if they are defined + for setting in self.all_settings: + value = getattr(settings, setting, None) + if value is not None: + model_field_name = setting.split("EHERKENNING_OIDC_")[1].lower() + form_data[model_field_name] = value + + form_data["enabled"] = True + + # Saving the form with the default error_message_mapping `{}` causes the save to fail + if not form_data["error_message_mapping"]: + del form_data["error_message_mapping"] + + # Use the admin for to apply validation and fetch URLs from the discovery endpoint + form = OpenIDConnectEHerkenningConfigForm(data=form_data) + if not form.is_valid(): + raise ConfigurationRunFailed( + f"Something went wrong while saving configuration: {form.errors}" + ) + + form.save() + + def test_configuration(self): + """ + TODO not sure if it is feasible (because there are different possible IdPs), + but it would be nice if we could test the login automatically + """ From 464b856b5db9ffab55505969ae03090db203fe14 Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Mon, 15 Apr 2024 11:08:12 +0200 Subject: [PATCH 2/9] :white_check_mark: [#2263] Add tests for setup config for DigiD/eHerkenning via OIDC task: https://taiga.maykinmedia.nl/project/open-inwoner/task/2263 --- .../tests/bootstrap/test_setup_auth_config.py | 429 ++++++++++++++++++ 1 file changed, 429 insertions(+) create mode 100644 src/open_inwoner/configurations/tests/bootstrap/test_setup_auth_config.py diff --git a/src/open_inwoner/configurations/tests/bootstrap/test_setup_auth_config.py b/src/open_inwoner/configurations/tests/bootstrap/test_setup_auth_config.py new file mode 100644 index 0000000000..c195d24c25 --- /dev/null +++ b/src/open_inwoner/configurations/tests/bootstrap/test_setup_auth_config.py @@ -0,0 +1,429 @@ +from unittest import skip + +from django.test import TestCase, override_settings + +import requests +import requests_mock +from django_setup_configuration.exceptions import ConfigurationRunFailed +from mozilla_django_oidc_db.models import UserInformationClaimsSources + +from digid_eherkenning_oidc_generics.models import ( + OpenIDConnectDigiDConfig, + OpenIDConnectEHerkenningConfig, +) +from open_inwoner.utils.test import ClearCachesMixin + +from ...bootstrap.auth import ( + DigiDOIDCConfigurationStep, + eHerkenningOIDCConfigurationStep, +) + +IDENTITY_PROVIDER = "https://keycloak.local/realms/digid/" +CONTACTMOMENTEN_API_ROOT = "https://openklant.local/contactmomenten/api/v1/" + +DISCOVERY_ENDPOINT_RESPONSE = { + "issuer": IDENTITY_PROVIDER, + "authorization_endpoint": f"{IDENTITY_PROVIDER}protocol/openid-connect/auth", + "token_endpoint": f"{IDENTITY_PROVIDER}protocol/openid-connect/token", + "userinfo_endpoint": f"{IDENTITY_PROVIDER}protocol/openid-connect/userinfo", + "end_session_endpoint": f"{IDENTITY_PROVIDER}protocol/openid-connect/logout", + "jwks_uri": f"{IDENTITY_PROVIDER}protocol/openid-connect/certs", +} + + +@override_settings( + DIGID_OIDC_OIDC_RP_CLIENT_ID="client-id", + DIGID_OIDC_OIDC_RP_CLIENT_SECRET="secret", + DIGID_OIDC_IDENTIFIER_CLAIM_NAME="claim_name", + DIGID_OIDC_OIDC_RP_SCOPES_LIST=["openid", "bsn", "extra_scope"], + DIGID_OIDC_OIDC_RP_SIGN_ALGO="RS256", + DIGID_OIDC_OIDC_RP_IDP_SIGN_KEY="key", + DIGID_OIDC_OIDC_OP_DISCOVERY_ENDPOINT=None, + DIGID_OIDC_OIDC_OP_JWKS_ENDPOINT=f"{IDENTITY_PROVIDER}protocol/openid-connect/certs", + DIGID_OIDC_OIDC_OP_AUTHORIZATION_ENDPOINT=f"{IDENTITY_PROVIDER}protocol/openid-connect/auth", + DIGID_OIDC_OIDC_OP_TOKEN_ENDPOINT=f"{IDENTITY_PROVIDER}protocol/openid-connect/token", + DIGID_OIDC_OIDC_OP_USER_ENDPOINT=f"{IDENTITY_PROVIDER}protocol/openid-connect/userinfo", + DIGID_OIDC_OIDC_OP_LOGOUT_ENDPOINT=f"{IDENTITY_PROVIDER}protocol/openid-connect/logout", + DIGID_OIDC_USERINFO_CLAIMS_SOURCE=UserInformationClaimsSources.id_token, + DIGID_OIDC_ERROR_MESSAGE_MAPPING={"some_error": "Some readable error"}, + DIGID_OIDC_OIDC_KEYCLOAK_IDP_HINT="parameter", +) +class DigiDOIDCConfigurationTests(ClearCachesMixin, TestCase): + def test_configure(self): + DigiDOIDCConfigurationStep().configure() + + config = OpenIDConnectDigiDConfig.get_solo() + + self.assertTrue(config.enabled) + self.assertEqual(config.oidc_rp_client_id, "client-id") + self.assertEqual(config.oidc_rp_client_secret, "secret") + self.assertEqual(config.identifier_claim_name, "claim_name") + self.assertEqual(config.oidc_rp_scopes_list, ["openid", "bsn", "extra_scope"]) + self.assertEqual(config.oidc_rp_sign_algo, "RS256") + self.assertEqual(config.oidc_rp_idp_sign_key, "key") + self.assertEqual(config.oidc_op_discovery_endpoint, "") + self.assertEqual( + config.oidc_op_jwks_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/certs", + ) + self.assertEqual( + config.oidc_op_authorization_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/auth", + ) + self.assertEqual( + config.oidc_op_token_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/token", + ) + self.assertEqual( + config.oidc_op_user_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/userinfo", + ) + self.assertEqual( + config.oidc_op_logout_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/logout", + ) + self.assertEqual( + config.userinfo_claims_source, UserInformationClaimsSources.id_token + ) + self.assertEqual( + config.error_message_mapping, {"some_error": "Some readable error"} + ) + self.assertEqual(config.oidc_keycloak_idp_hint, "parameter") + + @override_settings( + DIGID_OIDC_IDENTIFIER_CLAIM_NAME=None, + DIGID_OIDC_OIDC_RP_SCOPES_LIST=None, + DIGID_OIDC_OIDC_RP_SIGN_ALGO=None, + DIGID_OIDC_OIDC_RP_IDP_SIGN_KEY=None, + DIGID_OIDC_USERINFO_CLAIMS_SOURCE=None, + DIGID_OIDC_ERROR_MESSAGE_MAPPING=None, + DIGID_OIDC_OIDC_KEYCLOAK_IDP_HINT=None, + ) + def test_configure_use_defaults(self): + DigiDOIDCConfigurationStep().configure() + + config = OpenIDConnectDigiDConfig.get_solo() + + self.assertTrue(config.enabled) + self.assertEqual(config.oidc_rp_client_id, "client-id") + self.assertEqual(config.oidc_rp_client_secret, "secret") + self.assertEqual(config.identifier_claim_name, "bsn") + self.assertEqual(config.oidc_rp_scopes_list, ["openid", "bsn"]) + self.assertEqual(config.oidc_rp_sign_algo, "HS256") + self.assertEqual(config.oidc_rp_idp_sign_key, "") + self.assertEqual(config.oidc_op_discovery_endpoint, "") + self.assertEqual( + config.oidc_op_jwks_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/certs", + ) + self.assertEqual( + config.oidc_op_authorization_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/auth", + ) + self.assertEqual( + config.oidc_op_token_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/token", + ) + self.assertEqual( + config.oidc_op_user_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/userinfo", + ) + self.assertEqual( + config.oidc_op_logout_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/logout", + ) + self.assertEqual( + config.userinfo_claims_source, + UserInformationClaimsSources.userinfo_endpoint, + ) + self.assertEqual(config.error_message_mapping, {}) + self.assertEqual(config.oidc_keycloak_idp_hint, "") + + @override_settings( + DIGID_OIDC_OIDC_OP_DISCOVERY_ENDPOINT=IDENTITY_PROVIDER, + DIGID_OIDC_OIDC_OP_JWKS_ENDPOINT=None, + DIGID_OIDC_OIDC_OP_AUTHORIZATION_ENDPOINT=None, + DIGID_OIDC_OIDC_OP_TOKEN_ENDPOINT=None, + DIGID_OIDC_OIDC_OP_USER_ENDPOINT=None, + DIGID_OIDC_OIDC_OP_LOGOUT_ENDPOINT=None, + ) + @requests_mock.Mocker() + def test_configure_use_discovery_endpoint(self, m): + m.get( + f"{IDENTITY_PROVIDER}.well-known/openid-configuration", + json=DISCOVERY_ENDPOINT_RESPONSE, + ) + + DigiDOIDCConfigurationStep().configure() + + config = OpenIDConnectDigiDConfig.get_solo() + + self.assertTrue(config.enabled) + self.assertEqual(config.oidc_op_discovery_endpoint, IDENTITY_PROVIDER) + self.assertEqual( + config.oidc_op_jwks_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/certs", + ) + self.assertEqual( + config.oidc_op_authorization_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/auth", + ) + self.assertEqual( + config.oidc_op_token_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/token", + ) + self.assertEqual( + config.oidc_op_user_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/userinfo", + ) + self.assertEqual( + config.oidc_op_logout_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/logout", + ) + + @override_settings( + DIGID_OIDC_OIDC_OP_DISCOVERY_ENDPOINT=IDENTITY_PROVIDER, + DIGID_OIDC_OIDC_OP_JWKS_ENDPOINT=None, + DIGID_OIDC_OIDC_OP_AUTHORIZATION_ENDPOINT=None, + DIGID_OIDC_OIDC_OP_TOKEN_ENDPOINT=None, + DIGID_OIDC_OIDC_OP_USER_ENDPOINT=None, + DIGID_OIDC_OIDC_OP_LOGOUT_ENDPOINT=None, + ) + @requests_mock.Mocker() + def test_configure_failure(self, m): + mock_kwargs = ( + {"exc": requests.ConnectTimeout}, + {"exc": requests.ConnectionError}, + {"status_code": 404}, + {"status_code": 403}, + {"status_code": 500}, + ) + for mock_config in mock_kwargs: + with self.subTest(mock=mock_config): + m.get( + f"{IDENTITY_PROVIDER}.well-known/openid-configuration", + **mock_config, + ) + + with self.assertRaises(ConfigurationRunFailed): + DigiDOIDCConfigurationStep().configure() + + self.assertFalse(OpenIDConnectDigiDConfig.get_solo().enabled) + + @skip("Testing config for DigiD OIDC is not implemented yet") + @requests_mock.Mocker() + def test_configuration_check_ok(self, m): + raise NotImplementedError + + @skip("Testing config for DigiD OIDC is not implemented yet") + @requests_mock.Mocker() + def test_configuration_check_failures(self, m): + raise NotImplementedError + + def test_is_configured(self): + config = DigiDOIDCConfigurationStep() + + self.assertFalse(config.is_configured()) + + config.configure() + + self.assertTrue(config.is_configured()) + + +@override_settings( + EHERKENNING_OIDC_OIDC_RP_CLIENT_ID="client-id", + EHERKENNING_OIDC_OIDC_RP_CLIENT_SECRET="secret", + EHERKENNING_OIDC_IDENTIFIER_CLAIM_NAME="claim_name", + EHERKENNING_OIDC_OIDC_RP_SCOPES_LIST=["openid", "kvk", "extra_scope"], + EHERKENNING_OIDC_OIDC_RP_SIGN_ALGO="RS256", + EHERKENNING_OIDC_OIDC_RP_IDP_SIGN_KEY="key", + EHERKENNING_OIDC_OIDC_OP_DISCOVERY_ENDPOINT=None, + EHERKENNING_OIDC_OIDC_OP_JWKS_ENDPOINT=f"{IDENTITY_PROVIDER}protocol/openid-connect/certs", + EHERKENNING_OIDC_OIDC_OP_AUTHORIZATION_ENDPOINT=f"{IDENTITY_PROVIDER}protocol/openid-connect/auth", + EHERKENNING_OIDC_OIDC_OP_TOKEN_ENDPOINT=f"{IDENTITY_PROVIDER}protocol/openid-connect/token", + EHERKENNING_OIDC_OIDC_OP_USER_ENDPOINT=f"{IDENTITY_PROVIDER}protocol/openid-connect/userinfo", + EHERKENNING_OIDC_OIDC_OP_LOGOUT_ENDPOINT=f"{IDENTITY_PROVIDER}protocol/openid-connect/logout", + EHERKENNING_OIDC_USERINFO_CLAIMS_SOURCE=UserInformationClaimsSources.id_token, + EHERKENNING_OIDC_ERROR_MESSAGE_MAPPING={"some_error": "Some readable error"}, + EHERKENNING_OIDC_OIDC_KEYCLOAK_IDP_HINT="parameter", +) +class eHerkenningOIDCConfigurationTests(ClearCachesMixin, TestCase): + def test_configure(self): + eHerkenningOIDCConfigurationStep().configure() + + config = OpenIDConnectEHerkenningConfig.get_solo() + + self.assertTrue(config.enabled) + self.assertEqual(config.oidc_rp_client_id, "client-id") + self.assertEqual(config.oidc_rp_client_secret, "secret") + self.assertEqual(config.identifier_claim_name, "claim_name") + self.assertEqual(config.oidc_rp_scopes_list, ["openid", "kvk", "extra_scope"]) + self.assertEqual(config.oidc_rp_sign_algo, "RS256") + self.assertEqual(config.oidc_rp_idp_sign_key, "key") + self.assertEqual(config.oidc_op_discovery_endpoint, "") + self.assertEqual( + config.oidc_op_jwks_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/certs", + ) + self.assertEqual( + config.oidc_op_authorization_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/auth", + ) + self.assertEqual( + config.oidc_op_token_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/token", + ) + self.assertEqual( + config.oidc_op_user_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/userinfo", + ) + self.assertEqual( + config.oidc_op_logout_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/logout", + ) + self.assertEqual( + config.userinfo_claims_source, UserInformationClaimsSources.id_token + ) + self.assertEqual( + config.error_message_mapping, {"some_error": "Some readable error"} + ) + self.assertEqual(config.oidc_keycloak_idp_hint, "parameter") + + @override_settings( + EHERKENNING_OIDC_IDENTIFIER_CLAIM_NAME=None, + EHERKENNING_OIDC_OIDC_RP_SCOPES_LIST=None, + EHERKENNING_OIDC_OIDC_RP_SIGN_ALGO=None, + EHERKENNING_OIDC_OIDC_RP_IDP_SIGN_KEY=None, + EHERKENNING_OIDC_USERINFO_CLAIMS_SOURCE=None, + EHERKENNING_OIDC_ERROR_MESSAGE_MAPPING=None, + EHERKENNING_OIDC_OIDC_KEYCLOAK_IDP_HINT=None, + ) + def test_configure_use_defaults(self): + eHerkenningOIDCConfigurationStep().configure() + + config = OpenIDConnectEHerkenningConfig.get_solo() + + self.assertTrue(config.enabled) + self.assertEqual(config.oidc_rp_client_id, "client-id") + self.assertEqual(config.oidc_rp_client_secret, "secret") + self.assertEqual(config.identifier_claim_name, "kvk") + self.assertEqual(config.oidc_rp_scopes_list, ["openid", "kvk"]) + self.assertEqual(config.oidc_rp_sign_algo, "HS256") + self.assertEqual(config.oidc_rp_idp_sign_key, "") + self.assertEqual(config.oidc_op_discovery_endpoint, "") + self.assertEqual( + config.oidc_op_jwks_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/certs", + ) + self.assertEqual( + config.oidc_op_authorization_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/auth", + ) + self.assertEqual( + config.oidc_op_token_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/token", + ) + self.assertEqual( + config.oidc_op_user_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/userinfo", + ) + self.assertEqual( + config.oidc_op_logout_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/logout", + ) + self.assertEqual( + config.userinfo_claims_source, + UserInformationClaimsSources.userinfo_endpoint, + ) + self.assertEqual(config.error_message_mapping, {}) + self.assertEqual(config.oidc_keycloak_idp_hint, "") + + @override_settings( + EHERKENNING_OIDC_OIDC_OP_DISCOVERY_ENDPOINT=IDENTITY_PROVIDER, + EHERKENNING_OIDC_OIDC_OP_JWKS_ENDPOINT=None, + EHERKENNING_OIDC_OIDC_OP_AUTHORIZATION_ENDPOINT=None, + EHERKENNING_OIDC_OIDC_OP_TOKEN_ENDPOINT=None, + EHERKENNING_OIDC_OIDC_OP_USER_ENDPOINT=None, + EHERKENNING_OIDC_OIDC_OP_LOGOUT_ENDPOINT=None, + ) + @requests_mock.Mocker() + def test_configure_use_discovery_endpoint(self, m): + m.get( + f"{IDENTITY_PROVIDER}.well-known/openid-configuration", + json=DISCOVERY_ENDPOINT_RESPONSE, + ) + + eHerkenningOIDCConfigurationStep().configure() + + config = OpenIDConnectEHerkenningConfig.get_solo() + + self.assertTrue(config.enabled) + self.assertEqual(config.oidc_op_discovery_endpoint, IDENTITY_PROVIDER) + self.assertEqual( + config.oidc_op_jwks_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/certs", + ) + self.assertEqual( + config.oidc_op_authorization_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/auth", + ) + self.assertEqual( + config.oidc_op_token_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/token", + ) + self.assertEqual( + config.oidc_op_user_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/userinfo", + ) + self.assertEqual( + config.oidc_op_logout_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/logout", + ) + + @override_settings( + EHERKENNING_OIDC_OIDC_OP_DISCOVERY_ENDPOINT=IDENTITY_PROVIDER, + EHERKENNING_OIDC_OIDC_OP_JWKS_ENDPOINT=None, + EHERKENNING_OIDC_OIDC_OP_AUTHORIZATION_ENDPOINT=None, + EHERKENNING_OIDC_OIDC_OP_TOKEN_ENDPOINT=None, + EHERKENNING_OIDC_OIDC_OP_USER_ENDPOINT=None, + EHERKENNING_OIDC_OIDC_OP_LOGOUT_ENDPOINT=None, + ) + @requests_mock.Mocker() + def test_configure_failure(self, m): + mock_kwargs = ( + {"exc": requests.ConnectTimeout}, + {"exc": requests.ConnectionError}, + {"status_code": 404}, + {"status_code": 403}, + {"status_code": 500}, + ) + for mock_config in mock_kwargs: + with self.subTest(mock=mock_config): + m.get( + f"{IDENTITY_PROVIDER}.well-known/openid-configuration", + **mock_config, + ) + + with self.assertRaises(ConfigurationRunFailed): + eHerkenningOIDCConfigurationStep().configure() + + self.assertFalse(OpenIDConnectEHerkenningConfig.get_solo().enabled) + + @skip("Testing config for DigiD OIDC is not implemented yet") + @requests_mock.Mocker() + def test_configuration_check_ok(self, m): + raise NotImplementedError + + @skip("Testing config for DigiD OIDC is not implemented yet") + @requests_mock.Mocker() + def test_configuration_check_failures(self, m): + raise NotImplementedError + + def test_is_configured(self): + config = eHerkenningOIDCConfigurationStep() + + self.assertFalse(config.is_configured()) + + config.configure() + + self.assertTrue(config.is_configured()) From 07f2490e3b5a093a67e985ee35a7826e3e1f9cd2 Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Mon, 8 Apr 2024 14:46:55 +0200 Subject: [PATCH 3/9] :sparkles: [#2263] Setup config for admin login via OIDC task: https://taiga.maykinmedia.nl/project/open-inwoner/task/2263 --- .../conf/app/setup_configuration.py | 33 ++++++++ .../configurations/bootstrap/auth.py | 84 +++++++++++++++++++ 2 files changed, 117 insertions(+) diff --git a/src/open_inwoner/conf/app/setup_configuration.py b/src/open_inwoner/conf/app/setup_configuration.py index 999d485fa8..05d157791e 100644 --- a/src/open_inwoner/conf/app/setup_configuration.py +++ b/src/open_inwoner/conf/app/setup_configuration.py @@ -12,6 +12,7 @@ "open_inwoner.configurations.bootstrap.siteconfig.SiteConfigurationStep", "open_inwoner.configurations.bootstrap.auth.DigiDOIDCConfigurationStep", "open_inwoner.configurations.bootstrap.auth.eHerkenningOIDCConfigurationStep", + "open_inwoner.configurations.bootstrap.auth.AdminOIDCConfigurationStep", ] OIP_ORGANIZATION = config("OIP_ORGANIZATION", "") @@ -253,3 +254,35 @@ EHERKENNING_OIDC_OIDC_KEYCLOAK_IDP_HINT = config( "EHERKENNING_OIDC_OIDC_KEYCLOAK_IDP_HINT", None ) + +# NOTE variables are namespaced with `ADMIN_OIDC`, but some model field names also have `oidc_...` in them +ADMIN_OIDC_ENABLE = config("ADMIN_OIDC_ENABLE", default=True) +ADMIN_OIDC_OIDC_RP_CLIENT_ID = config("ADMIN_OIDC_OIDC_RP_CLIENT_ID", None) +ADMIN_OIDC_OIDC_RP_CLIENT_SECRET = config("ADMIN_OIDC_OIDC_RP_CLIENT_SECRET", None) +ADMIN_OIDC_OIDC_RP_SCOPES_LIST = config("ADMIN_OIDC_OIDC_RP_SCOPES_LIST", None) +ADMIN_OIDC_OIDC_RP_SIGN_ALGO = config("ADMIN_OIDC_OIDC_RP_SIGN_ALGO", None) +ADMIN_OIDC_OIDC_RP_IDP_SIGN_KEY = config("ADMIN_OIDC_OIDC_RP_IDP_SIGN_KEY", None) +ADMIN_OIDC_OIDC_OP_DISCOVERY_ENDPOINT = config( + "ADMIN_OIDC_OIDC_OP_DISCOVERY_ENDPOINT", None +) +ADMIN_OIDC_OIDC_OP_JWKS_ENDPOINT = config("ADMIN_OIDC_OIDC_OP_JWKS_ENDPOINT", None) +ADMIN_OIDC_OIDC_OP_AUTHORIZATION_ENDPOINT = config( + "ADMIN_OIDC_OIDC_OP_AUTHORIZATION_ENDPOINT", None +) +ADMIN_OIDC_OIDC_OP_TOKEN_ENDPOINT = config("ADMIN_OIDC_OIDC_OP_TOKEN_ENDPOINT", None) +ADMIN_OIDC_OIDC_OP_USER_ENDPOINT = config("ADMIN_OIDC_OIDC_OP_USER_ENDPOINT", None) +ADMIN_OIDC_USERNAME_CLAIM = config("ADMIN_OIDC_USERNAME_CLAIM", None) +ADMIN_OIDC_GROUPS_CLAIM = config("ADMIN_OIDC_GROUPS_CLAIM", None) +ADMIN_OIDC_CLAIM_MAPPING = config("ADMIN_OIDC_CLAIM_MAPPING", None) +ADMIN_OIDC_SYNC_GROUPS = config("ADMIN_OIDC_SYNC_GROUPS", None) +ADMIN_OIDC_SYNC_GROUPS_GLOB_PATTERN = config( + "ADMIN_OIDC_SYNC_GROUPS_GLOB_PATTERN", None +) +ADMIN_OIDC_DEFAULT_GROUPS = config("ADMIN_OIDC_DEFAULT_GROUPS", None) +ADMIN_OIDC_MAKE_USERS_STAFF = config("ADMIN_OIDC_MAKE_USERS_STAFF", None) +ADMIN_OIDC_SUPERUSER_GROUP_NAMES = config("ADMIN_OIDC_SUPERUSER_GROUP_NAMES", None) +ADMIN_OIDC_OIDC_USE_NONCE = config("ADMIN_OIDC_OIDC_USE_NONCE", None) +ADMIN_OIDC_OIDC_NONCE_SIZE = config("ADMIN_OIDC_OIDC_NONCE_SIZE", None) +ADMIN_OIDC_OIDC_STATE_SIZE = config("ADMIN_OIDC_OIDC_STATE_SIZE", None) +ADMIN_OIDC_OIDC_EXEMPT_URLS = config("ADMIN_OIDC_OIDC_EXEMPT_URLS", None) +ADMIN_OIDC_USERINFO_CLAIMS_SOURCE = config("ADMIN_OIDC_USERINFO_CLAIMS_SOURCE", None) diff --git a/src/open_inwoner/configurations/bootstrap/auth.py b/src/open_inwoner/configurations/bootstrap/auth.py index 576e0f470f..64ca134733 100644 --- a/src/open_inwoner/configurations/bootstrap/auth.py +++ b/src/open_inwoner/configurations/bootstrap/auth.py @@ -1,7 +1,10 @@ from django.conf import settings +from django.contrib.auth.models import Group from django_setup_configuration.configuration import BaseConfigurationStep from django_setup_configuration.exceptions import ConfigurationRunFailed +from mozilla_django_oidc_db.forms import OpenIDConnectConfigForm +from mozilla_django_oidc_db.models import OpenIDConnectConfig from digid_eherkenning_oidc_generics.admin import ( OpenIDConnectDigiDConfigForm, @@ -157,3 +160,84 @@ def test_configuration(self): TODO not sure if it is feasible (because there are different possible IdPs), but it would be nice if we could test the login automatically """ + + +class AdminOIDCConfigurationStep(BaseConfigurationStep): + """ + Configure admin login via OpenID Connect + """ + + verbose_name = "Configuration for admin login via OpenID Connect" + required_settings = [ + "ADMIN_OIDC_OIDC_RP_CLIENT_ID", + "ADMIN_OIDC_OIDC_RP_CLIENT_SECRET", + ] + all_settings = required_settings + [ + "ADMIN_OIDC_OIDC_RP_SCOPES_LIST", + "ADMIN_OIDC_OIDC_RP_SIGN_ALGO", + "ADMIN_OIDC_OIDC_RP_IDP_SIGN_KEY", + "ADMIN_OIDC_OIDC_OP_DISCOVERY_ENDPOINT", + "ADMIN_OIDC_OIDC_OP_JWKS_ENDPOINT", + "ADMIN_OIDC_OIDC_OP_AUTHORIZATION_ENDPOINT", + "ADMIN_OIDC_OIDC_OP_TOKEN_ENDPOINT", + "ADMIN_OIDC_OIDC_OP_USER_ENDPOINT", + "ADMIN_OIDC_USERNAME_CLAIM", + "ADMIN_OIDC_GROUPS_CLAIM", + "ADMIN_OIDC_CLAIM_MAPPING", + "ADMIN_OIDC_SYNC_GROUPS", + "ADMIN_OIDC_SYNC_GROUPS_GLOB_PATTERN", + "ADMIN_OIDC_DEFAULT_GROUPS", + "ADMIN_OIDC_MAKE_USERS_STAFF", + "ADMIN_OIDC_SUPERUSER_GROUP_NAMES", + "ADMIN_OIDC_OIDC_USE_NONCE", + "ADMIN_OIDC_OIDC_NONCE_SIZE", + "ADMIN_OIDC_OIDC_STATE_SIZE", + "ADMIN_OIDC_OIDC_EXEMPT_URLS", + "ADMIN_OIDC_USERINFO_CLAIMS_SOURCE", + ] + enable_setting = "ADMIN_OIDC_ENABLE" + + def is_configured(self) -> bool: + return OpenIDConnectConfig.get_solo().enabled + + def configure(self): + config = OpenIDConnectConfig.get_solo() + + # Use the model defaults + form_data = { + field.name: getattr(config, field.name) + for field in OpenIDConnectConfig._meta.fields + } + + # `email` is in the claim_mapping by default, but email is used as the username field + # by OIP, and you cannot map the username field when using OIDC + if "email" in form_data["claim_mapping"]: + del form_data["claim_mapping"]["email"] + + # Only override field values with settings if they are defined + for setting in self.all_settings: + value = getattr(settings, setting, None) + if value is not None: + model_field_name = setting.split("ADMIN_OIDC_")[1].lower() + if model_field_name == "default_groups": + for group_name in value: + Group.objects.get_or_create(name=group_name) + value = Group.objects.filter(name__in=value) + + form_data[model_field_name] = value + form_data["enabled"] = True + + # Use the admin for to apply validation and fetch URLs from the discovery endpoint + form = OpenIDConnectConfigForm(data=form_data) + if not form.is_valid(): + raise ConfigurationRunFailed( + f"Something went wrong while saving configuration: {form.errors}" + ) + + form.save() + + def test_configuration(self): + """ + TODO not sure if it is feasible (because there are different possible IdPs), + but it would be nice if we could test the login automatically + """ From 350412dd8e4f7aa94f679339af59d380ce22fbd3 Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Mon, 8 Apr 2024 14:47:17 +0200 Subject: [PATCH 4/9] :white_check_mark: [#2263] Add tests for setup config for admin login via OIDC task: https://taiga.maykinmedia.nl/project/open-inwoner/task/2263 --- .../tests/bootstrap/test_setup_auth_config.py | 232 +++++++++++++++++- 1 file changed, 231 insertions(+), 1 deletion(-) diff --git a/src/open_inwoner/configurations/tests/bootstrap/test_setup_auth_config.py b/src/open_inwoner/configurations/tests/bootstrap/test_setup_auth_config.py index c195d24c25..d748ff77f2 100644 --- a/src/open_inwoner/configurations/tests/bootstrap/test_setup_auth_config.py +++ b/src/open_inwoner/configurations/tests/bootstrap/test_setup_auth_config.py @@ -5,7 +5,10 @@ import requests import requests_mock from django_setup_configuration.exceptions import ConfigurationRunFailed -from mozilla_django_oidc_db.models import UserInformationClaimsSources +from mozilla_django_oidc_db.models import ( + OpenIDConnectConfig, + UserInformationClaimsSources, +) from digid_eherkenning_oidc_generics.models import ( OpenIDConnectDigiDConfig, @@ -14,6 +17,7 @@ from open_inwoner.utils.test import ClearCachesMixin from ...bootstrap.auth import ( + AdminOIDCConfigurationStep, DigiDOIDCConfigurationStep, eHerkenningOIDCConfigurationStep, ) @@ -427,3 +431,229 @@ def test_is_configured(self): config.configure() self.assertTrue(config.is_configured()) + + +@override_settings( + ADMIN_OIDC_OIDC_RP_CLIENT_ID="client-id", + ADMIN_OIDC_OIDC_RP_CLIENT_SECRET="secret", + ADMIN_OIDC_OIDC_RP_SCOPES_LIST=["open_id", "email", "profile", "extra_scope"], + ADMIN_OIDC_OIDC_RP_SIGN_ALGO="RS256", + ADMIN_OIDC_OIDC_RP_IDP_SIGN_KEY="key", + ADMIN_OIDC_OIDC_OP_DISCOVERY_ENDPOINT=None, + ADMIN_OIDC_OIDC_OP_JWKS_ENDPOINT=f"{IDENTITY_PROVIDER}protocol/openid-connect/certs", + ADMIN_OIDC_OIDC_OP_AUTHORIZATION_ENDPOINT=f"{IDENTITY_PROVIDER}protocol/openid-connect/auth", + ADMIN_OIDC_OIDC_OP_TOKEN_ENDPOINT=f"{IDENTITY_PROVIDER}protocol/openid-connect/token", + ADMIN_OIDC_OIDC_OP_USER_ENDPOINT=f"{IDENTITY_PROVIDER}protocol/openid-connect/userinfo", + ADMIN_OIDC_USERNAME_CLAIM="claim_name", + ADMIN_OIDC_GROUPS_CLAIM="groups_claim_name", + ADMIN_OIDC_CLAIM_MAPPING={"first_name": "given_name"}, + ADMIN_OIDC_SYNC_GROUPS=False, + ADMIN_OIDC_SYNC_GROUPS_GLOB_PATTERN="local.groups.*", + ADMIN_OIDC_DEFAULT_GROUPS=["Admins", "Read-only"], + ADMIN_OIDC_MAKE_USERS_STAFF=True, + ADMIN_OIDC_SUPERUSER_GROUP_NAMES=["superuser"], + ADMIN_OIDC_OIDC_USE_NONCE=False, + ADMIN_OIDC_OIDC_NONCE_SIZE=48, + ADMIN_OIDC_OIDC_STATE_SIZE=48, + ADMIN_OIDC_OIDC_EXEMPT_URLS=["http://testserver/some-endpoint"], + ADMIN_OIDC_USERINFO_CLAIMS_SOURCE=UserInformationClaimsSources.id_token, +) +class AdminOIDCConfigurationTests(ClearCachesMixin, TestCase): + def test_configure(self): + AdminOIDCConfigurationStep().configure() + + config = OpenIDConnectConfig.get_solo() + + self.assertTrue(config.enabled) + self.assertEqual(config.oidc_rp_client_id, "client-id") + self.assertEqual(config.oidc_rp_client_secret, "secret") + self.assertEqual( + config.oidc_rp_scopes_list, ["open_id", "email", "profile", "extra_scope"] + ) + self.assertEqual(config.oidc_rp_sign_algo, "RS256") + self.assertEqual(config.oidc_rp_idp_sign_key, "key") + self.assertEqual(config.oidc_op_discovery_endpoint, "") + self.assertEqual( + config.oidc_op_jwks_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/certs", + ) + self.assertEqual( + config.oidc_op_authorization_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/auth", + ) + self.assertEqual( + config.oidc_op_token_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/token", + ) + self.assertEqual( + config.oidc_op_user_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/userinfo", + ) + self.assertEqual(config.username_claim, "claim_name") + self.assertEqual(config.groups_claim, "groups_claim_name") + self.assertEqual(config.claim_mapping, {"first_name": "given_name"}) + self.assertEqual(config.sync_groups, False) + self.assertEqual(config.sync_groups_glob_pattern, "local.groups.*") + self.assertEqual( + list(group.name for group in config.default_groups.all()), + ["Admins", "Read-only"], + ) + self.assertEqual(config.make_users_staff, True) + self.assertEqual(config.superuser_group_names, ["superuser"]) + self.assertEqual(config.oidc_use_nonce, False) + self.assertEqual(config.oidc_nonce_size, 48) + self.assertEqual(config.oidc_state_size, 48) + self.assertEqual(config.oidc_exempt_urls, ["http://testserver/some-endpoint"]) + self.assertEqual( + config.userinfo_claims_source, UserInformationClaimsSources.id_token + ) + + @override_settings( + ADMIN_OIDC_OIDC_RP_SCOPES_LIST=None, + ADMIN_OIDC_OIDC_RP_SIGN_ALGO=None, + ADMIN_OIDC_OIDC_RP_IDP_SIGN_KEY=None, + ADMIN_OIDC_USERNAME_CLAIM=None, + ADMIN_OIDC_CLAIM_MAPPING=None, + ADMIN_OIDC_SYNC_GROUPS=None, + ADMIN_OIDC_SYNC_GROUPS_GLOB_PATTERN=None, + ADMIN_OIDC_MAKE_USERS_STAFF=None, + ADMIN_OIDC_OIDC_USE_NONCE=None, + ADMIN_OIDC_OIDC_NONCE_SIZE=None, + ADMIN_OIDC_OIDC_STATE_SIZE=None, + ADMIN_OIDC_OIDC_EXEMPT_URLS=None, + ADMIN_OIDC_USERINFO_CLAIMS_SOURCE=None, + ) + def test_configure_use_defaults(self): + AdminOIDCConfigurationStep().configure() + + config = OpenIDConnectConfig.get_solo() + + self.assertTrue(config.enabled) + self.assertEqual(config.oidc_rp_client_id, "client-id") + self.assertEqual(config.oidc_rp_client_secret, "secret") + self.assertEqual(config.oidc_rp_scopes_list, ["openid", "email", "profile"]) + self.assertEqual(config.oidc_rp_sign_algo, "HS256") + self.assertEqual(config.oidc_rp_idp_sign_key, "") + self.assertEqual(config.oidc_op_discovery_endpoint, "") + self.assertEqual( + config.oidc_op_jwks_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/certs", + ) + self.assertEqual( + config.oidc_op_authorization_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/auth", + ) + self.assertEqual( + config.oidc_op_token_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/token", + ) + self.assertEqual( + config.oidc_op_user_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/userinfo", + ) + self.assertEqual(config.username_claim, "sub") + self.assertEqual(config.groups_claim, "groups_claim_name") + self.assertEqual( + config.claim_mapping, + {"last_name": "family_name", "first_name": "given_name"}, + ) + self.assertEqual(config.sync_groups, True) + self.assertEqual(config.sync_groups_glob_pattern, "*") + self.assertEqual( + list(group.name for group in config.default_groups.all()), + ["Admins", "Read-only"], + ) + self.assertEqual(config.make_users_staff, False) + self.assertEqual(config.superuser_group_names, ["superuser"]) + self.assertEqual(config.oidc_use_nonce, True) + self.assertEqual(config.oidc_nonce_size, 32) + self.assertEqual(config.oidc_state_size, 32) + self.assertEqual(config.oidc_exempt_urls, []) + self.assertEqual( + config.userinfo_claims_source, + UserInformationClaimsSources.userinfo_endpoint, + ) + + @override_settings( + ADMIN_OIDC_OIDC_OP_DISCOVERY_ENDPOINT=IDENTITY_PROVIDER, + ADMIN_OIDC_OIDC_OP_JWKS_ENDPOINT=None, + ADMIN_OIDC_OIDC_OP_AUTHORIZATION_ENDPOINT=None, + ADMIN_OIDC_OIDC_OP_TOKEN_ENDPOINT=None, + ADMIN_OIDC_OIDC_OP_USER_ENDPOINT=None, + ) + @requests_mock.Mocker() + def test_configure_use_discovery_endpoint(self, m): + m.get( + f"{IDENTITY_PROVIDER}.well-known/openid-configuration", + json=DISCOVERY_ENDPOINT_RESPONSE, + ) + + AdminOIDCConfigurationStep().configure() + + config = OpenIDConnectConfig.get_solo() + + self.assertTrue(config.enabled) + self.assertEqual(config.oidc_op_discovery_endpoint, IDENTITY_PROVIDER) + self.assertEqual( + config.oidc_op_jwks_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/certs", + ) + self.assertEqual( + config.oidc_op_authorization_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/auth", + ) + self.assertEqual( + config.oidc_op_token_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/token", + ) + self.assertEqual( + config.oidc_op_user_endpoint, + f"{IDENTITY_PROVIDER}protocol/openid-connect/userinfo", + ) + + @override_settings( + ADMIN_OIDC_OIDC_OP_DISCOVERY_ENDPOINT=IDENTITY_PROVIDER, + ADMIN_OIDC_OIDC_OP_JWKS_ENDPOINT=None, + ADMIN_OIDC_OIDC_OP_AUTHORIZATION_ENDPOINT=None, + ADMIN_OIDC_OIDC_OP_TOKEN_ENDPOINT=None, + ADMIN_OIDC_OIDC_OP_USER_ENDPOINT=None, + ) + @requests_mock.Mocker() + def test_configure_failure(self, m): + mock_kwargs = ( + {"exc": requests.ConnectTimeout}, + {"exc": requests.ConnectionError}, + {"status_code": 404}, + {"status_code": 403}, + {"status_code": 500}, + ) + for mock_config in mock_kwargs: + with self.subTest(mock=mock_config): + m.get( + f"{IDENTITY_PROVIDER}.well-known/openid-configuration", + **mock_config, + ) + + with self.assertRaises(ConfigurationRunFailed): + AdminOIDCConfigurationStep().configure() + + self.assertFalse(OpenIDConnectConfig.get_solo().enabled) + + @skip("Testing config for DigiD OIDC is not implemented yet") + @requests_mock.Mocker() + def test_configuration_check_ok(self, m): + raise NotImplementedError + + @skip("Testing config for DigiD OIDC is not implemented yet") + @requests_mock.Mocker() + def test_configuration_check_failures(self, m): + raise NotImplementedError + + def test_is_configured(self): + config = AdminOIDCConfigurationStep() + + self.assertFalse(config.is_configured()) + + config.configure() + + self.assertTrue(config.is_configured()) From 1120aa936081c6e5f0a9a2f74b5509240fa46088 Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Thu, 11 Apr 2024 15:13:51 +0200 Subject: [PATCH 5/9] :sparkles: [#2263] Setup config for DigiD/eHerkenning SAML login https://taiga.maykinmedia.nl/project/open-inwoner/task/2263 also remove deprecated DIGID settings from base.py --- .../templates/registration/login.html | 44 ++-- .../conf/app/setup_configuration.py | 99 +++++++ src/open_inwoner/conf/base.py | 38 +-- src/open_inwoner/conf/production.py | 8 +- .../configurations/bootstrap/auth.py | 247 ++++++++++++++++++ 5 files changed, 377 insertions(+), 59 deletions(-) diff --git a/src/open_inwoner/accounts/templates/registration/login.html b/src/open_inwoner/accounts/templates/registration/login.html index 5a11e3abcd..faf2d4c6b4 100644 --- a/src/open_inwoner/accounts/templates/registration/login.html +++ b/src/open_inwoner/accounts/templates/registration/login.html @@ -15,30 +15,28 @@

{% trans 'Welkom' %}

{% if login_text %}
{{ login_text|markdown|safe }}
{% endif %}
- {% if settings.DIGID_ENABLED %} - {% get_solo 'digid_eherkenning_oidc_generics.OpenIDConnectDigiDConfig' as digid_oidc_config %} - {% if digid_oidc_config.enabled %} - {% render_card direction='horizontal' tinted=True compact=True %} - - {% url 'digid_oidc:init' as href %} - {% with href|addnexturl:next as href_with_next %} - {% link href=href_with_next text=_('Inloggen met DigiD') secondary=True icon='arrow_forward' extra_classes="link--digid" %} - {% endwith %} - {% endrender_card %} - {% else %} - {% render_card direction='horizontal' tinted=True compact=True %} - - {% url 'digid:login' as href %} - {% with href|addnexturl:next as href_with_next %} - {% link href=href_with_next text=_('Inloggen met DigiD') secondary=True icon='arrow_forward' extra_classes="link--digid" %} - {% endwith %} + {% get_solo 'digid_eherkenning_oidc_generics.OpenIDConnectDigiDConfig' as digid_oidc_config %} + {% if digid_oidc_config.enabled %} + {% render_card direction='horizontal' tinted=True compact=True %} + + {% url 'digid_oidc:init' as href %} + {% with href|addnexturl:next as href_with_next %} + {% link href=href_with_next text=_('Inloggen met DigiD') secondary=True icon='arrow_forward' extra_classes="link--digid" %} + {% endwith %} {% endrender_card %} - {% endif %} - {% endif %} + {% elif settings.DIGID_ENABLED %} + {% render_card direction='horizontal' tinted=True compact=True %} + + {% url 'digid:login' as href %} + {% with href|addnexturl:next as href_with_next %} + {% link href=href_with_next text=_('Inloggen met DigiD') secondary=True icon='arrow_forward' extra_classes="link--digid" %} + {% endwith %} + {% endrender_card %} + {% endif %} {% if eherkenning_enabled %} {% get_solo 'digid_eherkenning_oidc_generics.OpenIDConnectEHerkenningConfig' as eherkenning_oidc_config %} diff --git a/src/open_inwoner/conf/app/setup_configuration.py b/src/open_inwoner/conf/app/setup_configuration.py index 05d157791e..2245acf158 100644 --- a/src/open_inwoner/conf/app/setup_configuration.py +++ b/src/open_inwoner/conf/app/setup_configuration.py @@ -13,6 +13,8 @@ "open_inwoner.configurations.bootstrap.auth.DigiDOIDCConfigurationStep", "open_inwoner.configurations.bootstrap.auth.eHerkenningOIDCConfigurationStep", "open_inwoner.configurations.bootstrap.auth.AdminOIDCConfigurationStep", + "open_inwoner.configurations.bootstrap.auth.DigiDConfigurationStep", + "open_inwoner.configurations.bootstrap.auth.eHerkenningConfigurationStep", ] OIP_ORGANIZATION = config("OIP_ORGANIZATION", "") @@ -286,3 +288,100 @@ ADMIN_OIDC_OIDC_STATE_SIZE = config("ADMIN_OIDC_OIDC_STATE_SIZE", None) ADMIN_OIDC_OIDC_EXEMPT_URLS = config("ADMIN_OIDC_OIDC_EXEMPT_URLS", None) ADMIN_OIDC_USERINFO_CLAIMS_SOURCE = config("ADMIN_OIDC_USERINFO_CLAIMS_SOURCE", None) + +# TODO this setting is currently also used to determine whether or not to display +# the DigiD login link in the template, that should probably be replace by a flag on the +# SiteConfiguration +DIGID_ENABLED = config("DIGID_ENABLED", default=True) +DIGID_CERTIFICATE_LABEL = config("DIGID_CERTIFICATE_LABEL", None) +DIGID_CERTIFICATE_TYPE = config("DIGID_CERTIFICATE_TYPE", None) +DIGID_CERTIFICATE_PUBLIC_CERTIFICATE = config( + "DIGID_CERTIFICATE_PUBLIC_CERTIFICATE", None +) +DIGID_CERTIFICATE_PRIVATE_KEY = config("DIGID_CERTIFICATE_PRIVATE_KEY", None) +DIGID_METADATA_FILE_SOURCE = config("DIGID_METADATA_FILE_SOURCE", None) +DIGID_WANT_ASSERTIONS_SIGNED = config("DIGID_WANT_ASSERTIONS_SIGNED", None) +DIGID_WANT_ASSERTIONS_ENCRYPTED = config("DIGID_WANT_ASSERTIONS_ENCRYPTED", None) +DIGID_ARTIFACT_RESOLVE_CONTENT_TYPE = config( + "DIGID_ARTIFACT_RESOLVE_CONTENT_TYPE", None +) +DIGID_KEY_PASSPHRASE = config("DIGID_KEY_PASSPHRASE", None) +DIGID_SIGNATURE_ALGORITHM = config("DIGID_SIGNATURE_ALGORITHM", None) +DIGID_DIGEST_ALGORITHM = config("DIGID_DIGEST_ALGORITHM", None) +DIGID_ENTITY_ID = config("DIGID_ENTITY_ID", None) +DIGID_BASE_URL = config("DIGID_BASE_URL", None) +DIGID_SERVICE_NAME = config("DIGID_SERVICE_NAME", None) +DIGID_SERVICE_DESCRIPTION = config("DIGID_SERVICE_DESCRIPTION", None) +DIGID_TECHNICAL_CONTACT_PERSON_TELEPHONE = config( + "DIGID_TECHNICAL_CONTACT_PERSON_TELEPHONE", None +) +DIGID_TECHNICAL_CONTACT_PERSON_EMAIL = config( + "DIGID_TECHNICAL_CONTACT_PERSON_EMAIL", None +) +DIGID_ORGANIZATION_URL = config("DIGID_ORGANIZATION_URL", None) +DIGID_ORGANIZATION_NAME = config("DIGID_ORGANIZATION_NAME", None) +DIGID_ATTRIBUTE_CONSUMING_SERVICE_INDEX = config( + "DIGID_ATTRIBUTE_CONSUMING_SERVICE_INDEX", None +) +DIGID_REQUESTED_ATTRIBUTES = config("DIGID_REQUESTED_ATTRIBUTES", None) +DIGID_SLO = config("DIGID_SLO", None) + +EHERKENNING_ENABLE = config("EHERKENNING_ENABLE", default=True) +EHERKENNING_CERTIFICATE_LABEL = config("EHERKENNING_CERTIFICATE_LABEL", None) +EHERKENNING_CERTIFICATE_TYPE = config("EHERKENNING_CERTIFICATE_TYPE", None) +EHERKENNING_CERTIFICATE_PUBLIC_CERTIFICATE = config( + "EHERKENNING_CERTIFICATE_PUBLIC_CERTIFICATE", None +) +EHERKENNING_CERTIFICATE_PRIVATE_KEY = config( + "EHERKENNING_CERTIFICATE_PRIVATE_KEY", None +) +EHERKENNING_METADATA_FILE_SOURCE = config("EHERKENNING_METADATA_FILE_SOURCE", None) +EHERKENNING_WANT_ASSERTIONS_SIGNED = config("EHERKENNING_WANT_ASSERTIONS_SIGNED", None) +EHERKENNING_WANT_ASSERTIONS_ENCRYPTED = config( + "EHERKENNING_WANT_ASSERTIONS_ENCRYPTED", None +) +EHERKENNING_ARTIFACT_RESOLVE_CONTENT_TYPE = config( + "EHERKENNING_ARTIFACT_RESOLVE_CONTENT_TYPE", None +) +EHERKENNING_KEY_PASSPHRASE = config("EHERKENNING_KEY_PASSPHRASE", None) +EHERKENNING_SIGNATURE_ALGORITHM = config("EHERKENNING_SIGNATURE_ALGORITHM", None) +EHERKENNING_DIGEST_ALGORITHM = config("EHERKENNING_DIGEST_ALGORITHM", None) +EHERKENNING_ENTITY_ID = config("EHERKENNING_ENTITY_ID", None) +EHERKENNING_BASE_URL = config("EHERKENNING_BASE_URL", None) +EHERKENNING_SERVICE_NAME = config("EHERKENNING_SERVICE_NAME", None) +EHERKENNING_SERVICE_DESCRIPTION = config("EHERKENNING_SERVICE_DESCRIPTION", None) +EHERKENNING_TECHNICAL_CONTACT_PERSON_TELEPHONE = config( + "EHERKENNING_TECHNICAL_CONTACT_PERSON_TELEPHONE", None +) +EHERKENNING_TECHNICAL_CONTACT_PERSON_EMAIL = config( + "EHERKENNING_TECHNICAL_CONTACT_PERSON_EMAIL", None +) +EHERKENNING_ORGANIZATION_URL = config("EHERKENNING_ORGANIZATION_URL", None) +EHERKENNING_ORGANIZATION_NAME = config("EHERKENNING_ORGANIZATION_NAME", None) +EHERKENNING_EH_LOA = config("EHERKENNING_EH_LOA", None) +EHERKENNING_EH_ATTRIBUTE_CONSUMING_SERVICE_INDEX = config( + "EHERKENNING_EH_ATTRIBUTE_CONSUMING_SERVICE_INDEX", None +) +EHERKENNING_EH_REQUESTED_ATTRIBUTES = config( + "EHERKENNING_EH_REQUESTED_ATTRIBUTES", None +) +EHERKENNING_EH_SERVICE_UUID = config("EHERKENNING_EH_SERVICE_UUID", None) +EHERKENNING_EH_SERVICE_INSTANCE_UUID = config( + "EHERKENNING_EH_SERVICE_INSTANCE_UUID", None +) +EHERKENNING_EIDAS_LOA = config("EHERKENNING_EIDAS_LOA", None) +EHERKENNING_EIDAS_ATTRIBUTE_CONSUMING_SERVICE_INDEX = config( + "EHERKENNING_EIDAS_ATTRIBUTE_CONSUMING_SERVICE_INDEX", None +) +EHERKENNING_EIDAS_REQUESTED_ATTRIBUTES = config( + "EHERKENNING_EIDAS_REQUESTED_ATTRIBUTES", None +) +EHERKENNING_EIDAS_SERVICE_UUID = config("EHERKENNING_EIDAS_SERVICE_UUID", None) +EHERKENNING_EIDAS_SERVICE_INSTANCE_UUID = config( + "EHERKENNING_EIDAS_SERVICE_INSTANCE_UUID", None +) +EHERKENNING_OIN = config("EHERKENNING_OIN", None) +EHERKENNING_NO_EIDAS = config("EHERKENNING_NO_EIDAS", None) +EHERKENNING_PRIVACY_POLICY = config("EHERKENNING_PRIVACY_POLICY", None) +EHERKENNING_MAKELAAR_ID = config("EHERKENNING_MAKELAAR_ID", None) +EHERKENNING_SERVICE_LANGUAGE = config("EHERKENNING_SERVICE_LANGUAGE", None) diff --git a/src/open_inwoner/conf/base.py b/src/open_inwoner/conf/base.py index cda91d591c..70fc4e937a 100644 --- a/src/open_inwoner/conf/base.py +++ b/src/open_inwoner/conf/base.py @@ -836,38 +836,11 @@ # # DIGID # - -if ALLOWED_HOSTS: - BASE_URL = "https://{}".format(ALLOWED_HOSTS[0]) -else: - BASE_URL = "https://example.com" - DIGID_MOCK = config("DIGID_MOCK", default=True) -DIGID_ENABLED = config("DIGID_ENABLED", default=True) -DIGID_METADATA = config("DIGID_METADATA", "") -SSL_CERTIFICATE_PATH = config("SSL_CERTIFICATE_PATH", "") -SSL_KEY_PATH = config("SSL_KEY_PATH", "") -DIGID_SERVICE_ENTITY_ID = config( - "DIGID_SERVICE_ENTITY_ID", "https://was-preprod1.digid.nl/saml/idp/metadata" -) -DIGID_WANT_ASSERTIONS_SIGNED = config("DIGID_WANT_ASSERTIONS_SIGNED", default=True) - -DIGID = { - "base_url": BASE_URL, - "entity_id": BASE_URL, - # This is the metadata of the **Identity provider** NOT our own! - "metadata_file": DIGID_METADATA, - # SSL/TLS key - "key_file": SSL_KEY_PATH, - "cert_file": SSL_CERTIFICATE_PATH, - "service_entity_id": DIGID_SERVICE_ENTITY_ID, - "attribute_consuming_service_index": "1", - "requested_attributes": ["bsn"], - # Logius can sign the assertions (True) but others sign the entire response - # (False). - "want_assertions_signed": DIGID_WANT_ASSERTIONS_SIGNED, -} +# +# EHERKENNING +# EHERKENNING_MOCK = config("EHERKENNING_MOCK", default=True) THUMBNAIL_ALIASES = { @@ -919,6 +892,11 @@ MAIL_EDITOR_DYNAMIC_CONTEXT, ) +if ALLOWED_HOSTS: + BASE_URL = "https://{}".format(ALLOWED_HOSTS[0]) +else: + BASE_URL = "https://example.com" + MAIL_EDITOR_BASE_HOST = BASE_URL CKEDITOR_CONFIGS = { diff --git a/src/open_inwoner/conf/production.py b/src/open_inwoner/conf/production.py index 6d26f5ed16..b91060292b 100644 --- a/src/open_inwoner/conf/production.py +++ b/src/open_inwoner/conf/production.py @@ -22,14 +22,10 @@ ] DIGID_MOCK = config("DIGID_MOCK", default=False) -if DIGID_METADATA and not DEBUG: - AUTHENTICATION_BACKENDS += ["digid_eherkenning.backends.DigiDBackend"] - DIGID_ENABLED = True -elif DIGID_MOCK: +if DIGID_MOCK: AUTHENTICATION_BACKENDS += ["digid_eherkenning.mock.backends.DigiDBackend"] - DIGID_ENABLED = True else: - DIGID_ENABLED = False + AUTHENTICATION_BACKENDS += ["digid_eherkenning.backends.DigiDBackend"] EHERKENNING_MOCK = config("EHERKENNING_MOCK", default=False) if EHERKENNING_MOCK: diff --git a/src/open_inwoner/configurations/bootstrap/auth.py b/src/open_inwoner/configurations/bootstrap/auth.py index 64ca134733..f2711a21ad 100644 --- a/src/open_inwoner/configurations/bootstrap/auth.py +++ b/src/open_inwoner/configurations/bootstrap/auth.py @@ -1,10 +1,19 @@ from django.conf import settings +from django.contrib.admin.sites import AdminSite from django.contrib.auth.models import Group +from django.core.exceptions import ValidationError +from django.test import RequestFactory +from digid_eherkenning.admin import ( + DigidConfigurationAdmin, + EherkenningConfigurationAdmin, +) +from digid_eherkenning.models import DigidConfiguration, EherkenningConfiguration from django_setup_configuration.configuration import BaseConfigurationStep from django_setup_configuration.exceptions import ConfigurationRunFailed from mozilla_django_oidc_db.forms import OpenIDConnectConfigForm from mozilla_django_oidc_db.models import OpenIDConnectConfig +from simple_certmanager.models import Certificate from digid_eherkenning_oidc_generics.admin import ( OpenIDConnectDigiDConfigForm, @@ -14,6 +23,7 @@ OpenIDConnectDigiDConfig, OpenIDConnectEHerkenningConfig, ) +from open_inwoner.configurations.models import SiteConfiguration class DigiDOIDCConfigurationStep(BaseConfigurationStep): @@ -241,3 +251,240 @@ def test_configuration(self): TODO not sure if it is feasible (because there are different possible IdPs), but it would be nice if we could test the login automatically """ + + +class DigiDConfigurationStep(BaseConfigurationStep): + """ + Configure DigiD via SAML + """ + + verbose_name = "Configuration for DigiD via SAML" + required_settings = [ + "DIGID_CERTIFICATE_LABEL", + "DIGID_CERTIFICATE_TYPE", + "DIGID_CERTIFICATE_PUBLIC_CERTIFICATE", + "DIGID_METADATA_FILE_SOURCE", + "DIGID_ENTITY_ID", + "DIGID_BASE_URL", + "DIGID_SERVICE_NAME", + "DIGID_SERVICE_DESCRIPTION", + ] + all_settings = required_settings + [ + "DIGID_CERTIFICATE_PRIVATE_KEY", + "DIGID_WANT_ASSERTIONS_SIGNED", + "DIGID_WANT_ASSERTIONS_ENCRYPTED", + "DIGID_ARTIFACT_RESOLVE_CONTENT_TYPE", + "DIGID_KEY_PASSPHRASE", + "DIGID_SIGNATURE_ALGORITHM", + "DIGID_DIGEST_ALGORITHM", + "DIGID_TECHNICAL_CONTACT_PERSON_TELEPHONE", + "DIGID_TECHNICAL_CONTACT_PERSON_EMAIL", + "DIGID_ORGANIZATION_URL", + "DIGID_ORGANIZATION_NAME", + "DIGID_ATTRIBUTE_CONSUMING_SERVICE_INDEX", + "DIGID_REQUESTED_ATTRIBUTES", + "DIGID_SLO", + ] + enable_setting = "DIGID_ENABLED" + + def is_configured(self) -> bool: + config = DigidConfiguration.get_solo() + return bool( + config.certificate + and config.metadata_file_source + and config.entity_id + and config.base_url + and config.service_name + and config.service_description + ) + + def configure(self): + config = DigidConfiguration.get_solo() + + # Use the model defaults + form_data = { + field.name: getattr(config, field.name) + for field in DigidConfiguration._meta.fields + } + + # Only override field values with settings if they are defined + for setting in self.all_settings: + value = getattr(settings, setting, None) + if value is not None: + model_field_name = setting.split("DIGID_")[1].lower() + if model_field_name.startswith("certificate"): + continue + + form_data[model_field_name] = value + + certificate, _ = Certificate.objects.get_or_create( + label=settings.DIGID_CERTIFICATE_LABEL, + defaults={ + "type": settings.DIGID_CERTIFICATE_TYPE, + }, + ) + + # Save the certificates separately, to ensure the resulting file is stored in + # private media + with open(settings.DIGID_CERTIFICATE_PUBLIC_CERTIFICATE) as public_cert: + certificate.public_certificate.save("digid.crt", public_cert) + + if settings.DIGID_CERTIFICATE_PRIVATE_KEY: + with open(settings.DIGID_CERTIFICATE_PRIVATE_KEY) as private_key: + certificate.private_key.save("digid.key", private_key) + + form_data["certificate"] = certificate + + request = RequestFactory().get("/") + digid_admin = DigidConfigurationAdmin(DigidConfiguration, AdminSite()) + form_class = digid_admin.get_form(request) + + form = form_class(data=form_data) + if not form.is_valid(): + raise ConfigurationRunFailed( + f"Something went wrong while saving configuration: {form.errors}" + ) + + try: + form.save() + except ValidationError as e: + raise ConfigurationRunFailed( + "Something went wrong while saving configuration" + ) from e + + def test_configuration(self): + """ + TODO + """ + + +class eHerkenningConfigurationStep(BaseConfigurationStep): + """ + Configure eHerkenning via SAML + """ + + verbose_name = "Configuration for eHerkenning via SAML" + required_settings = [ + "EHERKENNING_CERTIFICATE_LABEL", + "EHERKENNING_CERTIFICATE_TYPE", + "EHERKENNING_CERTIFICATE_PUBLIC_CERTIFICATE", + "EHERKENNING_METADATA_FILE_SOURCE", + "EHERKENNING_ENTITY_ID", + "EHERKENNING_BASE_URL", + "EHERKENNING_SERVICE_NAME", + "EHERKENNING_SERVICE_DESCRIPTION", + "EHERKENNING_OIN", + "EHERKENNING_MAKELAAR_ID", + "EHERKENNING_PRIVACY_POLICY", + ] + all_settings = required_settings + [ + "EHERKENNING_CERTIFICATE_PRIVATE_KEY", + "EHERKENNING_WANT_ASSERTIONS_SIGNED", + "EHERKENNING_WANT_ASSERTIONS_ENCRYPTED", + "EHERKENNING_ARTIFACT_RESOLVE_CONTENT_TYPE", + "EHERKENNING_KEY_PASSPHRASE", + "EHERKENNING_SIGNATURE_ALGORITHM", + "EHERKENNING_DIGEST_ALGORITHM", + "EHERKENNING_ENTITY_ID", + "EHERKENNING_BASE_URL", + "EHERKENNING_SERVICE_NAME", + "EHERKENNING_SERVICE_DESCRIPTION", + "EHERKENNING_TECHNICAL_CONTACT_PERSON_TELEPHONE", + "EHERKENNING_TECHNICAL_CONTACT_PERSON_EMAIL", + "EHERKENNING_ORGANIZATION_URL", + "EHERKENNING_ORGANIZATION_NAME", + "EHERKENNING_EH_LOA", + "EHERKENNING_EH_ATTRIBUTE_CONSUMING_SERVICE_INDEX", + "EHERKENNING_EH_REQUESTED_ATTRIBUTES", + "EHERKENNING_EH_SERVICE_UUID", + "EHERKENNING_EH_SERVICE_INSTANCE_UUID", + "EHERKENNING_EIDAS_LOA", + "EHERKENNING_EIDAS_ATTRIBUTE_CONSUMING_SERVICE_INDEX", + "EHERKENNING_EIDAS_REQUESTED_ATTRIBUTES", + "EHERKENNING_EIDAS_SERVICE_UUID", + "EHERKENNING_EIDAS_SERVICE_INSTANCE_UUID", + "EHERKENNING_NO_EIDAS", + "EHERKENNING_SERVICE_LANGUAGE", + ] + enable_setting = "EHERKENNING_ENABLE" + + def is_configured(self) -> bool: + config = EherkenningConfiguration.get_solo() + site_config = SiteConfiguration.get_solo() + return site_config.eherkenning_enabled and bool( + config.certificate + and config.metadata_file_source + and config.entity_id + and config.base_url + and config.service_name + and config.service_description + and config.oin + and config.makelaar_id + and config.privacy_policy + and config.service_language + ) + + def configure(self): + config = EherkenningConfiguration.get_solo() + + # Use the model defaults + form_data = { + field.name: getattr(config, field.name) + for field in EherkenningConfiguration._meta.fields + } + + # Only override field values with settings if they are defined + for setting in self.all_settings: + value = getattr(settings, setting, None) + if value is not None: + model_field_name = setting.split("EHERKENNING_")[1].lower() + if model_field_name.startswith("certificate"): + continue + + form_data[model_field_name] = value + + certificate, _ = Certificate.objects.get_or_create( + label=settings.EHERKENNING_CERTIFICATE_LABEL, + defaults={ + "type": settings.EHERKENNING_CERTIFICATE_TYPE, + }, + ) + + # Save the certificates separately, to ensure the resulting file is stored in + # private media + with open(settings.EHERKENNING_CERTIFICATE_PUBLIC_CERTIFICATE) as public_cert: + certificate.public_certificate.save("eherkenning.crt", public_cert) + + if settings.EHERKENNING_CERTIFICATE_PRIVATE_KEY: + with open(settings.EHERKENNING_CERTIFICATE_PRIVATE_KEY) as private_key: + certificate.private_key.save("eherkenning.key", private_key) + + form_data["certificate"] = certificate + + request = RequestFactory().get("/") + eherkenning_admin = EherkenningConfigurationAdmin( + EherkenningConfiguration, AdminSite() + ) + form_class = eherkenning_admin.get_form(request) + + form = form_class(data=form_data) + if not form.is_valid(): + raise ConfigurationRunFailed( + f"Something went wrong while saving configuration: {form.errors}" + ) + + try: + form.save() + except ValidationError as e: + raise ConfigurationRunFailed( + "Something went wrong while saving configuration" + ) from e + + site_config = SiteConfiguration.get_solo() + site_config.eherkenning_enabled = True + site_config.save() + + def test_configuration(self): + """ + TODO + """ From 8e963664d8b646f6ba7cabee8cc0d30e94c2ea71 Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Thu, 11 Apr 2024 15:14:15 +0200 Subject: [PATCH 6/9] :white_check_mark: [#2263] Add tests for setup config for DigiD/eHerkenning SAML https://taiga.maykinmedia.nl/project/open-inwoner/task/2263 --- .../tests/bootstrap/files/digid-metadata.xml | 82 ++++ .../tests/bootstrap/test_setup_auth_config.py | 368 ++++++++++++++++++ 2 files changed, 450 insertions(+) create mode 100644 src/open_inwoner/configurations/tests/bootstrap/files/digid-metadata.xml diff --git a/src/open_inwoner/configurations/tests/bootstrap/files/digid-metadata.xml b/src/open_inwoner/configurations/tests/bootstrap/files/digid-metadata.xml new file mode 100644 index 0000000000..728d152fde --- /dev/null +++ b/src/open_inwoner/configurations/tests/bootstrap/files/digid-metadata.xml @@ -0,0 +1,82 @@ + +NoOIwQlJT+i8sPfoZAqpxlGTh7hqWVGeVHcu9vhQ+xA=ixScWp/yrGs4LRQHqnF9Zr/Jn1MOIS5TwiWwiUc3d5sv+jMbVFGSw4fHE0Yu6yp5kOajK3wCY8TbfcIb++na5XWlHDfMD3MhiNBTdr2vIw6tdqetSCng02r5BQN1wug1qH1RY8FRH39X0opOVbs/V9HsCoquRvVRxjidz9L5Q3PNx/VPGHWkW4iclJKsJT4UPqTR6ZQww3Krd7XzUA3pnTx97WxJegfwmg70H/WQiasV1eI4tWm3PFHhhS2TuVshxoWxa2Qzz6HHYsOX+jWVnL9M3YF/RXuoMdt3cOtde7/EX6Cw2r50hAODnClQgRoxuPMBhdTXAyq6NirmPR9dKg==7593b799e735055fcd479caa35d44d455576cefc7593b799e735055fcd479caa35d44d455576cefcMIIG+zCCBOOgAwIBAgIUJmQio80TiqOX3LMrbzeG1dh0ngEwDQYJKoZIhvcN +AQELBQAwgYAxCzAJBgNVBAYTAk5MMSAwHgYDVQQKDBdRdW9WYWRpcyBUcnVz +dGxpbmsgQi5WLjEXMBUGA1UEYQwOTlRSTkwtMzAyMzc0NTkxNjA0BgNVBAMM +LVF1b1ZhZGlzIFBLSW92ZXJoZWlkIFByaXZhdGUgU2VydmljZXMgQ0EgLSBH +MTAeFw0yMzA5MjExOTEyNTlaFw0yNjA5MjExOTA3MDBaMF4xCzAJBgNVBAYT +Ak5MMQ8wDQYDVQQKDAZMb2dpdXMxHTAbBgNVBAUTFDAwMDAwMDA0MTY2OTA5 +OTEzMDAwMR8wHQYDVQQDDBZzYW1sLXNpZ24ucHAxLmRpZ2lkLm5sMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5r8GtcRfX3UN3f7I8Iobhy/N +U0Y+MV6DEXTmDRvRDZjwjr6ammB/cd1fsDz7D5CU+eax205aDRH69mMZk11x +cIKDjISvtBFLQVxwWXgTGpUSBogFHfp70/yfvcC7vq8gE7zjRN9gNzZCC0Ak +D7ZwAfFBbwI9nBSiMby9MY41MjS/W10uik1I6s5Ok2u/WfUe6FjcnaoU1O7O +tpJGAkW/yDC5HWypqeTG1fSPee/0GjvU8FH+Bu73fAFHa86KSO15eaCUR6Ea +7qCjpLsfPizweP9Adehlal1blfxsfJdFunq/jnO8NhYnQ7DC0aBd6ET8Wo/O +1ZacGsYmJWq9dqeleQIDAQABo4ICjDCCAogwHwYDVR0jBBgwFoAUuWymE7q7 +LzRjgzEu+X5JHd8A9WMwfQYIKwYBBQUHAQEEcTBvMD4GCCsGAQUFBzAChjJo +dHRwOi8vdHJ1c3QucXVvdmFkaXNnbG9iYWwuY29tL3BraW9wcml2c2Vydmcx +LmNydDAtBggrBgEFBQcwAYYhaHR0cDovL3NsLm9jc3AucXVvdmFkaXNnbG9i +YWwuY29tMCEGA1UdEQQaMBiCFnNhbWwtc2lnbi5wcDEuZGlnaWQubmwwggEw +BgNVHSAEggEnMIIBIzCCAR8GCmCEEAGHawECCAYwggEPMDQGCCsGAQUFBwIB +FihodHRwOi8vd3d3LnF1b3ZhZGlzZ2xvYmFsLmNvbS9yZXBvc2l0b3J5MIHW +BggrBgEFBQcCAjCByQyBxlJlbGlhbmNlIG9uIHRoaXMgY2VydGlmaWNhdGUg +YnkgYW55IHBhcnR5IGFzc3VtZXMgYWNjZXB0YW5jZSBvZiB0aGUgcmVsZXZh +bnQgUXVvVmFkaXMgQ2VydGlmaWNhdGlvbiBQcmFjdGljZSBTdGF0ZW1lbnQg +YW5kIG90aGVyIGRvY3VtZW50cyBpbiB0aGUgUXVvVmFkaXMgcmVwb3NpdG9y +eSAoaHR0cDovL3d3dy5xdW92YWRpc2dsb2JhbC5jb20pLjAdBgNVHSUEFjAU +BggrBgEFBQcDAgYIKwYBBQUHAwEwQQYDVR0fBDowODA2oDSgMoYwaHR0cDov +L2NybC5xdW92YWRpc2dsb2JhbC5jb20vcGtpb3ByaXZzZXJ2ZzEuY3JsMB0G +A1UdDgQWBBRZAjjqOD74qry7CXrnZUyjOI33CzAOBgNVHQ8BAf8EBAMCBaAw +DQYJKoZIhvcNAQELBQADggIBABiPLzZxV8YAFV0OIvK35Qh11zDZYPEp09RN +9NDsb+kOvShIBQirgejYA1SVZ73vMzFUmVNW/LyKoflH/N3ziU5NoMHK/31G +yP3W5Ffeezo42wLqqv1Ttfod3Tg56LC/jZb0a7R4LoosfFuEvHwWM8vJO8oy +IFuNSclSXOR0UArdeUl6fXsYExFOkdKgsrjcDBH+DOs/LlDPwL9qL3aK6vOG +WMXlfaFfRVhmcqs1ZVXLc+ylyT2DKf96oQSXE/pIB/yCcl3MuG3Xb0mp4MEq +AcAvEa4bIW0c1ULmlmxfw7F8rR2pVWN3wl8fqsxYxg6SNp3+ZKjOvX5GVGz+ +2nGWC+W2szqpRL/uvNWquSZaFHRiFkbJLtZNMy9HF7F0P62ler7BuZ4resb1 +l5d+rRRUocPwBv6GrBv6WE6QXpKkYZyuElkl7u3W+/L5UGaz+rAaMYJ1DdQL +XgAdq4KIuh9VR/YsFpttXUz2ieRBm1s2t0otk/sr0zFT23mt22lVVSaHmfCB +X8xCL9z8Y+XlbhPWhoXf8hvKI9KLcpf+e9OiS84+MYq4xJxoESNoq31oYirM +I1g9TNGKXAKrXIv9laeinsIJnn7zhSFu0LWz8XuvjuxPXtPzi9mOh98wIp6H ++AbyNMBwYTQKpOAd8jsD6+2d1gK5WHtwce5cWxgVdo1czCvY +7593b799e735055fcd479caa35d44d455576cefcMIIG+zCCBOOgAwIBAgIUJmQio80TiqOX3LMrbzeG1dh0ngEwDQYJKoZIhvcN +AQELBQAwgYAxCzAJBgNVBAYTAk5MMSAwHgYDVQQKDBdRdW9WYWRpcyBUcnVz +dGxpbmsgQi5WLjEXMBUGA1UEYQwOTlRSTkwtMzAyMzc0NTkxNjA0BgNVBAMM +LVF1b1ZhZGlzIFBLSW92ZXJoZWlkIFByaXZhdGUgU2VydmljZXMgQ0EgLSBH +MTAeFw0yMzA5MjExOTEyNTlaFw0yNjA5MjExOTA3MDBaMF4xCzAJBgNVBAYT +Ak5MMQ8wDQYDVQQKDAZMb2dpdXMxHTAbBgNVBAUTFDAwMDAwMDA0MTY2OTA5 +OTEzMDAwMR8wHQYDVQQDDBZzYW1sLXNpZ24ucHAxLmRpZ2lkLm5sMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5r8GtcRfX3UN3f7I8Iobhy/N +U0Y+MV6DEXTmDRvRDZjwjr6ammB/cd1fsDz7D5CU+eax205aDRH69mMZk11x +cIKDjISvtBFLQVxwWXgTGpUSBogFHfp70/yfvcC7vq8gE7zjRN9gNzZCC0Ak +D7ZwAfFBbwI9nBSiMby9MY41MjS/W10uik1I6s5Ok2u/WfUe6FjcnaoU1O7O +tpJGAkW/yDC5HWypqeTG1fSPee/0GjvU8FH+Bu73fAFHa86KSO15eaCUR6Ea +7qCjpLsfPizweP9Adehlal1blfxsfJdFunq/jnO8NhYnQ7DC0aBd6ET8Wo/O +1ZacGsYmJWq9dqeleQIDAQABo4ICjDCCAogwHwYDVR0jBBgwFoAUuWymE7q7 +LzRjgzEu+X5JHd8A9WMwfQYIKwYBBQUHAQEEcTBvMD4GCCsGAQUFBzAChjJo +dHRwOi8vdHJ1c3QucXVvdmFkaXNnbG9iYWwuY29tL3BraW9wcml2c2Vydmcx +LmNydDAtBggrBgEFBQcwAYYhaHR0cDovL3NsLm9jc3AucXVvdmFkaXNnbG9i +YWwuY29tMCEGA1UdEQQaMBiCFnNhbWwtc2lnbi5wcDEuZGlnaWQubmwwggEw +BgNVHSAEggEnMIIBIzCCAR8GCmCEEAGHawECCAYwggEPMDQGCCsGAQUFBwIB +FihodHRwOi8vd3d3LnF1b3ZhZGlzZ2xvYmFsLmNvbS9yZXBvc2l0b3J5MIHW +BggrBgEFBQcCAjCByQyBxlJlbGlhbmNlIG9uIHRoaXMgY2VydGlmaWNhdGUg +YnkgYW55IHBhcnR5IGFzc3VtZXMgYWNjZXB0YW5jZSBvZiB0aGUgcmVsZXZh +bnQgUXVvVmFkaXMgQ2VydGlmaWNhdGlvbiBQcmFjdGljZSBTdGF0ZW1lbnQg +YW5kIG90aGVyIGRvY3VtZW50cyBpbiB0aGUgUXVvVmFkaXMgcmVwb3NpdG9y +eSAoaHR0cDovL3d3dy5xdW92YWRpc2dsb2JhbC5jb20pLjAdBgNVHSUEFjAU +BggrBgEFBQcDAgYIKwYBBQUHAwEwQQYDVR0fBDowODA2oDSgMoYwaHR0cDov +L2NybC5xdW92YWRpc2dsb2JhbC5jb20vcGtpb3ByaXZzZXJ2ZzEuY3JsMB0G +A1UdDgQWBBRZAjjqOD74qry7CXrnZUyjOI33CzAOBgNVHQ8BAf8EBAMCBaAw +DQYJKoZIhvcNAQELBQADggIBABiPLzZxV8YAFV0OIvK35Qh11zDZYPEp09RN +9NDsb+kOvShIBQirgejYA1SVZ73vMzFUmVNW/LyKoflH/N3ziU5NoMHK/31G +yP3W5Ffeezo42wLqqv1Ttfod3Tg56LC/jZb0a7R4LoosfFuEvHwWM8vJO8oy +IFuNSclSXOR0UArdeUl6fXsYExFOkdKgsrjcDBH+DOs/LlDPwL9qL3aK6vOG +WMXlfaFfRVhmcqs1ZVXLc+ylyT2DKf96oQSXE/pIB/yCcl3MuG3Xb0mp4MEq +AcAvEa4bIW0c1ULmlmxfw7F8rR2pVWN3wl8fqsxYxg6SNp3+ZKjOvX5GVGz+ +2nGWC+W2szqpRL/uvNWquSZaFHRiFkbJLtZNMy9HF7F0P62ler7BuZ4resb1 +l5d+rRRUocPwBv6GrBv6WE6QXpKkYZyuElkl7u3W+/L5UGaz+rAaMYJ1DdQL +XgAdq4KIuh9VR/YsFpttXUz2ieRBm1s2t0otk/sr0zFT23mt22lVVSaHmfCB +X8xCL9z8Y+XlbhPWhoXf8hvKI9KLcpf+e9OiS84+MYq4xJxoESNoq31oYirM +I1g9TNGKXAKrXIv9laeinsIJnn7zhSFu0LWz8XuvjuxPXtPzi9mOh98wIp6H ++AbyNMBwYTQKpOAd8jsD6+2d1gK5WHtwce5cWxgVdo1czCvY + diff --git a/src/open_inwoner/configurations/tests/bootstrap/test_setup_auth_config.py b/src/open_inwoner/configurations/tests/bootstrap/test_setup_auth_config.py index d748ff77f2..14f2938864 100644 --- a/src/open_inwoner/configurations/tests/bootstrap/test_setup_auth_config.py +++ b/src/open_inwoner/configurations/tests/bootstrap/test_setup_auth_config.py @@ -1,14 +1,28 @@ +import tempfile +import urllib.error from unittest import skip +from unittest.mock import patch +from uuid import UUID +from django.conf import settings from django.test import TestCase, override_settings import requests import requests_mock +from digid_eherkenning.choices import ( + AssuranceLevels, + DigestAlgorithms, + SignatureAlgorithms, + XMLContentTypes, +) +from digid_eherkenning.models import DigidConfiguration, EherkenningConfiguration from django_setup_configuration.exceptions import ConfigurationRunFailed from mozilla_django_oidc_db.models import ( OpenIDConnectConfig, UserInformationClaimsSources, ) +from privates.test import temp_private_root +from simple_certmanager.constants import CertificateTypes from digid_eherkenning_oidc_generics.models import ( OpenIDConnectDigiDConfig, @@ -18,7 +32,9 @@ from ...bootstrap.auth import ( AdminOIDCConfigurationStep, + DigiDConfigurationStep, DigiDOIDCConfigurationStep, + eHerkenningConfigurationStep, eHerkenningOIDCConfigurationStep, ) @@ -34,6 +50,19 @@ "jwks_uri": f"{IDENTITY_PROVIDER}protocol/openid-connect/certs", } +DIGID_XML_METADATA_PATH = ( + "src/open_inwoner/configurations/tests/bootstrap/files/digid-metadata.xml" +) + +PUBLIC_CERT_FILE = tempfile.NamedTemporaryFile() +PRIVATE_KEY_FILE = tempfile.NamedTemporaryFile() + +with open(PUBLIC_CERT_FILE.name, "w") as f: + f.write("cert") + +with open(PRIVATE_KEY_FILE.name, "w") as f: + f.write("key") + @override_settings( DIGID_OIDC_OIDC_RP_CLIENT_ID="client-id", @@ -657,3 +686,342 @@ def test_is_configured(self): config.configure() self.assertTrue(config.is_configured()) + + +@temp_private_root() +@override_settings( + DIGID_CERTIFICATE_LABEL="DigiD certificate", + DIGID_CERTIFICATE_TYPE=CertificateTypes.key_pair, + DIGID_CERTIFICATE_PUBLIC_CERTIFICATE=PUBLIC_CERT_FILE.name, + DIGID_CERTIFICATE_PRIVATE_KEY=PRIVATE_KEY_FILE.name, + DIGID_METADATA_FILE_SOURCE="http://metadata.local/file.xml", + DIGID_ENTITY_ID="1234", + DIGID_BASE_URL="http://digid.local", + DIGID_SERVICE_NAME="OIP", + DIGID_SERVICE_DESCRIPTION="Open Inwoner", + DIGID_WANT_ASSERTIONS_SIGNED=False, + DIGID_WANT_ASSERTIONS_ENCRYPTED=True, + DIGID_ARTIFACT_RESOLVE_CONTENT_TYPE=XMLContentTypes.text_xml, + DIGID_KEY_PASSPHRASE="foo", + DIGID_SIGNATURE_ALGORITHM=SignatureAlgorithms.dsa_sha1, + DIGID_DIGEST_ALGORITHM=DigestAlgorithms.sha512, + DIGID_TECHNICAL_CONTACT_PERSON_TELEPHONE="0612345678", + DIGID_TECHNICAL_CONTACT_PERSON_EMAIL="foo@bar.org", + DIGID_ORGANIZATION_URL="http://open-inwoner.local", + DIGID_ORGANIZATION_NAME="Open Inwoner", + DIGID_ATTRIBUTE_CONSUMING_SERVICE_INDEX="2", + DIGID_REQUESTED_ATTRIBUTES=[ + {"name": "bsn", "required": True}, + {"name": "email", "required": False}, + ], + DIGID_SLO=False, +) +class DigiDConfigurationTests(ClearCachesMixin, TestCase): + def test_configure(self): + with open(DIGID_XML_METADATA_PATH) as f: + with patch( + "onelogin.saml2.idp_metadata_parser.urllib2.urlopen", return_value=f + ): + DigiDConfigurationStep().configure() + + config = DigidConfiguration.get_solo() + + self.assertEqual(config.certificate.label, "DigiD certificate") + self.assertEqual(config.certificate.type, CertificateTypes.key_pair) + + public_cert = config.certificate.public_certificate + private_key = config.certificate.private_key + + self.assertTrue(public_cert.path.startswith(settings.PRIVATE_MEDIA_ROOT)) + self.assertEqual(public_cert.file.read(), b"cert") + self.assertTrue(private_key.path.startswith(settings.PRIVATE_MEDIA_ROOT)) + self.assertEqual(private_key.file.read(), b"key") + + self.assertEqual(config.key_passphrase, "foo") + self.assertEqual(config.metadata_file_source, "http://metadata.local/file.xml") + self.assertEqual( + config.idp_service_entity_id, + "https://was-preprod1.digid.nl/saml/idp/metadata", + ) + self.assertTrue(config.idp_metadata_file.path.endswith(".xml")) + self.assertEqual(config.entity_id, "1234") + self.assertEqual(config.base_url, "http://digid.local") + self.assertEqual(config.service_name, "OIP") + self.assertEqual(config.service_description, "Open Inwoner") + self.assertEqual(config.want_assertions_signed, False) + self.assertEqual(config.want_assertions_encrypted, True) + self.assertEqual(config.artifact_resolve_content_type, XMLContentTypes.text_xml) + self.assertEqual(config.signature_algorithm, SignatureAlgorithms.dsa_sha1) + self.assertEqual(config.digest_algorithm, DigestAlgorithms.sha512) + self.assertEqual(config.technical_contact_person_telephone, "0612345678") + self.assertEqual(config.technical_contact_person_email, "foo@bar.org") + self.assertEqual(config.organization_url, "http://open-inwoner.local") + self.assertEqual(config.organization_name, "Open Inwoner") + self.assertEqual(config.attribute_consuming_service_index, "2") + self.assertEqual( + config.requested_attributes, + [{"name": "bsn", "required": True}, {"name": "email", "required": False}], + ) + self.assertEqual(config.slo, False) + + @override_settings( + DIGID_WANT_ASSERTIONS_SIGNED=None, + DIGID_WANT_ASSERTIONS_ENCRYPTED=None, + DIGID_ARTIFACT_RESOLVE_CONTENT_TYPE=None, + DIGID_KEY_PASSPHRASE=None, + DIGID_SIGNATURE_ALGORITHM=None, + DIGID_DIGEST_ALGORITHM=None, + DIGID_ATTRIBUTE_CONSUMING_SERVICE_INDEX=None, + DIGID_REQUESTED_ATTRIBUTES=None, + DIGID_SLO=None, + ) + def test_configure_use_defaults(self): + with open(DIGID_XML_METADATA_PATH) as f: + with patch( + "onelogin.saml2.idp_metadata_parser.urllib2.urlopen", return_value=f + ): + DigiDConfigurationStep().configure() + + config = DigidConfiguration.get_solo() + + self.assertEqual(config.key_passphrase, "") + self.assertEqual(config.want_assertions_signed, True) + self.assertEqual(config.want_assertions_encrypted, False) + self.assertEqual(config.artifact_resolve_content_type, XMLContentTypes.soap_xml) + self.assertEqual(config.signature_algorithm, SignatureAlgorithms.rsa_sha1) + self.assertEqual(config.digest_algorithm, DigestAlgorithms.sha1) + self.assertEqual(config.attribute_consuming_service_index, "1") + self.assertEqual( + config.requested_attributes, + [{"name": "bsn", "required": True}], + ) + self.assertEqual(config.slo, True) + + def test_configure_failure(self): + exceptions = (urllib.error.HTTPError, urllib.error.URLError) + for exception in exceptions: + with self.subTest(exception=exception): + with patch( + "onelogin.saml2.idp_metadata_parser.urllib2.urlopen", + side_effect=exception, + ): + with self.assertRaises(ConfigurationRunFailed): + DigiDConfigurationStep().configure() + + config = DigidConfiguration.get_solo() + + self.assertFalse(config.certificate, None) + + @skip("Testing config for DigiD OIDC is not implemented yet") + @requests_mock.Mocker() + def test_configuration_check_ok(self, m): + raise NotImplementedError + + @skip("Testing config for DigiD OIDC is not implemented yet") + @requests_mock.Mocker() + def test_configuration_check_failures(self, m): + raise NotImplementedError + + def test_is_configured(self): + config = DigiDConfigurationStep() + + self.assertFalse(config.is_configured()) + + with open(DIGID_XML_METADATA_PATH) as f: + with patch( + "onelogin.saml2.idp_metadata_parser.urllib2.urlopen", return_value=f + ): + config.configure() + + self.assertTrue(config.is_configured()) + + +@temp_private_root() +@override_settings( + EHERKENNING_CERTIFICATE_LABEL="eHerkenning certificate", + EHERKENNING_CERTIFICATE_TYPE=CertificateTypes.key_pair, + EHERKENNING_CERTIFICATE_PUBLIC_CERTIFICATE=PUBLIC_CERT_FILE.name, + EHERKENNING_CERTIFICATE_PRIVATE_KEY=PRIVATE_KEY_FILE.name, + EHERKENNING_METADATA_FILE_SOURCE="http://metadata.local/file.xml", + EHERKENNING_ENTITY_ID="1234", + EHERKENNING_BASE_URL="http://eherkenning.local", + EHERKENNING_SERVICE_NAME="OIP", + EHERKENNING_SERVICE_DESCRIPTION="Open Inwoner", + EHERKENNING_WANT_ASSERTIONS_SIGNED=False, + EHERKENNING_WANT_ASSERTIONS_ENCRYPTED=True, + EHERKENNING_ARTIFACT_RESOLVE_CONTENT_TYPE=XMLContentTypes.text_xml, + EHERKENNING_KEY_PASSPHRASE="foo", + EHERKENNING_SIGNATURE_ALGORITHM=SignatureAlgorithms.dsa_sha1, + EHERKENNING_DIGEST_ALGORITHM=DigestAlgorithms.sha512, + EHERKENNING_TECHNICAL_CONTACT_PERSON_TELEPHONE="0612345678", + EHERKENNING_TECHNICAL_CONTACT_PERSON_EMAIL="foo@bar.org", + EHERKENNING_ORGANIZATION_URL="http://open-inwoner.local", + EHERKENNING_ORGANIZATION_NAME="Open Inwoner", + EHERKENNING_EH_LOA=AssuranceLevels.high, + EHERKENNING_EH_ATTRIBUTE_CONSUMING_SERVICE_INDEX="9053", + EHERKENNING_EH_REQUESTED_ATTRIBUTES=[{"name": "kvk", "required": True}], + EHERKENNING_EH_SERVICE_UUID="a89ca0cc-e0db-417a-993e-1a54300a3537", + EHERKENNING_EH_SERVICE_INSTANCE_UUID="feed1712-4d97-4aaf-92e1-607ebd65263d", + EHERKENNING_EIDAS_LOA=AssuranceLevels.high, + EHERKENNING_EIDAS_ATTRIBUTE_CONSUMING_SERVICE_INDEX="9054", + EHERKENNING_EIDAS_REQUESTED_ATTRIBUTES=[{"name": "kvk", "required": True}], + EHERKENNING_EIDAS_SERVICE_UUID="59d0bfe8-10e6-4830-bc2b-c7d895a16f31", + EHERKENNING_EIDAS_SERVICE_INSTANCE_UUID="c1cd3bfa-cd5e-4f68-8991-7a87c137f8f0", + EHERKENNING_OIN="11111222223333344444", + EHERKENNING_NO_EIDAS=True, + EHERKENNING_PRIVACY_POLICY="http://privacy-policy.local/", + EHERKENNING_MAKELAAR_ID="44444333332222211111", + EHERKENNING_SERVICE_LANGUAGE="en", +) +class eHerkenningConfigurationTests(ClearCachesMixin, TestCase): + def test_configure(self): + with open(DIGID_XML_METADATA_PATH) as f: + with patch( + "onelogin.saml2.idp_metadata_parser.urllib2.urlopen", return_value=f + ): + eHerkenningConfigurationStep().configure() + + config = EherkenningConfiguration.get_solo() + + self.assertEqual(config.certificate.label, "eHerkenning certificate") + self.assertEqual(config.certificate.type, CertificateTypes.key_pair) + + public_cert = config.certificate.public_certificate + private_key = config.certificate.private_key + + self.assertTrue(public_cert.path.startswith(settings.PRIVATE_MEDIA_ROOT)) + self.assertEqual(public_cert.file.read(), b"cert") + self.assertTrue(private_key.path.startswith(settings.PRIVATE_MEDIA_ROOT)) + self.assertEqual(private_key.file.read(), b"key") + + self.assertEqual(config.key_passphrase, "foo") + self.assertEqual(config.metadata_file_source, "http://metadata.local/file.xml") + self.assertEqual( + config.idp_service_entity_id, + "https://was-preprod1.digid.nl/saml/idp/metadata", + ) + self.assertTrue(config.idp_metadata_file.path.endswith(".xml")) + self.assertEqual(config.entity_id, "1234") + self.assertEqual(config.base_url, "http://eherkenning.local") + self.assertEqual(config.service_name, "OIP") + self.assertEqual(config.service_description, "Open Inwoner") + self.assertEqual(config.want_assertions_signed, False) + self.assertEqual(config.want_assertions_encrypted, True) + self.assertEqual(config.artifact_resolve_content_type, XMLContentTypes.text_xml) + self.assertEqual(config.signature_algorithm, SignatureAlgorithms.dsa_sha1) + self.assertEqual(config.digest_algorithm, DigestAlgorithms.sha512) + self.assertEqual(config.technical_contact_person_telephone, "0612345678") + self.assertEqual(config.technical_contact_person_email, "foo@bar.org") + self.assertEqual(config.organization_url, "http://open-inwoner.local") + self.assertEqual(config.organization_name, "Open Inwoner") + self.assertEqual(config.eh_loa, AssuranceLevels.high) + self.assertEqual(config.eh_attribute_consuming_service_index, "9053") + self.assertEqual( + config.eh_requested_attributes, [{"name": "kvk", "required": True}] + ) + self.assertEqual( + config.eh_service_uuid, UUID("a89ca0cc-e0db-417a-993e-1a54300a3537") + ) + self.assertEqual( + config.eh_service_instance_uuid, + UUID("feed1712-4d97-4aaf-92e1-607ebd65263d"), + ) + self.assertEqual(config.eidas_loa, AssuranceLevels.high) + self.assertEqual(config.eidas_attribute_consuming_service_index, "9054") + self.assertEqual( + config.eidas_requested_attributes, [{"name": "kvk", "required": True}] + ) + self.assertEqual( + config.eidas_service_uuid, UUID("59d0bfe8-10e6-4830-bc2b-c7d895a16f31") + ) + self.assertEqual( + config.eidas_service_instance_uuid, + UUID("c1cd3bfa-cd5e-4f68-8991-7a87c137f8f0"), + ) + self.assertEqual(config.oin, "11111222223333344444") + self.assertEqual(config.no_eidas, True) + self.assertEqual(config.privacy_policy, "http://privacy-policy.local/") + self.assertEqual(config.makelaar_id, "44444333332222211111") + self.assertEqual(config.service_language, "en") + + @override_settings( + EHERKENNING_WANT_ASSERTIONS_SIGNED=None, + EHERKENNING_WANT_ASSERTIONS_ENCRYPTED=None, + EHERKENNING_ARTIFACT_RESOLVE_CONTENT_TYPE=None, + EHERKENNING_KEY_PASSPHRASE=None, + EHERKENNING_SIGNATURE_ALGORITHM=None, + EHERKENNING_DIGEST_ALGORITHM=None, + EHERKENNING_EH_LOA=None, + EHERKENNING_EH_ATTRIBUTE_CONSUMING_SERVICE_INDEX=None, + EHERKENNING_EH_SERVICE_UUID=None, + EHERKENNING_EH_SERVICE_INSTANCE_UUID=None, + EHERKENNING_EIDAS_LOA=None, + EHERKENNING_EIDAS_ATTRIBUTE_CONSUMING_SERVICE_INDEX=None, + EHERKENNING_EIDAS_SERVICE_UUID=None, + EHERKENNING_EIDAS_SERVICE_INSTANCE_UUID=None, + EHERKENNING_NO_EIDAS=None, + EHERKENNING_SERVICE_LANGUAGE=None, + ) + def test_configure_use_defaults(self): + with open(DIGID_XML_METADATA_PATH) as f: + with patch( + "onelogin.saml2.idp_metadata_parser.urllib2.urlopen", return_value=f + ): + eHerkenningConfigurationStep().configure() + + config = EherkenningConfiguration.get_solo() + + self.assertEqual(config.want_assertions_signed, True) + self.assertEqual(config.want_assertions_encrypted, False) + self.assertEqual(config.artifact_resolve_content_type, XMLContentTypes.soap_xml) + self.assertEqual(config.key_passphrase, "") + self.assertEqual(config.signature_algorithm, SignatureAlgorithms.rsa_sha1) + self.assertEqual(config.digest_algorithm, DigestAlgorithms.sha1) + self.assertEqual(config.eh_loa, AssuranceLevels.substantial) + self.assertEqual(config.eh_attribute_consuming_service_index, "9052") + self.assertIsInstance(config.eh_service_uuid, UUID) + self.assertIsInstance(config.eh_service_instance_uuid, UUID) + self.assertEqual(config.eidas_loa, AssuranceLevels.substantial) + self.assertEqual(config.eidas_attribute_consuming_service_index, "9053") + self.assertIsInstance(config.eidas_service_uuid, UUID) + self.assertIsInstance(config.eidas_service_instance_uuid, UUID) + self.assertEqual(config.no_eidas, False) + self.assertEqual(config.service_language, "nl") + + def test_configure_failure(self): + exceptions = (urllib.error.HTTPError, urllib.error.URLError) + for exception in exceptions: + with self.subTest(exception=exception): + with patch( + "onelogin.saml2.idp_metadata_parser.urllib2.urlopen", + side_effect=exception, + ): + with self.assertRaises(ConfigurationRunFailed): + eHerkenningConfigurationStep().configure() + + config = EherkenningConfiguration.get_solo() + + self.assertFalse(config.certificate, None) + + @skip("Testing config for DigiD OIDC is not implemented yet") + @requests_mock.Mocker() + def test_configuration_check_ok(self, m): + raise NotImplementedError + + @skip("Testing config for DigiD OIDC is not implemented yet") + @requests_mock.Mocker() + def test_configuration_check_failures(self, m): + raise NotImplementedError + + def test_is_configured(self): + config = eHerkenningConfigurationStep() + + self.assertFalse(config.is_configured()) + + with open(DIGID_XML_METADATA_PATH) as f: + with patch( + "onelogin.saml2.idp_metadata_parser.urllib2.urlopen", return_value=f + ): + config.configure() + + self.assertTrue(config.is_configured()) From 2f1547ed23a73aaa8a73487d0a088b625a8fbe1b Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Thu, 18 Apr 2024 10:00:09 +0200 Subject: [PATCH 7/9] :ok_hand: [#2263] PR feedback * add missing model fields to DigiD/eHerkenning OIDC admin form * split up envvar into DIGID_ENABLED and DIGID_CONFIG_ENABLE * use DIGID_ENABLED for both SAML and OIDC versions --- src/digid_eherkenning_oidc_generics/admin.py | 14 ++++++ .../templates/registration/login.html | 44 ++++++++++--------- .../conf/app/setup_configuration.py | 19 +++++--- src/open_inwoner/conf/base.py | 1 + src/open_inwoner/conf/production.py | 9 ++-- .../configurations/bootstrap/auth.py | 34 +++++++------- .../tests/bootstrap/test_setup_auth_config.py | 32 ++++++++++++++ 7 files changed, 103 insertions(+), 50 deletions(-) diff --git a/src/digid_eherkenning_oidc_generics/admin.py b/src/digid_eherkenning_oidc_generics/admin.py index cfb5601682..b7bc796ae9 100644 --- a/src/digid_eherkenning_oidc_generics/admin.py +++ b/src/digid_eherkenning_oidc_generics/admin.py @@ -42,6 +42,20 @@ class OpenIDConnectConfigBaseAdmin(SingletonModelAdmin): }, ), (_("Keycloak specific settings"), {"fields": ("oidc_keycloak_idp_hint",)}), + ( + _("Advanced settings"), + { + "fields": ( + "oidc_use_nonce", + "oidc_nonce_size", + "oidc_state_size", + "oidc_exempt_urls", + ), + "classes": [ + "collapse in", + ], + }, + ), ) diff --git a/src/open_inwoner/accounts/templates/registration/login.html b/src/open_inwoner/accounts/templates/registration/login.html index faf2d4c6b4..5a11e3abcd 100644 --- a/src/open_inwoner/accounts/templates/registration/login.html +++ b/src/open_inwoner/accounts/templates/registration/login.html @@ -15,28 +15,30 @@

{% trans 'Welkom' %}

{% if login_text %}
{{ login_text|markdown|safe }}
{% endif %}
- {% get_solo 'digid_eherkenning_oidc_generics.OpenIDConnectDigiDConfig' as digid_oidc_config %} - {% if digid_oidc_config.enabled %} - {% render_card direction='horizontal' tinted=True compact=True %} - - {% url 'digid_oidc:init' as href %} - {% with href|addnexturl:next as href_with_next %} - {% link href=href_with_next text=_('Inloggen met DigiD') secondary=True icon='arrow_forward' extra_classes="link--digid" %} - {% endwith %} + {% if settings.DIGID_ENABLED %} + {% get_solo 'digid_eherkenning_oidc_generics.OpenIDConnectDigiDConfig' as digid_oidc_config %} + {% if digid_oidc_config.enabled %} + {% render_card direction='horizontal' tinted=True compact=True %} + + {% url 'digid_oidc:init' as href %} + {% with href|addnexturl:next as href_with_next %} + {% link href=href_with_next text=_('Inloggen met DigiD') secondary=True icon='arrow_forward' extra_classes="link--digid" %} + {% endwith %} + {% endrender_card %} + {% else %} + {% render_card direction='horizontal' tinted=True compact=True %} + + {% url 'digid:login' as href %} + {% with href|addnexturl:next as href_with_next %} + {% link href=href_with_next text=_('Inloggen met DigiD') secondary=True icon='arrow_forward' extra_classes="link--digid" %} + {% endwith %} {% endrender_card %} - {% elif settings.DIGID_ENABLED %} - {% render_card direction='horizontal' tinted=True compact=True %} - - {% url 'digid:login' as href %} - {% with href|addnexturl:next as href_with_next %} - {% link href=href_with_next text=_('Inloggen met DigiD') secondary=True icon='arrow_forward' extra_classes="link--digid" %} - {% endwith %} - {% endrender_card %} - {% endif %} + {% endif %} + {% endif %} {% if eherkenning_enabled %} {% get_solo 'digid_eherkenning_oidc_generics.OpenIDConnectEHerkenningConfig' as eherkenning_oidc_config %} diff --git a/src/open_inwoner/conf/app/setup_configuration.py b/src/open_inwoner/conf/app/setup_configuration.py index 2245acf158..3785a90227 100644 --- a/src/open_inwoner/conf/app/setup_configuration.py +++ b/src/open_inwoner/conf/app/setup_configuration.py @@ -192,7 +192,7 @@ # Authentication configuration variables # NOTE variables are namespaced with `DIGID_OIDC`, but some model field names also have `oidc_...` in them -DIGID_OIDC_ENABLE = config("DIGID_OIDC_ENABLE", True) +DIGID_OIDC_CONFIG_ENABLE = config("DIGID_OIDC_CONFIG_ENABLE", True) DIGID_OIDC_IDENTIFIER_CLAIM_NAME = config("DIGID_OIDC_IDENTIFIER_CLAIM_NAME", None) DIGID_OIDC_OIDC_RP_CLIENT_ID = config("DIGID_OIDC_OIDC_RP_CLIENT_ID", None) DIGID_OIDC_OIDC_RP_CLIENT_SECRET = config("DIGID_OIDC_OIDC_RP_CLIENT_SECRET", None) @@ -212,9 +212,13 @@ DIGID_OIDC_OIDC_OP_LOGOUT_ENDPOINT = config("DIGID_OIDC_OIDC_OP_LOGOUT_ENDPOINT", None) DIGID_OIDC_ERROR_MESSAGE_MAPPING = config("DIGID_OIDC_ERROR_MESSAGE_MAPPING", None) DIGID_OIDC_OIDC_KEYCLOAK_IDP_HINT = config("DIGID_OIDC_OIDC_KEYCLOAK_IDP_HINT", None) +DIGID_OIDC_OIDC_USE_NONCE = config("DIGID_OIDC_OIDC_USE_NONCE", None) +DIGID_OIDC_OIDC_NONCE_SIZE = config("DIGID_OIDC_OIDC_NONCE_SIZE", None) +DIGID_OIDC_OIDC_STATE_SIZE = config("DIGID_OIDC_OIDC_STATE_SIZE", None) +DIGID_OIDC_OIDC_EXEMPT_URLS = config("DIGID_OIDC_OIDC_EXEMPT_URLS", None) # NOTE variables are namespaced with `EHERKENNING_OIDC`, but some model field names also have `oidc_...` in them -EHERKENNING_OIDC_ENABLE = config("EHERKENNING_OIDC_ENABLE", True) +EHERKENNING_OIDC_CONFIG_ENABLE = config("EHERKENNING_OIDC_CONFIG_ENABLE", True) EHERKENNING_OIDC_IDENTIFIER_CLAIM_NAME = config( "EHERKENNING_OIDC_IDENTIFIER_CLAIM_NAME", None ) @@ -256,9 +260,13 @@ EHERKENNING_OIDC_OIDC_KEYCLOAK_IDP_HINT = config( "EHERKENNING_OIDC_OIDC_KEYCLOAK_IDP_HINT", None ) +EHERKENNING_OIDC_OIDC_USE_NONCE = config("EHERKENNING_OIDC_OIDC_USE_NONCE", None) +EHERKENNING_OIDC_OIDC_NONCE_SIZE = config("EHERKENNING_OIDC_OIDC_NONCE_SIZE", None) +EHERKENNING_OIDC_OIDC_STATE_SIZE = config("EHERKENNING_OIDC_OIDC_STATE_SIZE", None) +EHERKENNING_OIDC_OIDC_EXEMPT_URLS = config("EHERKENNING_OIDC_OIDC_EXEMPT_URLS", None) # NOTE variables are namespaced with `ADMIN_OIDC`, but some model field names also have `oidc_...` in them -ADMIN_OIDC_ENABLE = config("ADMIN_OIDC_ENABLE", default=True) +ADMIN_OIDC_CONFIG_ENABLE = config("ADMIN_OIDC_CONFIG_ENABLE", default=True) ADMIN_OIDC_OIDC_RP_CLIENT_ID = config("ADMIN_OIDC_OIDC_RP_CLIENT_ID", None) ADMIN_OIDC_OIDC_RP_CLIENT_SECRET = config("ADMIN_OIDC_OIDC_RP_CLIENT_SECRET", None) ADMIN_OIDC_OIDC_RP_SCOPES_LIST = config("ADMIN_OIDC_OIDC_RP_SCOPES_LIST", None) @@ -289,10 +297,7 @@ ADMIN_OIDC_OIDC_EXEMPT_URLS = config("ADMIN_OIDC_OIDC_EXEMPT_URLS", None) ADMIN_OIDC_USERINFO_CLAIMS_SOURCE = config("ADMIN_OIDC_USERINFO_CLAIMS_SOURCE", None) -# TODO this setting is currently also used to determine whether or not to display -# the DigiD login link in the template, that should probably be replace by a flag on the -# SiteConfiguration -DIGID_ENABLED = config("DIGID_ENABLED", default=True) +DIGID_CONFIG_ENABLE = config("DIGID_CONFIG_ENABLE", default=True) DIGID_CERTIFICATE_LABEL = config("DIGID_CERTIFICATE_LABEL", None) DIGID_CERTIFICATE_TYPE = config("DIGID_CERTIFICATE_TYPE", None) DIGID_CERTIFICATE_PUBLIC_CERTIFICATE = config( diff --git a/src/open_inwoner/conf/base.py b/src/open_inwoner/conf/base.py index 70fc4e937a..53b9ae96f5 100644 --- a/src/open_inwoner/conf/base.py +++ b/src/open_inwoner/conf/base.py @@ -836,6 +836,7 @@ # # DIGID # +DIGID_ENABLED = config("DIGID_ENABLED", default=True) DIGID_MOCK = config("DIGID_MOCK", default=True) # diff --git a/src/open_inwoner/conf/production.py b/src/open_inwoner/conf/production.py index b91060292b..fb9d69bb5d 100644 --- a/src/open_inwoner/conf/production.py +++ b/src/open_inwoner/conf/production.py @@ -22,10 +22,11 @@ ] DIGID_MOCK = config("DIGID_MOCK", default=False) -if DIGID_MOCK: - AUTHENTICATION_BACKENDS += ["digid_eherkenning.mock.backends.DigiDBackend"] -else: - AUTHENTICATION_BACKENDS += ["digid_eherkenning.backends.DigiDBackend"] +if DIGID_ENABLED: + if DIGID_MOCK: + AUTHENTICATION_BACKENDS += ["digid_eherkenning.mock.backends.DigiDBackend"] + else: + AUTHENTICATION_BACKENDS += ["digid_eherkenning.backends.DigiDBackend"] EHERKENNING_MOCK = config("EHERKENNING_MOCK", default=False) if EHERKENNING_MOCK: diff --git a/src/open_inwoner/configurations/bootstrap/auth.py b/src/open_inwoner/configurations/bootstrap/auth.py index f2711a21ad..375ff9bbf3 100644 --- a/src/open_inwoner/configurations/bootstrap/auth.py +++ b/src/open_inwoner/configurations/bootstrap/auth.py @@ -35,11 +35,6 @@ class DigiDOIDCConfigurationStep(BaseConfigurationStep): required_settings = [ "DIGID_OIDC_OIDC_RP_CLIENT_ID", "DIGID_OIDC_OIDC_RP_CLIENT_SECRET", - # NOTE these are part of the model, but not actually part of the admin form - # "DIGID_OIDC_OIDC_USE_NONCE", - # "DIGID_OIDC_OIDC_NONCE_SIZE", - # "DIGID_OIDC_OIDC_STATE_SIZE", - # "DIGID_OIDC_OIDC_EXEMPT_URLS", ] all_settings = required_settings + [ "DIGID_OIDC_IDENTIFIER_CLAIM_NAME", @@ -55,8 +50,12 @@ class DigiDOIDCConfigurationStep(BaseConfigurationStep): "DIGID_OIDC_USERINFO_CLAIMS_SOURCE", "DIGID_OIDC_ERROR_MESSAGE_MAPPING", "DIGID_OIDC_OIDC_KEYCLOAK_IDP_HINT", + "DIGID_OIDC_OIDC_USE_NONCE", + "DIGID_OIDC_OIDC_NONCE_SIZE", + "DIGID_OIDC_OIDC_STATE_SIZE", + "DIGID_OIDC_OIDC_EXEMPT_URLS", ] - enable_setting = "DIGID_OIDC_ENABLE" + enable_setting = "DIGID_OIDC_CONFIG_ENABLE" def is_configured(self) -> bool: return OpenIDConnectDigiDConfig.get_solo().enabled @@ -83,7 +82,7 @@ def configure(self): if not form_data["error_message_mapping"]: del form_data["error_message_mapping"] - # Use the admin for to apply validation and fetch URLs from the discovery endpoint + # Use the admin form to apply validation and fetch URLs from the discovery endpoint form = OpenIDConnectDigiDConfigForm(data=form_data) if not form.is_valid(): raise ConfigurationRunFailed( @@ -108,11 +107,6 @@ class eHerkenningOIDCConfigurationStep(BaseConfigurationStep): required_settings = [ "EHERKENNING_OIDC_OIDC_RP_CLIENT_ID", "EHERKENNING_OIDC_OIDC_RP_CLIENT_SECRET", - # NOTE these are part of the model, but not actually part of the admin form - # "EHERKENNING_OIDC_OIDC_USE_NONCE", - # "EHERKENNING_OIDC_OIDC_NONCE_SIZE", - # "EHERKENNING_OIDC_OIDC_STATE_SIZE", - # "EHERKENNING_OIDC_OIDC_EXEMPT_URLS", ] all_settings = required_settings + [ "EHERKENNING_OIDC_IDENTIFIER_CLAIM_NAME", @@ -128,8 +122,12 @@ class eHerkenningOIDCConfigurationStep(BaseConfigurationStep): "EHERKENNING_OIDC_USERINFO_CLAIMS_SOURCE", "EHERKENNING_OIDC_ERROR_MESSAGE_MAPPING", "EHERKENNING_OIDC_OIDC_KEYCLOAK_IDP_HINT", + "EHERKENNING_OIDC_OIDC_USE_NONCE", + "EHERKENNING_OIDC_OIDC_NONCE_SIZE", + "EHERKENNING_OIDC_OIDC_STATE_SIZE", + "EHERKENNING_OIDC_OIDC_EXEMPT_URLS", ] - enable_setting = "EHERKENNING_OIDC_ENABLE" + enable_setting = "EHERKENNING_OIDC_CONFIG_ENABLE" def is_configured(self) -> bool: return OpenIDConnectEHerkenningConfig.get_solo().enabled @@ -156,7 +154,7 @@ def configure(self): if not form_data["error_message_mapping"]: del form_data["error_message_mapping"] - # Use the admin for to apply validation and fetch URLs from the discovery endpoint + # Use the admin form to apply validation and fetch URLs from the discovery endpoint form = OpenIDConnectEHerkenningConfigForm(data=form_data) if not form.is_valid(): raise ConfigurationRunFailed( @@ -205,7 +203,7 @@ class AdminOIDCConfigurationStep(BaseConfigurationStep): "ADMIN_OIDC_OIDC_EXEMPT_URLS", "ADMIN_OIDC_USERINFO_CLAIMS_SOURCE", ] - enable_setting = "ADMIN_OIDC_ENABLE" + enable_setting = "ADMIN_OIDC_CONFIG_ENABLE" def is_configured(self) -> bool: return OpenIDConnectConfig.get_solo().enabled @@ -237,7 +235,7 @@ def configure(self): form_data[model_field_name] = value form_data["enabled"] = True - # Use the admin for to apply validation and fetch URLs from the discovery endpoint + # Use the admin form to apply validation and fetch URLs from the discovery endpoint form = OpenIDConnectConfigForm(data=form_data) if not form.is_valid(): raise ConfigurationRunFailed( @@ -285,7 +283,7 @@ class DigiDConfigurationStep(BaseConfigurationStep): "DIGID_REQUESTED_ATTRIBUTES", "DIGID_SLO", ] - enable_setting = "DIGID_ENABLED" + enable_setting = "DIGID_CONFIG_ENABLE" def is_configured(self) -> bool: config = DigidConfiguration.get_solo() @@ -406,7 +404,7 @@ class eHerkenningConfigurationStep(BaseConfigurationStep): "EHERKENNING_NO_EIDAS", "EHERKENNING_SERVICE_LANGUAGE", ] - enable_setting = "EHERKENNING_ENABLE" + enable_setting = "EHERKENNING_CONFIG_ENABLE" def is_configured(self) -> bool: config = EherkenningConfiguration.get_solo() diff --git a/src/open_inwoner/configurations/tests/bootstrap/test_setup_auth_config.py b/src/open_inwoner/configurations/tests/bootstrap/test_setup_auth_config.py index 14f2938864..f6e42af736 100644 --- a/src/open_inwoner/configurations/tests/bootstrap/test_setup_auth_config.py +++ b/src/open_inwoner/configurations/tests/bootstrap/test_setup_auth_config.py @@ -80,6 +80,10 @@ DIGID_OIDC_USERINFO_CLAIMS_SOURCE=UserInformationClaimsSources.id_token, DIGID_OIDC_ERROR_MESSAGE_MAPPING={"some_error": "Some readable error"}, DIGID_OIDC_OIDC_KEYCLOAK_IDP_HINT="parameter", + DIGID_OIDC_OIDC_USE_NONCE=False, + DIGID_OIDC_OIDC_NONCE_SIZE=64, + DIGID_OIDC_OIDC_STATE_SIZE=64, + DIGID_OIDC_OIDC_EXEMPT_URLS=["/foo"], ) class DigiDOIDCConfigurationTests(ClearCachesMixin, TestCase): def test_configure(self): @@ -122,6 +126,10 @@ def test_configure(self): config.error_message_mapping, {"some_error": "Some readable error"} ) self.assertEqual(config.oidc_keycloak_idp_hint, "parameter") + self.assertEqual(config.oidc_use_nonce, False) + self.assertEqual(config.oidc_nonce_size, 64) + self.assertEqual(config.oidc_state_size, 64) + self.assertEqual(config.oidc_exempt_urls, ["/foo"]) @override_settings( DIGID_OIDC_IDENTIFIER_CLAIM_NAME=None, @@ -131,6 +139,10 @@ def test_configure(self): DIGID_OIDC_USERINFO_CLAIMS_SOURCE=None, DIGID_OIDC_ERROR_MESSAGE_MAPPING=None, DIGID_OIDC_OIDC_KEYCLOAK_IDP_HINT=None, + DIGID_OIDC_OIDC_USE_NONCE=None, + DIGID_OIDC_OIDC_NONCE_SIZE=None, + DIGID_OIDC_OIDC_STATE_SIZE=None, + DIGID_OIDC_OIDC_EXEMPT_URLS=None, ) def test_configure_use_defaults(self): DigiDOIDCConfigurationStep().configure() @@ -171,6 +183,10 @@ def test_configure_use_defaults(self): ) self.assertEqual(config.error_message_mapping, {}) self.assertEqual(config.oidc_keycloak_idp_hint, "") + self.assertEqual(config.oidc_use_nonce, True) + self.assertEqual(config.oidc_nonce_size, 32) + self.assertEqual(config.oidc_state_size, 32) + self.assertEqual(config.oidc_exempt_urls, []) @override_settings( DIGID_OIDC_OIDC_OP_DISCOVERY_ENDPOINT=IDENTITY_PROVIDER, @@ -279,6 +295,10 @@ def test_is_configured(self): EHERKENNING_OIDC_USERINFO_CLAIMS_SOURCE=UserInformationClaimsSources.id_token, EHERKENNING_OIDC_ERROR_MESSAGE_MAPPING={"some_error": "Some readable error"}, EHERKENNING_OIDC_OIDC_KEYCLOAK_IDP_HINT="parameter", + EHERKENNING_OIDC_OIDC_USE_NONCE=False, + EHERKENNING_OIDC_OIDC_NONCE_SIZE=64, + EHERKENNING_OIDC_OIDC_STATE_SIZE=64, + EHERKENNING_OIDC_OIDC_EXEMPT_URLS=["/foo"], ) class eHerkenningOIDCConfigurationTests(ClearCachesMixin, TestCase): def test_configure(self): @@ -321,6 +341,10 @@ def test_configure(self): config.error_message_mapping, {"some_error": "Some readable error"} ) self.assertEqual(config.oidc_keycloak_idp_hint, "parameter") + self.assertEqual(config.oidc_use_nonce, False) + self.assertEqual(config.oidc_nonce_size, 64) + self.assertEqual(config.oidc_state_size, 64) + self.assertEqual(config.oidc_exempt_urls, ["/foo"]) @override_settings( EHERKENNING_OIDC_IDENTIFIER_CLAIM_NAME=None, @@ -330,6 +354,10 @@ def test_configure(self): EHERKENNING_OIDC_USERINFO_CLAIMS_SOURCE=None, EHERKENNING_OIDC_ERROR_MESSAGE_MAPPING=None, EHERKENNING_OIDC_OIDC_KEYCLOAK_IDP_HINT=None, + EHERKENNING_OIDC_OIDC_USE_NONCE=None, + EHERKENNING_OIDC_OIDC_NONCE_SIZE=None, + EHERKENNING_OIDC_OIDC_STATE_SIZE=None, + EHERKENNING_OIDC_OIDC_EXEMPT_URLS=None, ) def test_configure_use_defaults(self): eHerkenningOIDCConfigurationStep().configure() @@ -370,6 +398,10 @@ def test_configure_use_defaults(self): ) self.assertEqual(config.error_message_mapping, {}) self.assertEqual(config.oidc_keycloak_idp_hint, "") + self.assertEqual(config.oidc_use_nonce, True) + self.assertEqual(config.oidc_nonce_size, 32) + self.assertEqual(config.oidc_state_size, 32) + self.assertEqual(config.oidc_exempt_urls, []) @override_settings( EHERKENNING_OIDC_OIDC_OP_DISCOVERY_ENDPOINT=IDENTITY_PROVIDER, From 7265ef7eacd4340fc95b09322c129c291a09a65d Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Thu, 18 Apr 2024 15:11:03 +0200 Subject: [PATCH 8/9] :white_check_mark: [#2324] Add test for setup_configuration command --- .../bootstrap/test_setup_configuration.py | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 src/open_inwoner/configurations/tests/bootstrap/test_setup_configuration.py diff --git a/src/open_inwoner/configurations/tests/bootstrap/test_setup_configuration.py b/src/open_inwoner/configurations/tests/bootstrap/test_setup_configuration.py new file mode 100644 index 0000000000..cd829c5ac7 --- /dev/null +++ b/src/open_inwoner/configurations/tests/bootstrap/test_setup_configuration.py @@ -0,0 +1,138 @@ +from io import StringIO +from unittest.mock import patch + +from django.core.management import call_command + +from rest_framework.test import APITestCase + +from open_inwoner.configurations.bootstrap.auth import ( + AdminOIDCConfigurationStep, + DigiDConfigurationStep, + DigiDOIDCConfigurationStep, + eHerkenningConfigurationStep, + eHerkenningOIDCConfigurationStep, +) +from open_inwoner.configurations.bootstrap.kic import ( + ContactmomentenAPIConfigurationStep, + KICAPIsConfigurationStep, + KlantenAPIConfigurationStep, +) +from open_inwoner.configurations.bootstrap.siteconfig import SiteConfigurationStep +from open_inwoner.configurations.bootstrap.zgw import ( + CatalogiAPIConfigurationStep, + DocumentenAPIConfigurationStep, + FormulierenAPIConfigurationStep, + ZakenAPIConfigurationStep, + ZGWAPIsConfigurationStep, +) + + +class SetupConfigurationTests(APITestCase): + maxDiff = None + + @patch( + "open_inwoner.configurations.bootstrap.zgw.ZakenAPIConfigurationStep.configure" + ) + @patch( + "open_inwoner.configurations.bootstrap.zgw.CatalogiAPIConfigurationStep.configure" + ) + @patch( + "open_inwoner.configurations.bootstrap.zgw.DocumentenAPIConfigurationStep.configure" + ) + @patch( + "open_inwoner.configurations.bootstrap.zgw.FormulierenAPIConfigurationStep.configure" + ) + @patch( + "open_inwoner.configurations.bootstrap.zgw.ZGWAPIsConfigurationStep.configure" + ) + @patch( + "open_inwoner.configurations.bootstrap.kic.KlantenAPIConfigurationStep.configure" + ) + @patch( + "open_inwoner.configurations.bootstrap.kic.ContactmomentenAPIConfigurationStep.configure" + ) + @patch( + "open_inwoner.configurations.bootstrap.kic.KICAPIsConfigurationStep.configure" + ) + @patch( + "open_inwoner.configurations.bootstrap.siteconfig.SiteConfigurationStep.configure" + ) + @patch( + "open_inwoner.configurations.bootstrap.auth.DigiDOIDCConfigurationStep.configure" + ) + @patch( + "open_inwoner.configurations.bootstrap.auth.eHerkenningOIDCConfigurationStep.configure" + ) + @patch( + "open_inwoner.configurations.bootstrap.auth.AdminOIDCConfigurationStep.configure" + ) + @patch( + "open_inwoner.configurations.bootstrap.auth.DigiDConfigurationStep.configure" + ) + @patch( + "open_inwoner.configurations.bootstrap.auth.eHerkenningConfigurationStep.configure" + ) + def test_setup_configuration_success(self, *mocks): + stdout = StringIO() + + call_command( + "setup_configuration", + no_selftest=True, + stdout=stdout, + no_color=True, + ) + + steps_to_configure = [ + ZakenAPIConfigurationStep(), + CatalogiAPIConfigurationStep(), + DocumentenAPIConfigurationStep(), + FormulierenAPIConfigurationStep(), + ZGWAPIsConfigurationStep(), + KlantenAPIConfigurationStep(), + ContactmomentenAPIConfigurationStep(), + KICAPIsConfigurationStep(), + SiteConfigurationStep(), + DigiDOIDCConfigurationStep(), + eHerkenningOIDCConfigurationStep(), + AdminOIDCConfigurationStep(), + DigiDConfigurationStep(), + eHerkenningConfigurationStep(), + ] + + command_output = stdout.getvalue().splitlines() + expected_output = [ + "Configuration will be set up with following steps: " + f"[{', '.join(str(step) for step in steps_to_configure)}]", + f"Configuring {ZakenAPIConfigurationStep()}...", + f"{ZakenAPIConfigurationStep()} is successfully configured", + f"Configuring {CatalogiAPIConfigurationStep()}...", + f"{CatalogiAPIConfigurationStep()} is successfully configured", + f"Configuring {DocumentenAPIConfigurationStep()}...", + f"{DocumentenAPIConfigurationStep()} is successfully configured", + f"Configuring {FormulierenAPIConfigurationStep()}...", + f"{FormulierenAPIConfigurationStep()} is successfully configured", + f"Configuring {ZGWAPIsConfigurationStep()}...", + f"{ZGWAPIsConfigurationStep()} is successfully configured", + f"Configuring {KlantenAPIConfigurationStep()}...", + f"{KlantenAPIConfigurationStep()} is successfully configured", + f"Configuring {ContactmomentenAPIConfigurationStep()}...", + f"{ContactmomentenAPIConfigurationStep()} is successfully configured", + f"Configuring {KICAPIsConfigurationStep()}...", + f"{KICAPIsConfigurationStep()} is successfully configured", + f"Configuring {SiteConfigurationStep()}...", + f"{SiteConfigurationStep()} is successfully configured", + f"Configuring {DigiDOIDCConfigurationStep()}...", + f"{DigiDOIDCConfigurationStep()} is successfully configured", + f"Configuring {eHerkenningOIDCConfigurationStep()}...", + f"{eHerkenningOIDCConfigurationStep()} is successfully configured", + f"Configuring {AdminOIDCConfigurationStep()}...", + f"{AdminOIDCConfigurationStep()} is successfully configured", + f"Configuring {DigiDConfigurationStep()}...", + f"{DigiDConfigurationStep()} is successfully configured", + f"Configuring {eHerkenningConfigurationStep()}...", + f"{eHerkenningConfigurationStep()} is successfully configured", + "Selftest is skipped.", + "Instance configuration completed.", + ] + + self.assertEqual(command_output, expected_output) From 10b3c2dff1e591141f4678c5252cbf9313e1597c Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Thu, 18 Apr 2024 16:25:31 +0200 Subject: [PATCH 9/9] :ok_hand: [#2324] PR feedback --- .../bootstrap/test_setup_configuration.py | 135 +++++++----------- 1 file changed, 48 insertions(+), 87 deletions(-) diff --git a/src/open_inwoner/configurations/tests/bootstrap/test_setup_configuration.py b/src/open_inwoner/configurations/tests/bootstrap/test_setup_configuration.py index cd829c5ac7..9c5fbdb664 100644 --- a/src/open_inwoner/configurations/tests/bootstrap/test_setup_configuration.py +++ b/src/open_inwoner/configurations/tests/bootstrap/test_setup_configuration.py @@ -2,6 +2,7 @@ from unittest.mock import patch from django.core.management import call_command +from django.test import override_settings from rest_framework.test import APITestCase @@ -26,52 +27,51 @@ ZGWAPIsConfigurationStep, ) +STEPS_TO_CONFIGURE = [ + ZakenAPIConfigurationStep(), + CatalogiAPIConfigurationStep(), + DocumentenAPIConfigurationStep(), + FormulierenAPIConfigurationStep(), + ZGWAPIsConfigurationStep(), + KlantenAPIConfigurationStep(), + ContactmomentenAPIConfigurationStep(), + KICAPIsConfigurationStep(), + SiteConfigurationStep(), + DigiDOIDCConfigurationStep(), + eHerkenningOIDCConfigurationStep(), + AdminOIDCConfigurationStep(), + DigiDConfigurationStep(), + eHerkenningConfigurationStep(), +] +REQUIRED_SETTINGS = { + setting_name: "SET" + for step in STEPS_TO_CONFIGURE + for setting_name in step.required_settings +} + + +@override_settings(**REQUIRED_SETTINGS) class SetupConfigurationTests(APITestCase): maxDiff = None - @patch( - "open_inwoner.configurations.bootstrap.zgw.ZakenAPIConfigurationStep.configure" - ) - @patch( - "open_inwoner.configurations.bootstrap.zgw.CatalogiAPIConfigurationStep.configure" - ) - @patch( - "open_inwoner.configurations.bootstrap.zgw.DocumentenAPIConfigurationStep.configure" - ) - @patch( - "open_inwoner.configurations.bootstrap.zgw.FormulierenAPIConfigurationStep.configure" - ) - @patch( - "open_inwoner.configurations.bootstrap.zgw.ZGWAPIsConfigurationStep.configure" - ) - @patch( - "open_inwoner.configurations.bootstrap.kic.KlantenAPIConfigurationStep.configure" - ) - @patch( - "open_inwoner.configurations.bootstrap.kic.ContactmomentenAPIConfigurationStep.configure" - ) - @patch( - "open_inwoner.configurations.bootstrap.kic.KICAPIsConfigurationStep.configure" - ) - @patch( - "open_inwoner.configurations.bootstrap.siteconfig.SiteConfigurationStep.configure" - ) - @patch( - "open_inwoner.configurations.bootstrap.auth.DigiDOIDCConfigurationStep.configure" - ) - @patch( - "open_inwoner.configurations.bootstrap.auth.eHerkenningOIDCConfigurationStep.configure" - ) - @patch( - "open_inwoner.configurations.bootstrap.auth.AdminOIDCConfigurationStep.configure" - ) - @patch( - "open_inwoner.configurations.bootstrap.auth.DigiDConfigurationStep.configure" - ) - @patch( - "open_inwoner.configurations.bootstrap.auth.eHerkenningConfigurationStep.configure" - ) + def setUp(self): + super().setUp() + + self.mocks = [] + for step in STEPS_TO_CONFIGURE: + mock_step = patch( + f"{step.__class__.__module__}.{step.__class__.__qualname__}.configure" + ) + self.mocks.append(mock_step) + mock_step.start() + + def stop_mocks(): + for mock_step in self.mocks: + mock_step.stop() + + self.addCleanup(stop_mocks) + def test_setup_configuration_success(self, *mocks): stdout = StringIO() @@ -82,55 +82,16 @@ def test_setup_configuration_success(self, *mocks): no_color=True, ) - steps_to_configure = [ - ZakenAPIConfigurationStep(), - CatalogiAPIConfigurationStep(), - DocumentenAPIConfigurationStep(), - FormulierenAPIConfigurationStep(), - ZGWAPIsConfigurationStep(), - KlantenAPIConfigurationStep(), - ContactmomentenAPIConfigurationStep(), - KICAPIsConfigurationStep(), - SiteConfigurationStep(), - DigiDOIDCConfigurationStep(), - eHerkenningOIDCConfigurationStep(), - AdminOIDCConfigurationStep(), - DigiDConfigurationStep(), - eHerkenningConfigurationStep(), - ] + output_per_step = [] + for step in STEPS_TO_CONFIGURE: + output_per_step.append(f"Configuring {str(step)}...") + output_per_step.append(f"{str(step)} is successfully configured") command_output = stdout.getvalue().splitlines() expected_output = [ "Configuration will be set up with following steps: " - f"[{', '.join(str(step) for step in steps_to_configure)}]", - f"Configuring {ZakenAPIConfigurationStep()}...", - f"{ZakenAPIConfigurationStep()} is successfully configured", - f"Configuring {CatalogiAPIConfigurationStep()}...", - f"{CatalogiAPIConfigurationStep()} is successfully configured", - f"Configuring {DocumentenAPIConfigurationStep()}...", - f"{DocumentenAPIConfigurationStep()} is successfully configured", - f"Configuring {FormulierenAPIConfigurationStep()}...", - f"{FormulierenAPIConfigurationStep()} is successfully configured", - f"Configuring {ZGWAPIsConfigurationStep()}...", - f"{ZGWAPIsConfigurationStep()} is successfully configured", - f"Configuring {KlantenAPIConfigurationStep()}...", - f"{KlantenAPIConfigurationStep()} is successfully configured", - f"Configuring {ContactmomentenAPIConfigurationStep()}...", - f"{ContactmomentenAPIConfigurationStep()} is successfully configured", - f"Configuring {KICAPIsConfigurationStep()}...", - f"{KICAPIsConfigurationStep()} is successfully configured", - f"Configuring {SiteConfigurationStep()}...", - f"{SiteConfigurationStep()} is successfully configured", - f"Configuring {DigiDOIDCConfigurationStep()}...", - f"{DigiDOIDCConfigurationStep()} is successfully configured", - f"Configuring {eHerkenningOIDCConfigurationStep()}...", - f"{eHerkenningOIDCConfigurationStep()} is successfully configured", - f"Configuring {AdminOIDCConfigurationStep()}...", - f"{AdminOIDCConfigurationStep()} is successfully configured", - f"Configuring {DigiDConfigurationStep()}...", - f"{DigiDConfigurationStep()} is successfully configured", - f"Configuring {eHerkenningConfigurationStep()}...", - f"{eHerkenningConfigurationStep()} is successfully configured", + f"[{', '.join(str(step) for step in STEPS_TO_CONFIGURE)}]", + *output_per_step, "Selftest is skipped.", "Instance configuration completed.", ]