diff --git a/src/eherkenning/tests/test_mock_views.py b/src/eherkenning/tests/test_mock_views.py index c8964affb3..00778fc6dc 100644 --- a/src/eherkenning/tests/test_mock_views.py +++ b/src/eherkenning/tests/test_mock_views.py @@ -108,8 +108,15 @@ def test_get_returns_valid_response(self): self.assertContains(response, reverse("login")) self.assertNoEHerkenningURLS(response) - @patch("open_inwoner.kvk.client.KvKClient.get_all_company_branches") - def test_post_redirects_and_authenticates(self, mock_kvk): + @patch( + "open_inwoner.kvk.signals.KvKClient.retrieve_rsin_with_kvk", + return_value="123456789", + autospec=True, + ) + @patch("open_inwoner.kvk.client.KvKClient.get_all_company_branches", autospec=True) + def test_post_redirects_and_authenticates( + self, mock_kvk, mock_retrieve_rsin_with_kvk + ): mock_kvk.return_value = [ {"kvkNummer": "29664887", "vestigingsnummer": "1234"}, {"kvkNummer": "29664887", "vestigingsnummer": "5678"}, diff --git a/src/open_inwoner/accounts/eherkenning_urls.py b/src/open_inwoner/accounts/eherkenning_urls.py new file mode 100644 index 0000000000..bd03f5cda9 --- /dev/null +++ b/src/open_inwoner/accounts/eherkenning_urls.py @@ -0,0 +1,16 @@ +from django.urls import path + +from digid_eherkenning_oidc_generics.eherkenning_urls import urlpatterns + +from .views import CustomEHerkenningOIDCAuthenticationCallbackView + +app_name = "eherkenning_oidc" + + +urlpatterns = [ + path( + "callback/", + CustomEHerkenningOIDCAuthenticationCallbackView.as_view(), + name="callback", + ), +] + urlpatterns diff --git a/src/open_inwoner/accounts/tests/test_auth.py b/src/open_inwoner/accounts/tests/test_auth.py index 9ea470b54e..6efe7d8b78 100644 --- a/src/open_inwoner/accounts/tests/test_auth.py +++ b/src/open_inwoner/accounts/tests/test_auth.py @@ -20,6 +20,7 @@ from open_inwoner.haalcentraal.tests.mixins import HaalCentraalMixin from open_inwoner.kvk.branches import get_kvk_branch_number from open_inwoner.kvk.tests.factories import CertificateFactory +from open_inwoner.openzaak.models import OpenZaakConfig from ...cms.collaborate.cms_apps import CollaborateApphook from ...cms.tests import cms_tools @@ -34,6 +35,9 @@ eHerkenningUserFactory, ) +RETURN_URL = "/" +CANCEL_URL = reverse("login") + @override_settings(ROOT_URLCONF="open_inwoner.cms.tests.urls") class DigiDRegistrationTest(AssertRedirectsMixin, HaalCentraalMixin, WebTest): @@ -559,6 +563,46 @@ def test_eherkenning_fail_without_invite_redirects_to_login_page(self, m): self.assertRedirectsLogin(response, with_host=True) + @patch( + "open_inwoner.kvk.signals.KvKClient.retrieve_rsin_with_kvk", + return_value="", + autospec=True, + ) + @patch( + "open_inwoner.accounts.views.auth.OpenZaakConfig.get_solo", + return_value=OpenZaakConfig(fetch_eherkenning_zaken_with_rsin=True), + autospec=True, + ) + def test_login_as_eenmanszaak_blocked( + self, mock_oz_config, mock_retrieve_rsin_with_kvk + ): + url = reverse("eherkenning-mock:password") + params = { + "acs": f"http://testserver{reverse('eherkenning:acs')}", + "next": RETURN_URL, + "cancel": CANCEL_URL, + } + url = f"{url}?{urlencode(params)}" + + data = { + "auth_name": "29664887", + "auth_pass": "company@localhost", + } + + # post our password to the IDP + response = self.client.post(url, data, follow=False) + + # it will redirect to our ACS + self.assertEqual(response.status_code, 302) + self.assertIn(reverse("eherkenning:acs"), response["Location"]) + + # follow the ACS redirect and get/create the user + response = self.client.get(response["Location"]) + + # User is logged out and redirected to login view + self.assertNotIn("_auth_user_id", self.app.session) + self.assertRedirectsLogin(response, with_host=True) + @patch("eherkenning.validators.KVKValidator.__call__") def test_eherkenning_fail_without_invite_and_next_url_redirects_to_login_page( self, m @@ -614,12 +658,24 @@ def test_eherkenning_fail_with_invite_redirects_to_register_page(self, m): f"http://testserver{reverse('django_registration_register')}?invite={invite.key}", ) - @patch("open_inwoner.kvk.client.KvKClient.get_all_company_branches") + @patch( + "open_inwoner.kvk.signals.KvKClient.retrieve_rsin_with_kvk", + return_value="123456789", + autospec=True, + ) + @patch( + "open_inwoner.kvk.client.KvKClient.get_all_company_branches", + autospec=True, + ) @patch( "open_inwoner.kvk.models.KvKConfig.get_solo", + autospec=True, ) def test_invite_url_not_in_session_after_successful_login( - self, mock_solo, mock_kvk + self, + mock_solo, + mock_kvk, + mock_retrieve_rsin_with_kvk, ): mock_kvk.return_value = [ {"kvkNummer": "12345678", "vestigingsnummer": "1234"}, @@ -687,7 +743,7 @@ def test_redirect_flow_with_no_vestigingsnummer(self, mock_solo, mock_kvk): mock_solo.return_value.server_certificate = CertificateFactory() user = eHerkenningUserFactory.create( - kvk="12345678", email="user-12345678@localhost" + kvk="12345678", rsin="123456789", email="user-12345678@localhost" ) url = reverse("eherkenning-mock:password") @@ -1048,8 +1104,16 @@ def test_digid_user_success(self): self.assertEqual(users.first().email, "test@example.com") self.assertEqual(users.last().email, "test@example.com") - @patch("open_inwoner.kvk.client.KvKClient.get_all_company_branches") - def test_eherkenning_user_success(self, mock_kvk): + @patch( + "open_inwoner.kvk.signals.KvKClient.retrieve_rsin_with_kvk", + return_value="123456789", + autospec=True, + ) + @patch( + "open_inwoner.kvk.client.KvKClient.get_all_company_branches", + autospec=True, + ) + def test_eherkenning_user_success(self, mock_kvk, mock_retrieve_rsin_with_kvk): """Assert that eHerkenning users can register with duplicate emails""" mock_kvk.return_value = [ @@ -1068,6 +1132,7 @@ def test_eherkenning_user_success(self, mock_kvk): test_user = eHerkenningUserFactory.create( email="test@localhost", kvk="64819772", + rsin="123456789", ) url = reverse("eherkenning-mock:password") diff --git a/src/open_inwoner/accounts/tests/test_oidc_views.py b/src/open_inwoner/accounts/tests/test_oidc_views.py index 1e1ffbe332..d35995ffb6 100644 --- a/src/open_inwoner/accounts/tests/test_oidc_views.py +++ b/src/open_inwoner/accounts/tests/test_oidc_views.py @@ -6,6 +6,7 @@ from django.core.exceptions import ValidationError from django.test import TestCase, modify_settings, override_settings from django.urls import reverse +from django.utils.translation import gettext as _ import requests import requests_mock @@ -25,6 +26,7 @@ from open_inwoner.configurations.choices import OpenIDDisplayChoices from open_inwoner.configurations.models import SiteConfiguration from open_inwoner.kvk.branches import KVK_BRANCH_SESSION_VARIABLE +from open_inwoner.openzaak.models import OpenZaakConfig from ..choices import LoginTypeChoices from .factories import DigidUserFactory, UserFactory, eHerkenningUserFactory @@ -977,6 +979,7 @@ def test_existing_kvk_creates_no_new_user( first_name="John", last_name="Doe", kvk="12345678", + rsin="123456789", email="user-12345678@localhost", is_prepopulated=True, ) @@ -1031,7 +1034,9 @@ def test_new_user_is_created_when_new_kvk( mock_retrieve_rsin_with_kvk.return_value = "123456789" # set up a user with a non existing email address mock_get_userinfo.return_value = {"sub": "00000000"} - eHerkenningUserFactory.create(kvk="12345678", email="existing_user@example.com") + eHerkenningUserFactory.create( + kvk="12345678", rsin="123456789", email="existing_user@example.com" + ) session = self.client.session session["oidc_states"] = {"mock": {"nonce": "nonce"}} session.save() @@ -1109,14 +1114,32 @@ def test_logout(self, mock_get_solo): ] } ) - @patch("open_inwoner.kvk.client.KvKClient.get_all_company_branches") - @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.get_userinfo") - @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.store_tokens") - @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.verify_token") - @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.get_token") + @patch( + "open_inwoner.kvk.signals.KvKClient.retrieve_rsin_with_kvk", + return_value="123456789", + autospec=True, + ) + @patch("open_inwoner.kvk.client.KvKClient.get_all_company_branches", autospec=True) + @patch( + "mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.get_userinfo", + autospec=True, + ) + @patch( + "mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.store_tokens", + autospec=True, + ) + @patch( + "mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.verify_token", + autospec=True, + ) + @patch( + "mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.get_token", + autospec=True, + ) @patch( "digid_eherkenning_oidc_generics.models.OpenIDConnectEHerkenningConfig.get_solo", return_value=OpenIDConnectEHerkenningConfig(id=1, enabled=True), + autospec=True, ) def test_error_first_cleared_after_succesful_login( self, @@ -1126,6 +1149,7 @@ def test_error_first_cleared_after_succesful_login( mock_store_tokens, mock_get_userinfo, mock_kvk, + mock_retrieve_rsin_with_kvk, ): mock_get_userinfo.return_value = { "sub": "some_username", @@ -1318,17 +1342,111 @@ def test_login_validation_error( self.assertEqual(error_msg, str(GENERIC_EHERKENNING_ERROR_MSG)) - @patch("open_inwoner.kvk.client.KvKClient.get_all_company_branches") - @patch("open_inwoner.utils.context_processors.SiteConfiguration") - @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.get_userinfo") - @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.store_tokens") - @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.verify_token") - @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.get_token") + @patch( + "open_inwoner.accounts.views.auth.OpenZaakConfig.get_solo", + return_value=OpenZaakConfig(fetch_eherkenning_zaken_with_rsin=True), + autospec=True, + ) + @patch("open_inwoner.kvk.signals.KvKClient.retrieve_rsin_with_kvk", autospec=True) + @patch( + "mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.get_userinfo", + autospec=True, + ) + @patch( + "mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.store_tokens", + autospec=True, + ) + @patch( + "mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.verify_token", + autospec=True, + ) + @patch( + "mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.get_token", + autospec=True, + ) + @patch( + "digid_eherkenning_oidc_generics.models.OpenIDConnectEHerkenningConfig.get_solo", + return_value=OpenIDConnectEHerkenningConfig( + id=1, enabled=True, identifier_claim_name="sub" + ), + autospec=True, + ) + def test_login_as_eenmanszaak_blocked( + self, + mock_get_solo, + mock_get_token, + mock_verify_token, + mock_store_tokens, + mock_get_userinfo, + mock_retrieve_rsin_with_kvk, + mock_oz_config, + ): + """ + Eenmanszaken do not have an RSIN, which means that if we have a feature flag + to fetch resources using RSIN (from Open Zaak or Open Klant) enabled, we cannot + let eenmanszaken log in using eHerkenning + """ + mock_retrieve_rsin_with_kvk.return_value = "" + # set up a user with a non existing email address + mock_get_userinfo.return_value = {"sub": "00000000"} + eHerkenningUserFactory.create(kvk="12345678", email="existing_user@example.com") + session = self.client.session + session["oidc_states"] = {"mock": {"nonce": "nonce"}} + session.save() + callback_url = reverse("eherkenning_oidc:callback") + + self.assertFalse(User.objects.filter(email="new_user@example.com").exists()) + + # enter the login flow + callback_response = self.client.get( + callback_url, {"code": "mock", "state": "mock"} + ) + + # User is logged out and redirected to login view + self.assertNotIn("_auth_user_id", self.app.session) + self.assertRedirects( + callback_response, reverse("login"), fetch_redirect_response=False + ) + + response = self.client.get(callback_response.url) + + self.assertContains(response, _("Use DigiD to log in as a sole proprietor.")) + + @patch( + "open_inwoner.kvk.signals.KvKClient.retrieve_rsin_with_kvk", + return_value="123456789", + autospec=True, + ) + @patch( + "open_inwoner.kvk.client.KvKClient.get_all_company_branches", + autospec=True, + ) + @patch( + "open_inwoner.utils.context_processors.SiteConfiguration", + autospec=True, + ) + @patch( + "mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.get_userinfo", + autospec=True, + ) + @patch( + "mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.store_tokens", + autospec=True, + ) + @patch( + "mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.verify_token", + autospec=True, + ) + @patch( + "mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.get_token", + autospec=True, + ) @patch( "digid_eherkenning_oidc_generics.models.OpenIDConnectEHerkenningConfig.get_solo", return_value=OpenIDConnectEHerkenningConfig( id=1, enabled=True, oidc_op_authorization_endpoint="http://idp.local/auth" ), + autospec=True, ) def test_redirect_after_login_with_registration_and_branch_selection( self, @@ -1339,6 +1457,7 @@ def test_redirect_after_login_with_registration_and_branch_selection( mock_get_userinfo, mock_siteconfig, mock_kvk, + mock_retrieve_rsin_with_kvk, ): """ Full authentication flow with redirect after successful login @@ -1409,17 +1528,40 @@ def test_redirect_after_login_with_registration_and_branch_selection( self.assertEqual(profile_response.status_code, 200) - @patch("open_inwoner.kvk.client.KvKClient.get_all_company_branches") - @patch("open_inwoner.utils.context_processors.SiteConfiguration") - @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.get_userinfo") - @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.store_tokens") - @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.verify_token") - @patch("mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.get_token") + @patch( + "open_inwoner.kvk.signals.KvKClient.retrieve_rsin_with_kvk", + autospec=True, + ) + @patch( + "open_inwoner.kvk.client.KvKClient.get_all_company_branches", + autospec=True, + ) + @patch( + "open_inwoner.utils.context_processors.SiteConfiguration", + autospec=True, + ) + @patch( + "mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.get_userinfo", + autospec=True, + ) + @patch( + "mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.store_tokens", + autospec=True, + ) + @patch( + "mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.verify_token", + autospec=True, + ) + @patch( + "mozilla_django_oidc_db.backends.OIDCAuthenticationBackend.get_token", + autospec=True, + ) @patch( "digid_eherkenning_oidc_generics.models.OpenIDConnectEHerkenningConfig.get_solo", return_value=OpenIDConnectEHerkenningConfig( id=1, enabled=True, oidc_op_authorization_endpoint="http://idp.local/auth" ), + autospec=True, ) def test_redirect_after_login_no_registration_with_branch_selection( self, @@ -1430,11 +1572,12 @@ def test_redirect_after_login_no_registration_with_branch_selection( mock_get_userinfo, mock_siteconfig, mock_kvk, + mock_retrieve_rsin_with_kvk, ): """ Full authentication flow with redirect after successful login """ - user = eHerkenningUserFactory.create(kvk="12345678") + user = eHerkenningUserFactory.create(kvk="12345678", rsin="123456789") mock_get_userinfo.return_value = { "sub": "some_username", "kvk": "12345678", @@ -1512,7 +1655,7 @@ def test_redirect_after_login_no_registration_and_no_branch_selection( """ Full authentication flow with redirect after successful login """ - user = eHerkenningUserFactory.create(kvk="12345678") + user = eHerkenningUserFactory.create(kvk="12345678", rsin="123456789") mock_get_userinfo.return_value = { "sub": "some_username", "kvk": "12345678", diff --git a/src/open_inwoner/accounts/views/__init__.py b/src/open_inwoner/accounts/views/__init__.py index 7fbe86a2cb..c7b3733bfa 100644 --- a/src/open_inwoner/accounts/views/__init__.py +++ b/src/open_inwoner/accounts/views/__init__.py @@ -13,6 +13,7 @@ CustomDigiDAssertionConsumerServiceView, CustomeHerkenningAssertionConsumerServiceMockView, CustomeHerkenningAssertionConsumerServiceView, + CustomEHerkenningOIDCAuthenticationCallbackView, LogPasswordChangeView, LogPasswordResetConfirmView, LogPasswordResetView, @@ -81,4 +82,5 @@ "NewsletterSubscribeView", "CustomRegistrationView", "NecessaryFieldsUserView", + "CustomEHerkenningOIDCAuthenticationCallbackView", ] diff --git a/src/open_inwoner/accounts/views/auth.py b/src/open_inwoner/accounts/views/auth.py index 236aff9e63..062556081e 100644 --- a/src/open_inwoner/accounts/views/auth.py +++ b/src/open_inwoner/accounts/views/auth.py @@ -1,10 +1,12 @@ from django.conf import settings +from django.contrib import auth, messages from django.contrib.auth.mixins import UserPassesTestMixin from django.contrib.auth.views import ( PasswordChangeView, PasswordResetConfirmView, PasswordResetView, ) +from django.http import HttpResponseRedirect from django.shortcuts import resolve_url from django.urls import reverse from django.utils.translation import gettext as _ @@ -15,10 +17,15 @@ from digid_eherkenning.views.digid import DigiDAssertionConsumerServiceView from digid_eherkenning.views.eherkenning import eHerkenningAssertionConsumerServiceView +from digid_eherkenning_oidc_generics.views import ( + eHerkenningOIDCAuthenticationCallbackView, +) from eherkenning.mock import eherkenning_conf from eherkenning.mock.views.eherkenning import ( eHerkenningAssertionConsumerServiceMockView, ) +from open_inwoner.openklant.models import OpenKlantConfig +from open_inwoner.openzaak.models import OpenZaakConfig from open_inwoner.utils.views import LogMixin from ..choices import LoginTypeChoices @@ -133,8 +140,30 @@ def get_success_url(self): return super().get_success_url() +class BlockEenmanszaakLoginMixin: + def get(self, request): + response = super().get(request) + + openzaak_config = OpenZaakConfig.get_solo() + openklant_config = OpenKlantConfig.get_solo() + if ( + hasattr(request.user, "rsin") + and not request.user.rsin + and ( + openzaak_config.fetch_eherkenning_zaken_with_rsin + or openklant_config.use_rsin_for_innNnpId_query_parameter + ) + ): + auth.logout(request) + message = _("Use DigiD to log in as a sole proprietor.") + messages.error(request, message) + failure_url = self.get_failure_url() + return HttpResponseRedirect(failure_url) + return response + + class CustomeHerkenningAssertionConsumerServiceMockView( - eHerkenningAssertionConsumerServiceMockView + BlockEenmanszaakLoginMixin, eHerkenningAssertionConsumerServiceMockView ): def get_login_url(self): """ @@ -201,3 +230,10 @@ def get_success_url(self): del session["invite_url"] return super().get_success_url() + + +class CustomEHerkenningOIDCAuthenticationCallbackView( + BlockEenmanszaakLoginMixin, eHerkenningOIDCAuthenticationCallbackView +): + def get_failure_url(self): + return settings.LOGIN_URL diff --git a/src/open_inwoner/urls.py b/src/open_inwoner/urls.py index 2f9af3dfc8..43522a1fa7 100644 --- a/src/open_inwoner/urls.py +++ b/src/open_inwoner/urls.py @@ -121,9 +121,7 @@ ), path( "eherkenning-oidc/", - include( - "digid_eherkenning_oidc_generics.eherkenning_urls", - ), + include("open_inwoner.accounts.eherkenning_urls"), ), path("login/failure/", OIDCFailureView.as_view(), name="oidc-error"), path("faq/", FAQView.as_view(), name="general_faq"),