diff --git a/openwisp_radius/saml/views.py b/openwisp_radius/saml/views.py index 11ffc10a..83efd589 100644 --- a/openwisp_radius/saml/views.py +++ b/openwisp_radius/saml/views.py @@ -2,12 +2,13 @@ from urllib.parse import parse_qs, quote, urlencode, urlparse import swapper +from allauth.account.models import EmailAddress from allauth.account.utils import send_email_confirmation from django import forms from django.conf import settings from django.contrib.auth import get_user_model, logout from django.contrib.auth.mixins import LoginRequiredMixin -from django.core.exceptions import ObjectDoesNotExist, PermissionDenied +from django.core.exceptions import ObjectDoesNotExist, PermissionDenied, ValidationError from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.views.generic import UpdateView @@ -78,6 +79,19 @@ def post_login_hook(self, request, user, session_info): ) registered_user.full_clean() registered_user.save() + # The user is just created, it will not have an email address + if user.email: + try: + email_address = EmailAddress( + user=user, email=user.email, primary=True, verified=True + ) + email_address.full_clean() + email_address.save() + except ValidationError: + logger.exception( + f'Failed email validation for "{user}"' + ' during SAML user creation' + ) def customize_relay_state(self, relay_state): """ diff --git a/openwisp_radius/tests/test_saml/test_views.py b/openwisp_radius/tests/test_saml/test_views.py index a4cc4801..7231d873 100644 --- a/openwisp_radius/tests/test_saml/test_views.py +++ b/openwisp_radius/tests/test_saml/test_views.py @@ -60,17 +60,20 @@ class TestAssertionConsumerServiceView(TestSamlMixin, TestCase): def _get_relay_state(self, redirect_url, org_slug): return f'{redirect_url}?org={org_slug}' - def _get_saml_response_for_acs_view(self, relay_state): + def _get_saml_response_for_acs_view(self, relay_state, uid='org_user@example.com'): response = self.client.get(self.login_url, {'RelayState': relay_state}) saml2_req = saml2_from_httpredirect_request(response.url) session_id = get_session_id_from_saml2(saml2_req) self.add_outstanding_query(session_id, relay_state) - return auth_response(session_id, 'org_user@example.com'), relay_state + return auth_response(session_id, uid), relay_state def _post_successful_auth_assertions(self, query_params, org_slug): self.assertEqual(User.objects.count(), 1) user_id = self.client.session[SESSION_KEY] user = User.objects.get(id=user_id) + self.assertEqual( + user.emailaddress_set.filter(verified=True, primary=True).count(), 1 + ) self.assertEqual(user.username, 'org_user@example.com') self.assertEqual(OrganizationUser.objects.count(), 1) org_user = OrganizationUser.objects.get(user_id=user_id) @@ -105,6 +108,28 @@ def test_organization_slug_present(self): query_params = parse_qs(urlparse(response.url).query) self._post_successful_auth_assertions(query_params, org_slug) + @capture_any_output() + def test_invalid_email_raise_validation_error(self): + invalid_email = 'invalid_email@example' + relay_state = self._get_relay_state( + redirect_url='https://captive-portal.example.com', org_slug='default' + ) + saml_response, relay_state = self._get_saml_response_for_acs_view( + relay_state, uid=invalid_email + ) + with patch('logging.Logger.exception') as mocked_logger: + self.client.post( + reverse('radius:saml2_acs'), + { + 'SAMLResponse': self.b64_for_post(saml_response), + 'RelayState': relay_state, + }, + ) + mocked_logger.assert_called_once_with( + 'Failed email validation for "invalid_email@example" during' + ' SAML user creation' + ) + @capture_any_output() def test_relay_state_relative_path(self): expected_redirect_path = '/radius/saml2/additional-info/'