From 52d192091410cc7d94a061e10183f0d854391ac4 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Fri, 14 Dec 2018 09:49:34 +0100 Subject: [PATCH] core: fix mfa, split up into multiple files, move factors to settings --- passbook/core/auth/backend_factor.py | 34 +++++++++ passbook/core/auth/dummy.py | 14 ++++ passbook/core/auth/factor.py | 29 +++++++ passbook/core/auth/mfa.py | 110 ++++++++------------------- passbook/core/settings.py | 5 ++ 5 files changed, 114 insertions(+), 78 deletions(-) create mode 100644 passbook/core/auth/backend_factor.py create mode 100644 passbook/core/auth/dummy.py create mode 100644 passbook/core/auth/factor.py diff --git a/passbook/core/auth/backend_factor.py b/passbook/core/auth/backend_factor.py new file mode 100644 index 000000000..bb7c60ed1 --- /dev/null +++ b/passbook/core/auth/backend_factor.py @@ -0,0 +1,34 @@ +"""passbook multi-factor authentication engine""" +from logging import getLogger + +from django.contrib.auth import authenticate +from django.views.generic import FormView + +from passbook.core.auth.factor import AuthenticationFactor +from passbook.core.auth.mfa import MultiFactorAuthenticator +from passbook.core.forms.authentication import AuthenticationBackendFactorForm +from passbook.lib.config import CONFIG + +LOGGER = getLogger(__name__) + + +class AuthenticationBackendFactor(FormView, AuthenticationFactor): + """Authentication factor which authenticates against django's AuthBackend""" + + form_class = AuthenticationBackendFactorForm + + def form_valid(self, form): + """Authenticate against django's authentication backend""" + uid_fields = CONFIG.y('passbook.uid_fields') + kwargs = { + 'password': form.cleaned_data.get('password'), + } + for uid_field in uid_fields: + kwargs[uid_field] = getattr(self.authenticator.pending_user, uid_field) + user = authenticate(self.request, **kwargs) + if user: + # User instance returned from authenticate() has .backend property set + self.authenticator.pending_user = user + self.request.session[MultiFactorAuthenticator.SESSION_USER_BACKEND] = user.backend + return self.authenticator.user_ok() + return self.authenticator.user_invalid() diff --git a/passbook/core/auth/dummy.py b/passbook/core/auth/dummy.py new file mode 100644 index 000000000..593916409 --- /dev/null +++ b/passbook/core/auth/dummy.py @@ -0,0 +1,14 @@ +"""passbook multi-factor authentication engine""" +from logging import getLogger + +from passbook.core.auth.factor import AuthenticationFactor + +LOGGER = getLogger(__name__) + + +class DummyFactor(AuthenticationFactor): + """Dummy factor for testing with multiple factors""" + + def post(self, request): + """Just redirect to next factor""" + return self.authenticator.user_ok() diff --git a/passbook/core/auth/factor.py b/passbook/core/auth/factor.py new file mode 100644 index 000000000..0aa1db87d --- /dev/null +++ b/passbook/core/auth/factor.py @@ -0,0 +1,29 @@ +"""passbook multi-factor authentication engine""" +from logging import getLogger + +from django.utils.translation import gettext as _ +from django.views.generic import TemplateView + +from passbook.lib.config import CONFIG + +LOGGER = getLogger(__name__) + + +class AuthenticationFactor(TemplateView): + """Abstract Authentication factor, inherits TemplateView but can be combined with FormView""" + + form = None + required = True + authenticator = None + request = None + template_name = 'login/form.html' + + def __init__(self, authenticator): + self.authenticator = authenticator + + def get_context_data(self, **kwargs): + kwargs['config'] = CONFIG.get('passbook') + kwargs['is_login'] = True + kwargs['title'] = _('Log in to your account') + kwargs['primary_action'] = _('Log in') + return super().get_context_data(**kwargs) diff --git a/passbook/core/auth/mfa.py b/passbook/core/auth/mfa.py index b49e851d5..abfce0c45 100644 --- a/passbook/core/auth/mfa.py +++ b/passbook/core/auth/mfa.py @@ -1,79 +1,29 @@ """passbook multi-factor authentication engine""" from logging import getLogger -from django.contrib.auth import authenticate, login +from django.conf import settings +from django.contrib.auth import login from django.http import Http404 from django.shortcuts import get_object_or_404, redirect, reverse -from django.utils.translation import gettext as _ -from django.views.generic import FormView, TemplateView, View +from django.views.generic import View -from passbook.core.forms.authentication import AuthenticationBackendFactorForm from passbook.core.models import User -from passbook.lib.config import CONFIG +from passbook.lib.utils.reflection import class_to_path, path_to_class LOGGER = getLogger(__name__) - -class AuthenticationFactor(TemplateView): - """Abstract Authentication factor, inherits TemplateView but can be combined with FormView""" - - form = None - required = True - authenticator = None - request = None - template_name = 'login/form.html' - - def __init__(self, authenticator): - self.authenticator = authenticator - - def get_context_data(self, **kwargs): - kwargs['config'] = CONFIG.get('passbook') - kwargs['is_login'] = True - kwargs['title'] = _('Log in to your account') - kwargs['primary_action'] = _('Log in') - return super().get_context_data(**kwargs) - -class AuthenticationBackendFactor(FormView, AuthenticationFactor): - """Authentication factor which authenticates against django's AuthBackend""" - - form_class = AuthenticationBackendFactorForm - - def form_valid(self, form): - """Authenticate against django's authentication backend""" - uid_fields = CONFIG.y('passbook.uid_fields') - kwargs = { - 'password': form.cleaned_data.get('password'), - } - for uid_field in uid_fields: - kwargs[uid_field] = getattr(self.authenticator.pending_user, uid_field) - user = authenticate(self.request, **kwargs) - if user: - # User instance returned from authenticate() has .backend property set - self.authenticator.pending_user = user - return self.authenticator.user_ok() - return self.authenticator.user_invalid() - -class DummyFactor(AuthenticationFactor): - """Dummy factor for testing with multiple factors""" - - def post(self, request): - """Just redirect to next factor""" - return self.authenticator.user_ok() - class MultiFactorAuthenticator(View): """Wizard-like Multi-factor authenticator""" SESSION_FACTOR = 'passbook_factor' SESSION_PENDING_FACTORS = 'passbook_pending_factors' SESSION_PENDING_USER = 'passbook_pending_user' + SESSION_USER_BACKEND = 'passbook_user_backend' pending_user = None pending_factors = [] - factors = [ - AuthenticationBackendFactor, - DummyFactor - ] + factors = settings.AUTHENTICATION_FACTORS.copy() _current_factor = None @@ -84,55 +34,59 @@ class MultiFactorAuthenticator(View): User, id=self.request.session[MultiFactorAuthenticator.SESSION_PENDING_USER]) else: raise Http404 - # Read and instantiate factor from session - factor = None - if MultiFactorAuthenticator.SESSION_FACTOR in request.session: - factor = next(x for x in self.factors if x.__name__ == - request.session[MultiFactorAuthenticator.SESSION_FACTOR]) - else: - factor = self.factors[0] # Write pending factors to session if MultiFactorAuthenticator.SESSION_PENDING_FACTORS in request.session: self.pending_factors = request.session[MultiFactorAuthenticator.SESSION_PENDING_FACTORS] else: - self.pending_factors = MultiFactorAuthenticator.factors.copy() + self.pending_factors = self.factors.copy() + # Read and instantiate factor from session + factor_class = None + if MultiFactorAuthenticator.SESSION_FACTOR not in request.session: + factor_class = self.pending_factors[0] + else: + factor_class = request.session[MultiFactorAuthenticator.SESSION_FACTOR] + factor = path_to_class(factor_class) self._current_factor = factor(self) self._current_factor.request = request return super().dispatch(request, *args, **kwargs) def get(self, request, *args, **kwargs): """pass get request to current factor""" + LOGGER.debug("Passing GET to %s", class_to_path(self._current_factor.__class__)) return self._current_factor.get(request, *args, **kwargs) def post(self, request, *args, **kwargs): """pass post request to current factor""" + LOGGER.debug("Passing POST to %s", class_to_path(self._current_factor.__class__)) return self._current_factor.post(request, *args, **kwargs) def user_ok(self): """Redirect to next Factor""" - LOGGER.debug("Factor %s passed", self._current_factor.__name__) + LOGGER.debug("Factor %s passed", class_to_path(self._current_factor.__class__)) + # Remove passed factor from pending factors + if class_to_path(self._current_factor.__class__) in self.pending_factors: + self.pending_factors.remove(class_to_path(self._current_factor.__class__)) next_factor = None if self.pending_factors: next_factor = self.pending_factors.pop() self.request.session[MultiFactorAuthenticator.SESSION_PENDING_FACTORS] = \ self.pending_factors - LOGGER.debug("Next Factor is %s", next_factor) - if next_factor: - self.request.session[MultiFactorAuthenticator.SESSION_FACTOR] = next_factor.__name__ - LOGGER.debug("Rendering next factor") - return self.dispatch(self.request) + self.request.session[MultiFactorAuthenticator.SESSION_FACTOR] = next_factor + LOGGER.debug("Rendering Factor is %s", next_factor) + return redirect(reverse('passbook_core:mfa')) # User passed all factors LOGGER.debug("User passed all factors, logging in") - return self.user_passed() - - def user_passed(self): - """User Successfully passed all factors""" - # user = authenticate(request=self.request, ) - login(self.request, self.pending_user) - LOGGER.debug("Logged in user %s", self.pending_user) - return redirect(reverse('passbook_core:overview')) + return self._user_passed() def user_invalid(self): """Show error message, user could not be authenticated""" LOGGER.debug("User invalid") # TODO: Redirect to error view + + def _user_passed(self): + """User Successfully passed all factors""" + # user = authenticate(request=self.request, ) + backend = self.request.session[MultiFactorAuthenticator.SESSION_USER_BACKEND] + login(self.request, self.pending_user, backend=backend) + LOGGER.debug("Logged in user %s", self.pending_user) + return redirect(reverse('passbook_core:overview')) diff --git a/passbook/core/settings.py b/passbook/core/settings.py index 8526e8b7e..fa1ebea45 100644 --- a/passbook/core/settings.py +++ b/passbook/core/settings.py @@ -36,6 +36,7 @@ INTERNAL_IPS = ['127.0.0.1'] ALLOWED_HOSTS = [] LOGIN_URL = 'passbook_core:auth-login' +# CSRF_FAILURE_VIEW = 'passbook.core.views.errors.CSRFErrorView.as_view' # Custom user model AUTH_USER_MODEL = 'passbook_core.User' @@ -48,6 +49,10 @@ AUTHENTICATION_BACKENDS = [ 'django.contrib.auth.backends.ModelBackend', 'passbook.oauth_client.backends.AuthorizedServiceBackend' ] +AUTHENTICATION_FACTORS = [ + 'passbook.core.auth.backend_factor.AuthenticationBackendFactor', + 'passbook.core.auth.dummy.DummyFactor', +] # Application definition