From 501fed19229e6cadc20a27baf7b1826ab585350d Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Sun, 10 Mar 2019 21:47:08 +0100 Subject: [PATCH] rewrite PasswordFactor to use backends setting instead of trying all backends --- passbook/core/auth/factors/password.py | 36 +++++++++++++++++-- passbook/core/forms/factors.py | 16 ++++++--- .../migrations/0014_auto_20190226_0850.py | 2 +- passbook/core/settings.py | 3 +- passbook/core/tests/test_auth_view.py | 9 +++-- 5 files changed, 53 insertions(+), 13 deletions(-) diff --git a/passbook/core/auth/factors/password.py b/passbook/core/auth/factors/password.py index d8956d377..1be36d663 100644 --- a/passbook/core/auth/factors/password.py +++ b/passbook/core/auth/factors/password.py @@ -1,8 +1,10 @@ """passbook multi-factor authentication engine""" +from inspect import Signature from logging import getLogger from django.contrib import messages -from django.contrib.auth import authenticate +from django.contrib.auth import _clean_credentials +from django.contrib.auth.signals import user_login_failed from django.core.exceptions import PermissionDenied from django.forms.utils import ErrorList from django.shortcuts import redirect, reverse @@ -15,10 +17,40 @@ from passbook.core.forms.authentication import PasswordFactorForm from passbook.core.models import Nonce from passbook.core.tasks import send_email from passbook.lib.config import CONFIG +from passbook.lib.utils.reflection import path_to_class LOGGER = getLogger(__name__) +def authenticate(request, backends, **credentials): + """If the given credentials are valid, return a User object. + Customized version of django's authenticate, which accepts a list of backends""" + for backend_path in backends: + backend = path_to_class(backend_path)() + try: + signature = Signature.from_callable(backend.authenticate) + signature.bind(request, **credentials) + except TypeError: + LOGGER.debug("Backend %s doesn't accept our arguments", backend) + # This backend doesn't accept these credentials as arguments. Try the next one. + continue + LOGGER.debug('Attempting authentication with %s...', backend) + try: + user = backend.authenticate(request, **credentials) + except PermissionDenied: + LOGGER.debug('Backend %r threw PermissionDenied', backend) + # This backend says to stop in our tracks - this user should not be allowed in at all. + break + if user is None: + continue + # Annotate the user object with the path of the backend. + user.backend = backend_path + return user + + # The credentials supplied are invalid to all backends, fire signal + user_login_failed.send(sender=__name__, credentials=_clean_credentials( + credentials), request=request) + class PasswordFactor(FormView, AuthenticationFactor): """Authentication factor which authenticates against django's AuthBackend""" @@ -57,7 +89,7 @@ class PasswordFactor(FormView, AuthenticationFactor): for uid_field in uid_fields: kwargs[uid_field] = getattr(self.authenticator.pending_user, uid_field) try: - user = authenticate(self.request, **kwargs) + user = authenticate(self.request, self.authenticator.current_factor.backends, **kwargs) if user: # User instance returned from authenticate() has .backend property set self.authenticator.pending_user = user diff --git a/passbook/core/forms/factors.py b/passbook/core/forms/factors.py index 79d52d0ed..3a5b14b4d 100644 --- a/passbook/core/forms/factors.py +++ b/passbook/core/forms/factors.py @@ -1,13 +1,20 @@ """passbook administration forms""" from django import forms +from django.conf import settings from django.contrib.admin.widgets import FilteredSelectMultiple from django.utils.translation import gettext as _ from passbook.core.models import DummyFactor, PasswordFactor -from passbook.lib.fields import DynamicArrayField +from passbook.lib.utils.reflection import path_to_class GENERAL_FIELDS = ['name', 'slug', 'order', 'policies', 'enabled'] +def get_authentication_backends(): + """Return all available authentication backends as tuple set""" + for backend in settings.AUTHENTICATION_BACKENDS: + klass = path_to_class(backend) + yield backend, getattr(klass(), 'name', '%s (%s)' % (klass.__name__, klass.__module__)) + class PasswordFactorForm(forms.ModelForm): """Form to create/edit Password Factors""" @@ -18,10 +25,9 @@ class PasswordFactorForm(forms.ModelForm): widgets = { 'name': forms.TextInput(), 'order': forms.NumberInput(), - 'policies': FilteredSelectMultiple(_('policies'), False) - } - field_classes = { - 'backends': DynamicArrayField + 'policies': FilteredSelectMultiple(_('policies'), False), + 'backends': FilteredSelectMultiple(_('backends'), False, + choices=get_authentication_backends()) } class DummyFactorForm(forms.ModelForm): diff --git a/passbook/core/migrations/0014_auto_20190226_0850.py b/passbook/core/migrations/0014_auto_20190226_0850.py index b5b8bb709..1b9ed8c68 100644 --- a/passbook/core/migrations/0014_auto_20190226_0850.py +++ b/passbook/core/migrations/0014_auto_20190226_0850.py @@ -11,7 +11,7 @@ def create_initial_factor(apps, schema_editor): name='password', slug='password', order=0, - backends=[] + backends=['django.contrib.auth.backends.ModelBackend'] ) class Migration(migrations.Migration): diff --git a/passbook/core/settings.py b/passbook/core/settings.py index 495dd9282..e05247a3b 100644 --- a/passbook/core/settings.py +++ b/passbook/core/settings.py @@ -47,8 +47,7 @@ SESSION_COOKIE_NAME = 'passbook_session' LANGUAGE_COOKIE_NAME = 'passbook_language' AUTHENTICATION_BACKENDS = [ - 'django.contrib.auth.backends.ModelBackend', - 'passbook.oauth_client.backends.AuthorizedServiceBackend' + 'django.contrib.auth.backends.ModelBackend' ] # Application definition diff --git a/passbook/core/tests/test_auth_view.py b/passbook/core/tests/test_auth_view.py index 573c214c8..97ab425ff 100644 --- a/passbook/core/tests/test_auth_view.py +++ b/passbook/core/tests/test_auth_view.py @@ -18,9 +18,12 @@ class TestFactorAuthentication(TestCase): super().setUp() self.password = ''.join(SystemRandom().choice( string.ascii_uppercase + string.digits) for _ in range(8)) - self.factor, _ = PasswordFactor.objects.get_or_create(name='password', - slug='password', - backends=[]) + self.factor, _ = PasswordFactor.objects.get_or_create(slug='password', defaults={ + 'name': 'password', + 'slug': 'password', + 'order': 0, + 'backends': ['django.contrib.auth.backends.ModelBackend'] + }) self.user = User.objects.create_user(username='test', email='test@test.test', password=self.password)