core: fix mfa, split up into multiple files, move factors to settings
This commit is contained in:
parent
83ed1d857b
commit
52d1920914
34
passbook/core/auth/backend_factor.py
Normal file
34
passbook/core/auth/backend_factor.py
Normal 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()
|
14
passbook/core/auth/dummy.py
Normal file
14
passbook/core/auth/dummy.py
Normal 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()
|
29
passbook/core/auth/factor.py
Normal file
29
passbook/core/auth/factor.py
Normal 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)
|
|
@ -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'))
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Reference in a new issue