This repository has been archived on 2024-05-31. You can view files and clone it, but cannot push or open issues or pull requests.
authentik/passbook/core/views/authentication.py

254 lines
9.9 KiB
Python
Raw Normal View History

"""passbook core authentication views"""
from typing import Dict, Optional
2018-11-11 12:41:48 +00:00
from django.contrib import messages
from django.contrib.auth import login, logout
2018-11-23 08:44:30 +00:00
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.forms.utils import ErrorList
2018-11-11 12:41:48 +00:00
from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, redirect, reverse
2018-11-11 12:41:48 +00:00
from django.utils.translation import ugettext as _
2018-11-23 08:44:30 +00:00
from django.views import View
2018-11-11 12:41:48 +00:00
from django.views.generic import FormView
2019-10-01 08:24:10 +00:00
from structlog import get_logger
2018-11-11 12:41:48 +00:00
from passbook.core.forms.authentication import LoginForm, SignUpForm
from passbook.core.models import Invitation, Nonce, Source, User
2018-12-10 13:21:42 +00:00
from passbook.core.signals import invitation_used, user_signed_up
2019-10-07 14:33:48 +00:00
from passbook.factors.password.exceptions import PasswordPolicyInvalid
from passbook.factors.view import AuthenticationView, _redirect_with_qs
2018-11-11 12:41:48 +00:00
from passbook.lib.config import CONFIG
LOGGER = get_logger()
2018-11-11 12:41:48 +00:00
2018-11-11 12:41:48 +00:00
class LoginView(UserPassesTestMixin, FormView):
"""Allow users to sign in"""
template_name = 'login/form.html'
form_class = LoginForm
success_url = '.'
# Allow only not authenticated users to login
def test_func(self):
2018-12-09 20:07:38 +00:00
return self.request.user.is_authenticated is False
def handle_no_permission(self):
if 'next' in self.request.GET:
return redirect(self.request.GET.get('next'))
return redirect(reverse('passbook_core:overview'))
2018-11-11 12:41:48 +00:00
def get_context_data(self, **kwargs):
2019-09-30 16:04:04 +00:00
kwargs['config'] = CONFIG.y('passbook')
2018-11-11 12:41:48 +00:00
kwargs['is_login'] = True
kwargs['title'] = _('Log in to your account')
kwargs['primary_action'] = _('Log in')
kwargs['show_sign_up_notice'] = CONFIG.y('passbook.sign_up.enabled')
kwargs['sources'] = []
sources = Source.objects.filter(enabled=True).select_subclasses()
if any(source.is_link for source in sources):
for source in sources:
kwargs['sources'].append(source.get_login_button)
self.template_name = 'login/with_sources.html'
2018-11-11 12:41:48 +00:00
return super().get_context_data(**kwargs)
def get_user(self, uid_value) -> Optional[User]:
2018-11-11 12:41:48 +00:00
"""Find user instance. Returns None if no user was found."""
for search_field in CONFIG.y('passbook.uid_fields'):
2019-02-25 18:43:33 +00:00
# Workaround for E-Mail -> email
if search_field == 'e-mail':
search_field = 'email'
2018-11-11 12:41:48 +00:00
users = User.objects.filter(**{search_field: uid_value})
if users.exists():
LOGGER.debug("Found user", user=users.first(), uid_field=search_field)
2018-11-11 12:41:48 +00:00
return users.first()
return None
def form_valid(self, form: LoginForm) -> HttpResponse:
"""Form data is valid"""
pre_user = self.get_user(form.cleaned_data.get('uid_field'))
if not pre_user:
# No user found
2018-11-23 08:44:30 +00:00
return self.invalid_login(self.request)
2019-06-25 15:57:25 +00:00
# self.request.session.flush()
2019-02-16 08:52:37 +00:00
self.request.session[AuthenticationView.SESSION_PENDING_USER] = pre_user.pk
return _redirect_with_qs('passbook_core:auth-process', self.request.GET)
2018-11-11 12:41:48 +00:00
2018-11-23 08:44:30 +00:00
def invalid_login(self, request: HttpRequest, disabled_user: User = None) -> HttpResponse:
2018-11-11 12:41:48 +00:00
"""Handle login for disabled users/invalid login attempts"""
2018-11-23 08:44:30 +00:00
messages.error(request, _('Failed to authenticate.'))
return self.render_to_response(self.get_context_data())
2018-11-23 08:44:30 +00:00
class LogoutView(LoginRequiredMixin, View):
"""Log current user out"""
def dispatch(self, request):
"""Log current user out"""
logout(request)
messages.success(request, _("You've successfully been logged out."))
return redirect(reverse('passbook_core:auth-login'))
class SignUpView(UserPassesTestMixin, FormView):
2018-12-10 13:21:42 +00:00
"""Sign up new user, optionally consume one-use invitation link."""
template_name = 'login/form.html'
form_class = SignUpForm
success_url = '.'
# Invitation instance, if invitation link was used
2018-12-10 13:21:42 +00:00
_invitation = None
# Instance of newly created user
_user = None
# Allow only not authenticated users to login
def test_func(self):
return self.request.user.is_authenticated is False
def handle_no_permission(self):
return redirect(reverse('passbook_core:overview'))
def dispatch(self, request, *args, **kwargs):
2018-12-10 13:21:42 +00:00
"""Check if sign-up is enabled or invitation link given"""
allowed = False
2018-12-10 13:21:42 +00:00
if 'invitation' in request.GET:
2018-12-10 13:49:15 +00:00
invitations = Invitation.objects.filter(uuid=request.GET.get('invitation'))
2018-12-10 13:21:42 +00:00
allowed = invitations.exists()
if allowed:
2018-12-10 13:21:42 +00:00
self._invitation = invitations.first()
if CONFIG.y('passbook.sign_up.enabled'):
allowed = True
if not allowed:
messages.error(request, _('Sign-ups are currently disabled.'))
return redirect(reverse('passbook_core:auth-login'))
return super().dispatch(request, *args, **kwargs)
2018-12-10 13:49:15 +00:00
def get_initial(self):
if self._invitation:
initial = {}
if self._invitation.fixed_username:
initial['username'] = self._invitation.fixed_username
if self._invitation.fixed_email:
initial['email'] = self._invitation.fixed_email
2018-12-10 13:49:15 +00:00
return initial
return super().get_initial()
def get_context_data(self, **kwargs):
2019-09-30 16:04:04 +00:00
kwargs['config'] = CONFIG.y('passbook')
kwargs['is_login'] = True
kwargs['title'] = _('Sign Up')
kwargs['primary_action'] = _('Sign up')
return super().get_context_data(**kwargs)
def form_valid(self, form: SignUpForm) -> HttpResponse:
"""Create user"""
try:
self._user = SignUpView.create_user(form.cleaned_data, self.request)
except PasswordPolicyInvalid as exc:
# Manually inject error into form
# pylint: disable=protected-access
errors = form._errors.setdefault("password", ErrorList())
for error in exc.messages:
errors.append(error)
return self.form_invalid(form)
# needs_confirmation = True
# if self._invitation and not self._invitation.needs_confirmation:
# needs_confirmation = False
# if needs_confirmation:
# nonce = Nonce.objects.create(user=self._user)
# LOGGER.debug(str(nonce.uuid))
# # Send email to user
# send_email.delay(self._user.email, _('Confirm your account.'),
# 'email/account_confirm.html', {
# 'url': self.request.build_absolute_uri(
# reverse('passbook_core:auth-sign-up-confirm', kwargs={
# 'nonce': nonce.uuid
# })
# )
# })
# self._user.is_active = False
# self._user.save()
2018-12-10 13:21:42 +00:00
self.consume_invitation()
messages.success(self.request, _("Successfully signed up!"))
LOGGER.debug("Successfully signed up %s",
form.cleaned_data.get('email'))
return redirect(reverse('passbook_core:auth-login'))
2018-12-10 13:21:42 +00:00
def consume_invitation(self):
"""Consume invitation if an invitation was used"""
if self._invitation:
invitation_used.send(
sender=self,
request=self.request,
2018-12-10 13:21:42 +00:00
invitation=self._invitation,
user=self._user)
2018-12-10 13:21:42 +00:00
self._invitation.delete()
@staticmethod
def create_user(data: Dict, request: HttpRequest = None) -> User:
"""Create user from data
Args:
2018-12-10 13:26:10 +00:00
data: Dictionary as returned by SignUpForm's cleaned_data
request: Optional current request.
Returns:
The user created
Raises:
PasswordPolicyInvalid: if any policy are not fulfilled.
This also deletes the created user.
"""
# Create user
new_user = User.objects.create(
username=data.get('username'),
email=data.get('email'),
name=data.get('name'),
)
new_user.is_active = True
try:
new_user.set_password(data.get('password'))
new_user.save()
request.user = new_user
# Send signal for other auth sources
user_signed_up.send(
sender=SignUpView,
user=new_user,
request=request)
return new_user
except PasswordPolicyInvalid as exc:
new_user.delete()
raise exc
class SignUpConfirmView(View):
"""Confirm registration from Nonce"""
def get(self, request, nonce):
"""Verify UUID and activate user"""
nonce = get_object_or_404(Nonce, uuid=nonce)
nonce.user.is_active = True
nonce.user.save()
# Workaround: hardcoded reference to ModelBackend, needs testing
nonce.user.backend = 'django.contrib.auth.backends.ModelBackend'
login(request, nonce.user)
nonce.delete()
messages.success(request, _('Successfully confirmed registration.'))
return redirect('passbook_core:overview')
class PasswordResetView(View):
"""Temporarily authenticate User and allow them to reset their password"""
def get(self, request, nonce):
"""Authenticate user with nonce and redirect to password change view"""
# 3. (Optional) Trap user in password change view
nonce = get_object_or_404(Nonce, uuid=nonce)
# Workaround: hardcoded reference to ModelBackend, needs testing
nonce.user.backend = 'django.contrib.auth.backends.ModelBackend'
login(request, nonce.user)
nonce.delete()
messages.success(request, _(('Temporarily authenticated with Nonce, '
'please change your password')))
return redirect('passbook_core:user-change-password')