authentik/passbook/core/auth/mfa.py

139 lines
5.3 KiB
Python
Raw Normal View History

2018-12-13 17:02:08 +00:00
"""passbook multi-factor authentication engine"""
from logging import getLogger
from django.contrib.auth import authenticate, 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 passbook.core.forms.authentication import AuthenticationBackendFactorForm
from passbook.core.models import User
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)
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'
pending_user = None
pending_factors = []
factors = [
AuthenticationBackendFactor,
DummyFactor
]
_current_factor = None
def dispatch(self, request, *args, **kwargs):
# Extract pending user from session (only remember uid)
if MultiFactorAuthenticator.SESSION_PENDING_USER in request.session:
self.pending_user = get_object_or_404(
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._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"""
return self._current_factor.get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
"""pass post request to current factor"""
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__)
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)
# 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'))
def user_invalid(self):
"""Show error message, user could not be authenticated"""
LOGGER.debug("User invalid")
# TODO: Redirect to error view