diff --git a/passbook/core/templates/error/400.html b/passbook/core/templates/error/400.html index 5cdbb3846..410263962 100644 --- a/passbook/core/templates/error/400.html +++ b/passbook/core/templates/error/400.html @@ -20,7 +20,7 @@
{% if message %} -

{% trans message %}

+

{% trans message %}

{% endif %} {% if 'back' in request.GET %} {% trans 'Back' %} diff --git a/passbook/factors/tests.py b/passbook/factors/tests.py index 3622321e9..3311081a3 100644 --- a/passbook/factors/tests.py +++ b/passbook/factors/tests.py @@ -38,9 +38,8 @@ class TestFactorAuthentication(TestCase): def test_unauthenticated_raw(self): """test direct call to AuthenticationView""" response = self.client.get(reverse("passbook_core:auth-process")) - # Response should be 302 since no pending user is set - self.assertEqual(response.status_code, 302) - self.assertEqual(response.url, reverse("passbook_core:auth-login")) + # Response should be 400 since no pending user is set + self.assertEqual(response.status_code, 400) def test_unauthenticated_prepared(self): """test direct call but with pending_uesr in session""" @@ -71,9 +70,8 @@ class TestFactorAuthentication(TestCase): """Test with already logged in user""" self.client.force_login(self.user) response = self.client.get(reverse("passbook_core:auth-process")) - # Response should be 302 since no pending user is set - self.assertEqual(response.status_code, 302) - self.assertEqual(response.url, reverse("passbook_core:overview")) + # Response should be 400 since no pending user is set + self.assertEqual(response.status_code, 400) self.client.logout() def test_unauthenticated_post(self): diff --git a/passbook/factors/view.py b/passbook/factors/view.py index b3546c216..e3702a5a0 100644 --- a/passbook/factors/view.py +++ b/passbook/factors/view.py @@ -1,15 +1,17 @@ """passbook multi-factor authentication engine""" -from typing import List, Tuple +from typing import List, Optional, Tuple from django.contrib.auth import login from django.contrib.auth.mixins import UserPassesTestMixin -from django.shortcuts import get_object_or_404, redirect, reverse +from django.http import HttpRequest, HttpResponse +from django.shortcuts import get_object_or_404, redirect, render, reverse from django.utils.http import urlencode from django.views.generic import View from structlog import get_logger from passbook.core.models import Factor, User from passbook.core.views.utils import PermissionDeniedView +from passbook.lib.config import CONFIG from passbook.lib.utils.reflection import class_to_path, path_to_class from passbook.lib.utils.urls import is_url_absolute from passbook.policies.engine import PolicyEngine @@ -44,10 +46,26 @@ class AuthenticationView(UserPassesTestMixin, View): current_factor: Factor # Allow only not authenticated users to login - def test_func(self): + def test_func(self) -> bool: return AuthenticationView.SESSION_PENDING_USER in self.request.session - def handle_no_permission(self): + def _check_config_domain(self) -> Optional[HttpResponse]: + """Checks if current request's domain matches configured Domain, and + adds a warning if not.""" + current_domain = self.request.get_host() + config_domain = CONFIG.y("domain") + if current_domain != config_domain: + message = ( + f"Current domain of '{current_domain}' doesn't " + f"match configured domain of '{config_domain}'." + ) + LOGGER.warning(message) + return render( + self.request, "error/400.html", context={"message": message}, status=400 + ) + return None + + def handle_no_permission(self) -> HttpResponse: # Function from UserPassesTestMixin if NEXT_ARG_NAME in self.request.GET: return redirect(self.request.GET.get(NEXT_ARG_NAME)) @@ -55,7 +73,7 @@ class AuthenticationView(UserPassesTestMixin, View): return _redirect_with_qs("passbook_core:overview", self.request.GET) return _redirect_with_qs("passbook_core:auth-login", self.request.GET) - def get_pending_factors(self): + def get_pending_factors(self) -> List[Tuple[str, str]]: """Loading pending factors from Database or load from session variable""" # Write pending factors to session if AuthenticationView.SESSION_PENDING_FACTORS in self.request.session: @@ -67,6 +85,7 @@ class AuthenticationView(UserPassesTestMixin, View): ) pending_factors = [] for factor in _all_factors: + factor: Factor LOGGER.debug( "Checking if factor applies to user", factor=factor, @@ -81,10 +100,13 @@ class AuthenticationView(UserPassesTestMixin, View): LOGGER.debug("Factor applies", factor=factor, user=self.pending_user) return pending_factors - def dispatch(self, request, *args, **kwargs): + def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: # Check if user passes test (i.e. SESSION_PENDING_USER is set) user_test_result = self.get_test_func()() if not user_test_result: + incorrect_domain_message = self._check_config_domain() + if incorrect_domain_message: + return incorrect_domain_message return self.handle_no_permission() # Extract pending user from session (only remember uid) self.pending_user = get_object_or_404( @@ -117,7 +139,7 @@ class AuthenticationView(UserPassesTestMixin, View): self._current_factor_class.request = request return super().dispatch(request, *args, **kwargs) - def get(self, request, *args, **kwargs): + def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: """pass get request to current factor""" LOGGER.debug( "Passing GET", @@ -125,7 +147,7 @@ class AuthenticationView(UserPassesTestMixin, View): ) return self._current_factor_class.get(request, *args, **kwargs) - def post(self, request, *args, **kwargs): + def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: """pass post request to current factor""" LOGGER.debug( "Passing POST", @@ -133,7 +155,7 @@ class AuthenticationView(UserPassesTestMixin, View): ) return self._current_factor_class.post(request, *args, **kwargs) - def user_ok(self): + def user_ok(self) -> HttpResponse: """Redirect to next Factor""" LOGGER.debug( "Factor passed", @@ -160,14 +182,14 @@ class AuthenticationView(UserPassesTestMixin, View): LOGGER.debug("User passed all factors, logging in", user=self.pending_user) return self._user_passed() - def user_invalid(self): + def user_invalid(self) -> HttpResponse: """Show error message, user cannot login. This should only be shown if user authenticated successfully, but is disabled/locked/etc""" LOGGER.debug("User invalid") self.cleanup() return _redirect_with_qs("passbook_core:auth-denied", self.request.GET) - def _user_passed(self): + def _user_passed(self) -> HttpResponse: """User Successfully passed all factors""" backend = self.request.session[AuthenticationView.SESSION_USER_BACKEND] login(self.request, self.pending_user, backend=backend)