core: fix mfa, split up into multiple files, move factors to settings

This commit is contained in:
Jens Langhammer 2018-12-14 09:49:34 +01:00
parent 83ed1d857b
commit 52d1920914
5 changed files with 114 additions and 78 deletions

View File

@ -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()

View File

@ -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()

View File

@ -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)

View File

@ -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'))

View File

@ -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