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""" """passbook multi-factor authentication engine"""
from logging import getLogger 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.http import Http404
from django.shortcuts import get_object_or_404, redirect, reverse from django.shortcuts import get_object_or_404, redirect, reverse
from django.utils.translation import gettext as _ from django.views.generic import View
from django.views.generic import FormView, TemplateView, View
from passbook.core.forms.authentication import AuthenticationBackendFactorForm
from passbook.core.models import User 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__) 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): class MultiFactorAuthenticator(View):
"""Wizard-like Multi-factor authenticator""" """Wizard-like Multi-factor authenticator"""
SESSION_FACTOR = 'passbook_factor' SESSION_FACTOR = 'passbook_factor'
SESSION_PENDING_FACTORS = 'passbook_pending_factors' SESSION_PENDING_FACTORS = 'passbook_pending_factors'
SESSION_PENDING_USER = 'passbook_pending_user' SESSION_PENDING_USER = 'passbook_pending_user'
SESSION_USER_BACKEND = 'passbook_user_backend'
pending_user = None pending_user = None
pending_factors = [] pending_factors = []
factors = [ factors = settings.AUTHENTICATION_FACTORS.copy()
AuthenticationBackendFactor,
DummyFactor
]
_current_factor = None _current_factor = None
@ -84,55 +34,59 @@ class MultiFactorAuthenticator(View):
User, id=self.request.session[MultiFactorAuthenticator.SESSION_PENDING_USER]) User, id=self.request.session[MultiFactorAuthenticator.SESSION_PENDING_USER])
else: else:
raise Http404 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 # Write pending factors to session
if MultiFactorAuthenticator.SESSION_PENDING_FACTORS in request.session: if MultiFactorAuthenticator.SESSION_PENDING_FACTORS in request.session:
self.pending_factors = request.session[MultiFactorAuthenticator.SESSION_PENDING_FACTORS] self.pending_factors = request.session[MultiFactorAuthenticator.SESSION_PENDING_FACTORS]
else: 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 = factor(self)
self._current_factor.request = request self._current_factor.request = request
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
"""pass get request to current factor""" """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) return self._current_factor.get(request, *args, **kwargs)
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
"""pass post request to current factor""" """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) return self._current_factor.post(request, *args, **kwargs)
def user_ok(self): def user_ok(self):
"""Redirect to next Factor""" """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 next_factor = None
if self.pending_factors: if self.pending_factors:
next_factor = self.pending_factors.pop() next_factor = self.pending_factors.pop()
self.request.session[MultiFactorAuthenticator.SESSION_PENDING_FACTORS] = \ self.request.session[MultiFactorAuthenticator.SESSION_PENDING_FACTORS] = \
self.pending_factors self.pending_factors
LOGGER.debug("Next Factor is %s", next_factor) self.request.session[MultiFactorAuthenticator.SESSION_FACTOR] = next_factor
if next_factor: LOGGER.debug("Rendering Factor is %s", next_factor)
self.request.session[MultiFactorAuthenticator.SESSION_FACTOR] = next_factor.__name__ return redirect(reverse('passbook_core:mfa'))
LOGGER.debug("Rendering next factor")
return self.dispatch(self.request)
# User passed all factors # User passed all factors
LOGGER.debug("User passed all factors, logging in") LOGGER.debug("User passed all factors, logging in")
return self.user_passed() 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'))
def user_invalid(self): def user_invalid(self):
"""Show error message, user could not be authenticated""" """Show error message, user could not be authenticated"""
LOGGER.debug("User invalid") LOGGER.debug("User invalid")
# TODO: Redirect to error view # 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 = [] ALLOWED_HOSTS = []
LOGIN_URL = 'passbook_core:auth-login' LOGIN_URL = 'passbook_core:auth-login'
# CSRF_FAILURE_VIEW = 'passbook.core.views.errors.CSRFErrorView.as_view'
# Custom user model # Custom user model
AUTH_USER_MODEL = 'passbook_core.User' AUTH_USER_MODEL = 'passbook_core.User'
@ -48,6 +49,10 @@ AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend', 'django.contrib.auth.backends.ModelBackend',
'passbook.oauth_client.backends.AuthorizedServiceBackend' 'passbook.oauth_client.backends.AuthorizedServiceBackend'
] ]
AUTHENTICATION_FACTORS = [
'passbook.core.auth.backend_factor.AuthenticationBackendFactor',
'passbook.core.auth.dummy.DummyFactor',
]
# Application definition # Application definition