diff --git a/custom_auth/tests/test_02_login_redirect.py b/custom_auth/tests/test_02_login_redirect.py new file mode 100644 index 0000000..6096b3d --- /dev/null +++ b/custom_auth/tests/test_02_login_redirect.py @@ -0,0 +1,100 @@ +from __future__ import annotations + +from django.test import TestCase, RequestFactory +from django.contrib.auth import get_user_model +from django.conf import settings +from unittest import mock + +from custom_auth.views import email_token_login +from custom_auth.utils.auth import generate_login_token + +User = get_user_model() + + +class EmailTokenLoginRedirectTest(TestCase): + """Test that email token login respects the 'next' parameter""" + + fixtures = ["registry/tests/fixtures/integration.json"] + + def setUp(self): + self.factory = RequestFactory() + self.user = User.objects.get(username="Mounfem") + + def test_redirect_with_next_parameter(self): + """Test that login redirects to the 'next' URL when provided""" + token = generate_login_token(self.user) + + # Create a POST request with a valid token and next parameter + request = self.factory.post('/auth/magic/', { + 'token': token, + 'next': '/payments/' + }) + # Set the host for URL validation (use testserver for test compatibility) + request.META['HTTP_HOST'] = 'testserver' + + with mock.patch('custom_auth.views.authenticate', return_value=self.user): + with mock.patch('custom_auth.views.login'): + response = email_token_login(request) + + # Should redirect to /payments/ + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, '/payments/') + + def test_redirect_without_next_parameter(self): + """Test that login redirects to LOGIN_REDIRECT_URL when 'next' is not provided""" + token = generate_login_token(self.user) + + # Create a POST request with a valid token but no next parameter + request = self.factory.post('/auth/magic/', { + 'token': token + }) + # Set the host for URL validation (use testserver for test compatibility) + request.META['HTTP_HOST'] = 'testserver' + + with mock.patch('custom_auth.views.authenticate', return_value=self.user): + with mock.patch('custom_auth.views.login'): + response = email_token_login(request) + + # Should redirect to LOGIN_REDIRECT_URL (default: '/') + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, settings.LOGIN_REDIRECT_URL) + + def test_redirect_with_empty_next_parameter(self): + """Test that login redirects to LOGIN_REDIRECT_URL when 'next' is empty""" + token = generate_login_token(self.user) + + # Create a POST request with a valid token and empty next parameter + request = self.factory.post('/auth/magic/', { + 'token': token, + 'next': '' + }) + # Set the host for URL validation (use testserver for test compatibility) + request.META['HTTP_HOST'] = 'testserver' + + with mock.patch('custom_auth.views.authenticate', return_value=self.user): + with mock.patch('custom_auth.views.login'): + response = email_token_login(request) + + # Should redirect to LOGIN_REDIRECT_URL (default: '/') + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, settings.LOGIN_REDIRECT_URL) + + def test_redirect_blocks_external_urls(self): + """Test that external URLs are blocked (security check)""" + token = generate_login_token(self.user) + + # Create a POST request with a valid token and external next URL + request = self.factory.post('/auth/magic/', { + 'token': token, + 'next': 'https://evil.com/phishing' + }) + # Set the host for URL validation (use testserver for test compatibility) + request.META['HTTP_HOST'] = 'testserver' + + with mock.patch('custom_auth.views.authenticate', return_value=self.user): + with mock.patch('custom_auth.views.login'): + response = email_token_login(request) + + # Should redirect to LOGIN_REDIRECT_URL, not the external URL + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, settings.LOGIN_REDIRECT_URL) diff --git a/custom_auth/views.py b/custom_auth/views.py index b750c4d..b4c6d78 100644 --- a/custom_auth/views.py +++ b/custom_auth/views.py @@ -4,9 +4,10 @@ from django.contrib.auth import authenticate, login, views from django.core.exceptions import SuspiciousOperation from django.http import HttpResponse, HttpResponseForbidden, HttpResponseRedirect -from django.shortcuts import render +from django.shortcuts import redirect, render from django.urls import reverse from django.utils.decorators import method_decorator +from django.utils.http import url_has_allowed_host_and_scheme from django.views.decorators.cache import never_cache from django.views.decorators.csrf import csrf_protect from django.views.decorators.debug import sensitive_post_parameters @@ -46,8 +47,8 @@ def email_token_login(request: HttpRequest) -> HttpResponse: if res is not None: login(request, res) - next_url = request.POST.get("next", None) - return HttpResponseRedirect(next_url) + next_url = request.POST.get("next", settings.LOGIN_REDIRECT_URL) + return redirect(next_url) else: return render(request, "auth/token_login.html", context={"error": True}) @@ -77,6 +78,8 @@ class ClientIdLoginView(views.LoginView): def get_context_data(self, **kwargs): context = super(views.LoginView, self).get_context_data(**kwargs) context["googlefail"] = self.request.GET.get("error", "") == "googlefail" + context["next"] = self.request.GET.get("next", settings.LOGIN_REDIRECT_URL) + context.update(self.extra_context) return context