rewrite PasswordFactor to use backends setting instead of trying all backends

This commit is contained in:
Jens Langhammer 2019-03-10 21:47:08 +01:00
parent ad8125ac1c
commit 501fed1922
5 changed files with 53 additions and 13 deletions

View file

@ -1,8 +1,10 @@
"""passbook multi-factor authentication engine""" """passbook multi-factor authentication engine"""
from inspect import Signature
from logging import getLogger from logging import getLogger
from django.contrib import messages 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.core.exceptions import PermissionDenied
from django.forms.utils import ErrorList from django.forms.utils import ErrorList
from django.shortcuts import redirect, reverse 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.models import Nonce
from passbook.core.tasks import send_email from passbook.core.tasks import send_email
from passbook.lib.config import CONFIG from passbook.lib.config import CONFIG
from passbook.lib.utils.reflection import path_to_class
LOGGER = getLogger(__name__) 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): class PasswordFactor(FormView, AuthenticationFactor):
"""Authentication factor which authenticates against django's AuthBackend""" """Authentication factor which authenticates against django's AuthBackend"""
@ -57,7 +89,7 @@ class PasswordFactor(FormView, AuthenticationFactor):
for uid_field in uid_fields: for uid_field in uid_fields:
kwargs[uid_field] = getattr(self.authenticator.pending_user, uid_field) kwargs[uid_field] = getattr(self.authenticator.pending_user, uid_field)
try: try:
user = authenticate(self.request, **kwargs) user = authenticate(self.request, self.authenticator.current_factor.backends, **kwargs)
if user: if user:
# User instance returned from authenticate() has .backend property set # User instance returned from authenticate() has .backend property set
self.authenticator.pending_user = user self.authenticator.pending_user = user

View file

@ -1,13 +1,20 @@
"""passbook administration forms""" """passbook administration forms"""
from django import forms from django import forms
from django.conf import settings
from django.contrib.admin.widgets import FilteredSelectMultiple from django.contrib.admin.widgets import FilteredSelectMultiple
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from passbook.core.models import DummyFactor, PasswordFactor 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'] 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): class PasswordFactorForm(forms.ModelForm):
"""Form to create/edit Password Factors""" """Form to create/edit Password Factors"""
@ -18,10 +25,9 @@ class PasswordFactorForm(forms.ModelForm):
widgets = { widgets = {
'name': forms.TextInput(), 'name': forms.TextInput(),
'order': forms.NumberInput(), 'order': forms.NumberInput(),
'policies': FilteredSelectMultiple(_('policies'), False) 'policies': FilteredSelectMultiple(_('policies'), False),
} 'backends': FilteredSelectMultiple(_('backends'), False,
field_classes = { choices=get_authentication_backends())
'backends': DynamicArrayField
} }
class DummyFactorForm(forms.ModelForm): class DummyFactorForm(forms.ModelForm):

View file

@ -11,7 +11,7 @@ def create_initial_factor(apps, schema_editor):
name='password', name='password',
slug='password', slug='password',
order=0, order=0,
backends=[] backends=['django.contrib.auth.backends.ModelBackend']
) )
class Migration(migrations.Migration): class Migration(migrations.Migration):

View file

@ -47,8 +47,7 @@ SESSION_COOKIE_NAME = 'passbook_session'
LANGUAGE_COOKIE_NAME = 'passbook_language' LANGUAGE_COOKIE_NAME = 'passbook_language'
AUTHENTICATION_BACKENDS = [ AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend', 'django.contrib.auth.backends.ModelBackend'
'passbook.oauth_client.backends.AuthorizedServiceBackend'
] ]
# Application definition # Application definition

View file

@ -18,9 +18,12 @@ class TestFactorAuthentication(TestCase):
super().setUp() super().setUp()
self.password = ''.join(SystemRandom().choice( self.password = ''.join(SystemRandom().choice(
string.ascii_uppercase + string.digits) for _ in range(8)) string.ascii_uppercase + string.digits) for _ in range(8))
self.factor, _ = PasswordFactor.objects.get_or_create(name='password', self.factor, _ = PasswordFactor.objects.get_or_create(slug='password', defaults={
slug='password', 'name': 'password',
backends=[]) 'slug': 'password',
'order': 0,
'backends': ['django.contrib.auth.backends.ModelBackend']
})
self.user = User.objects.create_user(username='test', self.user = User.objects.create_user(username='test',
email='test@test.test', email='test@test.test',
password=self.password) password=self.password)