diff --git a/src/openforms/authentication/contrib/digid_eherkenning_oidc/tests/base.py b/src/openforms/authentication/contrib/digid_eherkenning_oidc/tests/base.py new file mode 100644 index 0000000000..2cfa31ea2f --- /dev/null +++ b/src/openforms/authentication/contrib/digid_eherkenning_oidc/tests/base.py @@ -0,0 +1,69 @@ +from contextlib import contextmanager +from functools import partial +from pathlib import Path +from unittest.mock import patch + +from django.apps import apps +from django.test import override_settings + +from django_webtest import WebTest + +from openforms.utils.tests.vcr import OFVCRMixin + +TEST_FILES = (Path(__file__).parent / "data").resolve() + +KEYCLOAK_BASE_URL = "http://localhost:8080/realms/test/protocol/openid-connect" + + +@contextmanager +def mock_config(model: str, **overrides): + """ + Bundle all the required mocks. + + This context manager deliberately prevents the mocked things from being injected in + the test method signature. + """ + defaults = { + "enabled": True, + "oidc_rp_client_id": "testid", + "oidc_rp_client_secret": "7DB3KUAAizYCcmZufpHRVOcD0TOkNO3I", + "oidc_rp_sign_algo": "RS256", + "oidc_rp_scopes_list": ["openid"], + "oidc_op_jwks_endpoint": f"{KEYCLOAK_BASE_URL}/certs", + "oidc_op_authorization_endpoint": f"{KEYCLOAK_BASE_URL}/auth", + "oidc_op_token_endpoint": f"{KEYCLOAK_BASE_URL}/token", + "oidc_op_user_endpoint": f"{KEYCLOAK_BASE_URL}/userinfo", + } + field_values = {**defaults, **overrides} + model_cls = apps.get_model("digid_eherkenning_oidc_generics", model) + with ( + # bypass django-solo queries + cache hits + patch( + f"digid_eherkenning_oidc_generics.models.{model}.get_solo", + return_value=model_cls(**field_values), + ), + # mock the state & nonce random value generation so we get predictable URLs to + # match with VCR + patch( + "mozilla_django_oidc.views.get_random_string", + return_value="not-a-random-string", + ), + ): + yield + + +mock_digid_config = partial( + mock_config, + model="OpenIDConnectPublicConfig", + oidc_rp_scopes_list=["openid", "bsn"], +) +mock_eherkenning_config = partial( + mock_config, + model="OpenIDConnectEHerkenningConfig", + oidc_rp_scopes_list=["openid", "kvk"], +) + + +@override_settings(CORS_ALLOW_ALL_ORIGINS=True, IS_HTTPS=True) +class IntegrationTestsBase(OFVCRMixin, WebTest): + VCR_TEST_FILES = TEST_FILES diff --git a/src/openforms/authentication/contrib/digid_eherkenning_oidc/tests/test_auth_flow.py b/src/openforms/authentication/contrib/digid_eherkenning_oidc/tests/test_auth_flow_callbacks.py similarity index 60% rename from src/openforms/authentication/contrib/digid_eherkenning_oidc/tests/test_auth_flow.py rename to src/openforms/authentication/contrib/digid_eherkenning_oidc/tests/test_auth_flow_callbacks.py index 6190aed2ce..0883799bf5 100644 --- a/src/openforms/authentication/contrib/digid_eherkenning_oidc/tests/test_auth_flow.py +++ b/src/openforms/authentication/contrib/digid_eherkenning_oidc/tests/test_auth_flow_callbacks.py @@ -11,142 +11,19 @@ to bring up a Keycloak instance. """ -from contextlib import contextmanager -from functools import partial -from pathlib import Path -from unittest.mock import patch - -from django.apps import apps -from django.test import override_settings, tag -from django.urls import reverse_lazy +from django.test import tag import requests -from django_webtest import WebTest from furl import furl from openforms.accounts.tests.factories import StaffUserFactory from openforms.authentication.tests.utils import URLsHelper from openforms.authentication.views import BACKEND_OUTAGE_RESPONSE_PARAMETER from openforms.forms.tests.factories import FormFactory -from openforms.utils.tests.vcr import OFVCRMixin +from .base import IntegrationTestsBase, mock_digid_config, mock_eherkenning_config from .keycloak_utils import keycloak_login -TEST_FILES = (Path(__file__).parent / "data").resolve() - -KEYCLOAK_BASE_URL = "http://localhost:8080/realms/test/protocol/openid-connect" - - -@contextmanager -def mock_config(model: str, **overrides): - """ - Bundle all the required mocks. - - This context manager deliberately prevents the mocked things from being injected in - the test method signature. - """ - defaults = { - "enabled": True, - "oidc_rp_client_id": "testid", - "oidc_rp_client_secret": "7DB3KUAAizYCcmZufpHRVOcD0TOkNO3I", - "oidc_rp_sign_algo": "RS256", - "oidc_rp_scopes_list": ["openid"], - "oidc_op_jwks_endpoint": f"{KEYCLOAK_BASE_URL}/certs", - "oidc_op_authorization_endpoint": f"{KEYCLOAK_BASE_URL}/auth", - "oidc_op_token_endpoint": f"{KEYCLOAK_BASE_URL}/token", - "oidc_op_user_endpoint": f"{KEYCLOAK_BASE_URL}/userinfo", - } - field_values = {**defaults, **overrides} - model_cls = apps.get_model("digid_eherkenning_oidc_generics", model) - with ( - # bypass django-solo queries + cache hits - patch( - f"digid_eherkenning_oidc_generics.models.{model}.get_solo", - return_value=model_cls(**field_values), - ), - # mock the state & nonce random value generation so we get predictable URLs to - # match with VCR - patch( - "mozilla_django_oidc.views.get_random_string", - return_value="not-a-random-string", - ), - ): - yield - - -mock_digid_config = partial( - mock_config, - model="OpenIDConnectPublicConfig", - oidc_rp_scopes_list=["openid", "bsn"], -) -mock_eherkenning_config = partial( - mock_config, - model="OpenIDConnectEHerkenningConfig", - oidc_rp_scopes_list=["openid", "kvk"], -) - - -@override_settings(CORS_ALLOW_ALL_ORIGINS=True, IS_HTTPS=True) -class IntegrationTestsBase(OFVCRMixin, WebTest): - VCR_TEST_FILES = TEST_FILES - - -class DigiDInitTests(IntegrationTestsBase): - """ - Test the outbound part of OIDC-based DigiD authentication. - """ - - CALLBACK_URL = f"http://testserver{reverse_lazy('digid_oidc:callback')}" - - @mock_digid_config() - def test_start_flow_redirects_to_oidc_provider(self): - form = FormFactory.create(authentication_backends=["digid_oidc"]) - start_url = URLsHelper(form=form).get_auth_start(plugin_id="digid_oidc") - - response = self.app.get(start_url) - - self.assertEqual(response.status_code, 302) - redirect_target = furl(response["Location"]) - query_params = redirect_target.query.params - self.assertEqual(redirect_target.host, "localhost") - self.assertEqual(redirect_target.port, 8080) - self.assertEqual( - redirect_target.path, - "/realms/test/protocol/openid-connect/auth", - ) - self.assertEqual(query_params["scope"], "openid bsn") - self.assertEqual(query_params["client_id"], "testid") - self.assertEqual(query_params["redirect_uri"], self.CALLBACK_URL) - - @mock_digid_config( - oidc_op_authorization_endpoint="http://localhost:8080/i-dont-exist" - ) - def test_idp_availability_check(self): - form = FormFactory.create(authentication_backends=["digid_oidc"]) - url_helper = URLsHelper(form=form) - start_url = url_helper.get_auth_start(plugin_id="digid_oidc") - - response = self.app.get(start_url) - - self.assertEqual(response.status_code, 302) - redirect_url = furl(response["Location"]) - self.assertEqual(redirect_url.host, "testserver") - self.assertEqual(redirect_url.path, url_helper.form_path) - query_params = redirect_url.query.params - self.assertEqual(query_params[BACKEND_OUTAGE_RESPONSE_PARAMETER], "digid_oidc") - - @mock_digid_config(oidc_keycloak_idp_hint="oidc-digid") - def test_keycloak_idp_hint_is_respected(self): - form = FormFactory.create(authentication_backends=["digid_oidc"]) - url_helper = URLsHelper(form=form) - start_url = url_helper.get_auth_start(plugin_id="digid_oidc") - - response = self.app.get(start_url) - - self.assertEqual(response.status_code, 302) - redirect_url = furl(response["Location"]) - self.assertEqual(redirect_url.args["kc_idp_hint"], "oidc-digid") - class DigiDCallbackTests(IntegrationTestsBase): """ @@ -250,65 +127,6 @@ def test_digid_error_reported_for_cancelled_login_with_staff_django_user(self): self.assertEqual(callback_response.request.url, str(expected_url)) -class EHerkenningInitTests(IntegrationTestsBase): - """ - Test the outbound part of OIDC-based eHerkenning authentication. - """ - - CALLBACK_URL = f"http://testserver{reverse_lazy('eherkenning_oidc:callback')}" - - @mock_eherkenning_config() - def test_start_flow_redirects_to_oidc_provider(self): - form = FormFactory.create(authentication_backends=["eherkenning_oidc"]) - start_url = URLsHelper(form=form).get_auth_start(plugin_id="eherkenning_oidc") - - response = self.app.get(start_url) - - self.assertEqual(response.status_code, 302) - redirect_target = furl(response["Location"]) - query_params = redirect_target.query.params - self.assertEqual(redirect_target.host, "localhost") - self.assertEqual(redirect_target.port, 8080) - self.assertEqual( - redirect_target.path, - "/realms/test/protocol/openid-connect/auth", - ) - self.assertEqual(query_params["scope"], "openid kvk") - self.assertEqual(query_params["client_id"], "testid") - self.assertEqual(query_params["redirect_uri"], self.CALLBACK_URL) - - @mock_eherkenning_config( - oidc_op_authorization_endpoint="http://localhost:8080/i-dont-exist" - ) - def test_idp_availability_check(self): - form = FormFactory.create(authentication_backends=["eherkenning_oidc"]) - url_helper = URLsHelper(form=form) - start_url = url_helper.get_auth_start(plugin_id="eherkenning_oidc") - - response = self.app.get(start_url) - - self.assertEqual(response.status_code, 302) - redirect_url = furl(response["Location"]) - self.assertEqual(redirect_url.host, "testserver") - self.assertEqual(redirect_url.path, url_helper.form_path) - query_params = redirect_url.query.params - self.assertEqual( - query_params[BACKEND_OUTAGE_RESPONSE_PARAMETER], "eherkenning_oidc" - ) - - @mock_eherkenning_config(oidc_keycloak_idp_hint="oidc-eherkenning") - def test_keycloak_idp_hint_is_respected(self): - form = FormFactory.create(authentication_backends=["eherkenning_oidc"]) - url_helper = URLsHelper(form=form) - start_url = url_helper.get_auth_start(plugin_id="eherkenning_oidc") - - response = self.app.get(start_url) - - self.assertEqual(response.status_code, 302) - redirect_url = furl(response["Location"]) - self.assertEqual(redirect_url.args["kc_idp_hint"], "oidc-eherkenning") - - class EHerkenningCallbackTests(IntegrationTestsBase): """ Test the return/callback side after authenticating with the identity provider. diff --git a/src/openforms/authentication/contrib/digid_eherkenning_oidc/tests/test_auth_flow_init.py b/src/openforms/authentication/contrib/digid_eherkenning_oidc/tests/test_auth_flow_init.py new file mode 100644 index 0000000000..8074337460 --- /dev/null +++ b/src/openforms/authentication/contrib/digid_eherkenning_oidc/tests/test_auth_flow_init.py @@ -0,0 +1,138 @@ +""" +Test the authentication flow for a form. + +These tests use VCR. When re-recording, making sure to: + +.. code-block:: bash + + cd docker + docker compose -f docker-compose.keycloak.yml up + +to bring up a Keycloak instance. +""" + +from django.urls import reverse_lazy + +from furl import furl + +from openforms.authentication.tests.utils import URLsHelper +from openforms.authentication.views import BACKEND_OUTAGE_RESPONSE_PARAMETER +from openforms.forms.tests.factories import FormFactory + +from .base import IntegrationTestsBase, mock_digid_config, mock_eherkenning_config + + +class DigiDInitTests(IntegrationTestsBase): + """ + Test the outbound part of OIDC-based DigiD authentication. + """ + + CALLBACK_URL = f"http://testserver{reverse_lazy('digid_oidc:callback')}" + + @mock_digid_config() + def test_start_flow_redirects_to_oidc_provider(self): + form = FormFactory.create(authentication_backends=["digid_oidc"]) + start_url = URLsHelper(form=form).get_auth_start(plugin_id="digid_oidc") + + response = self.app.get(start_url) + + self.assertEqual(response.status_code, 302) + redirect_target = furl(response["Location"]) + query_params = redirect_target.query.params + self.assertEqual(redirect_target.host, "localhost") + self.assertEqual(redirect_target.port, 8080) + self.assertEqual( + redirect_target.path, + "/realms/test/protocol/openid-connect/auth", + ) + self.assertEqual(query_params["scope"], "openid bsn") + self.assertEqual(query_params["client_id"], "testid") + self.assertEqual(query_params["redirect_uri"], self.CALLBACK_URL) + + @mock_digid_config( + oidc_op_authorization_endpoint="http://localhost:8080/i-dont-exist" + ) + def test_idp_availability_check(self): + form = FormFactory.create(authentication_backends=["digid_oidc"]) + url_helper = URLsHelper(form=form) + start_url = url_helper.get_auth_start(plugin_id="digid_oidc") + + response = self.app.get(start_url) + + self.assertEqual(response.status_code, 302) + redirect_url = furl(response["Location"]) + self.assertEqual(redirect_url.host, "testserver") + self.assertEqual(redirect_url.path, url_helper.form_path) + query_params = redirect_url.query.params + self.assertEqual(query_params[BACKEND_OUTAGE_RESPONSE_PARAMETER], "digid_oidc") + + @mock_digid_config(oidc_keycloak_idp_hint="oidc-digid") + def test_keycloak_idp_hint_is_respected(self): + form = FormFactory.create(authentication_backends=["digid_oidc"]) + url_helper = URLsHelper(form=form) + start_url = url_helper.get_auth_start(plugin_id="digid_oidc") + + response = self.app.get(start_url) + + self.assertEqual(response.status_code, 302) + redirect_url = furl(response["Location"]) + self.assertEqual(redirect_url.args["kc_idp_hint"], "oidc-digid") + + +class EHerkenningInitTests(IntegrationTestsBase): + """ + Test the outbound part of OIDC-based eHerkenning authentication. + """ + + CALLBACK_URL = f"http://testserver{reverse_lazy('eherkenning_oidc:callback')}" + + @mock_eherkenning_config() + def test_start_flow_redirects_to_oidc_provider(self): + form = FormFactory.create(authentication_backends=["eherkenning_oidc"]) + start_url = URLsHelper(form=form).get_auth_start(plugin_id="eherkenning_oidc") + + response = self.app.get(start_url) + + self.assertEqual(response.status_code, 302) + redirect_target = furl(response["Location"]) + query_params = redirect_target.query.params + self.assertEqual(redirect_target.host, "localhost") + self.assertEqual(redirect_target.port, 8080) + self.assertEqual( + redirect_target.path, + "/realms/test/protocol/openid-connect/auth", + ) + self.assertEqual(query_params["scope"], "openid kvk") + self.assertEqual(query_params["client_id"], "testid") + self.assertEqual(query_params["redirect_uri"], self.CALLBACK_URL) + + @mock_eherkenning_config( + oidc_op_authorization_endpoint="http://localhost:8080/i-dont-exist" + ) + def test_idp_availability_check(self): + form = FormFactory.create(authentication_backends=["eherkenning_oidc"]) + url_helper = URLsHelper(form=form) + start_url = url_helper.get_auth_start(plugin_id="eherkenning_oidc") + + response = self.app.get(start_url) + + self.assertEqual(response.status_code, 302) + redirect_url = furl(response["Location"]) + self.assertEqual(redirect_url.host, "testserver") + self.assertEqual(redirect_url.path, url_helper.form_path) + query_params = redirect_url.query.params + self.assertEqual( + query_params[BACKEND_OUTAGE_RESPONSE_PARAMETER], "eherkenning_oidc" + ) + + @mock_eherkenning_config(oidc_keycloak_idp_hint="oidc-eherkenning") + def test_keycloak_idp_hint_is_respected(self): + form = FormFactory.create(authentication_backends=["eherkenning_oidc"]) + url_helper = URLsHelper(form=form) + start_url = url_helper.get_auth_start(plugin_id="eherkenning_oidc") + + response = self.app.get(start_url) + + self.assertEqual(response.status_code, 302) + redirect_url = furl(response["Location"]) + self.assertEqual(redirect_url.args["kc_idp_hint"], "oidc-eherkenning")