authentik/passbook/factors/otp/views.py

153 lines
6.5 KiB
Python

"""passbook OTP Views"""
from base64 import b32encode
from binascii import unhexlify
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import Http404, HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils.translation import ugettext as _
from django.views import View
from django.views.generic import FormView, TemplateView
from django_otp.plugins.otp_static.models import StaticDevice, StaticToken
from django_otp.plugins.otp_totp.models import TOTPDevice
from qrcode import make
from qrcode.image.svg import SvgPathImage
from structlog import get_logger
from passbook.audit.models import Event, EventAction
from passbook.factors.otp.forms import OTPSetupForm
from passbook.factors.otp.utils import otpauth_url
from passbook.lib.boilerplate import NeverCacheMixin
from passbook.lib.config import CONFIG
OTP_SESSION_KEY = 'passbook_factors_otp_key'
OTP_SETTING_UP_KEY = 'passbook_factors_otp_setup'
LOGGER = get_logger()
class UserSettingsView(LoginRequiredMixin, TemplateView):
"""View for user settings to control OTP"""
template_name = 'otp/user_settings.html'
# TODO: Check if OTP Factor exists and applies to user
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
static = StaticDevice.objects.filter(user=self.request.user, confirmed=True)
if static.exists():
kwargs['static_tokens'] = StaticToken.objects.filter(device=static.first()) \
.order_by('token')
totp_devices = TOTPDevice.objects.filter(user=self.request.user, confirmed=True)
kwargs['state'] = totp_devices.exists() and static.exists()
return kwargs
class DisableView(LoginRequiredMixin, View):
"""Disable TOTP for user"""
def get(self, request, *args, **kwargs):
"""Delete all the devices for user"""
static = get_object_or_404(StaticDevice, user=request.user, confirmed=True)
static_tokens = StaticToken.objects.filter(device=static).order_by('token')
totp = TOTPDevice.objects.filter(user=request.user, confirmed=True)
static.delete()
totp.delete()
for token in static_tokens:
token.delete()
messages.success(request, 'Successfully disabled OTP')
# Create event with email notification
Event.new(EventAction.CUSTOM, message='User disabled OTP.').from_http(request)
return redirect(reverse('passbook_factors_otp:otp-user-settings'))
class EnableView(LoginRequiredMixin, FormView):
"""View to set up OTP"""
title = _('Set up OTP')
form_class = OTPSetupForm
template_name = 'login/form.html'
totp_device = None
static_device = None
# TODO: Check if OTP Factor exists and applies to user
def get_context_data(self, **kwargs):
kwargs['config'] = CONFIG.y('passbook')
kwargs['is_login'] = True
kwargs['title'] = _('Configure OTP')
kwargs['primary_action'] = _('Setup')
return super().get_context_data(**kwargs)
def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
# Check if user has TOTP setup already
finished_totp_devices = TOTPDevice.objects.filter(user=request.user, confirmed=True)
finished_static_devices = StaticDevice.objects.filter(user=request.user, confirmed=True)
if finished_totp_devices.exists() and finished_static_devices.exists():
messages.error(request, _('You already have TOTP enabled!'))
del request.session[OTP_SETTING_UP_KEY]
return redirect('passbook_factors_otp:otp-user-settings')
request.session[OTP_SETTING_UP_KEY] = True
# Check if there's an unconfirmed device left to set up
totp_devices = TOTPDevice.objects.filter(user=request.user, confirmed=False)
if not totp_devices.exists():
# Create new TOTPDevice and save it, but not confirm it
self.totp_device = TOTPDevice(user=request.user, confirmed=False)
self.totp_device.save()
else:
self.totp_device = totp_devices.first()
# Check if we have a static device already
static_devices = StaticDevice.objects.filter(user=request.user, confirmed=False)
if not static_devices.exists():
# Create new static device and some codes
self.static_device = StaticDevice(user=request.user, confirmed=False)
self.static_device.save()
# Create 9 tokens and save them
# TODO: Send static tokens via E-Mail
for _counter in range(0, 9):
token = StaticToken(device=self.static_device, token=StaticToken.random_token())
token.save()
else:
self.static_device = static_devices.first()
# Somehow convert the generated key to base32 for the QR code
rawkey = unhexlify(self.totp_device.key.encode('ascii'))
request.session[OTP_SESSION_KEY] = b32encode(rawkey).decode("utf-8")
return super().dispatch(request, *args, **kwargs)
def get_form(self, form_class=None):
form = super().get_form(form_class=form_class)
form.device = self.totp_device
form.fields['qr_code'].initial = reverse('passbook_factors_otp:otp-qr')
tokens = [(x.token, x.token) for x in self.static_device.token_set.all()]
form.fields['tokens'].choices = tokens
return form
def form_valid(self, form):
# Save device as confirmed
LOGGER.debug("Saved OTP Devices")
self.totp_device.confirmed = True
self.totp_device.save()
self.static_device.confirmed = True
self.static_device.save()
del self.request.session[OTP_SETTING_UP_KEY]
Event.new(EventAction.CUSTOM, message='User enabled OTP.').from_http(self.request)
return redirect('passbook_factors_otp:otp-user-settings')
class QRView(NeverCacheMixin, View):
"""View returns an SVG image with the OTP token information"""
def get(self, request: HttpRequest) -> HttpResponse:
"""View returns an SVG image with the OTP token information"""
# Get the data from the session
try:
key = request.session[OTP_SESSION_KEY]
except KeyError:
raise Http404
url = otpauth_url(accountname=request.user.username, secret=key)
# Make and return QR code
img = make(url, image_factory=SvgPathImage)
resp = HttpResponse(content_type='image/svg+xml; charset=utf-8')
img.save(resp)
return resp