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/factors/otp/views.py

167 lines
6.7 KiB
Python
Raw Normal View History

"""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
2019-02-26 11:43:59 +00:00
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
2020-02-18 21:28:47 +00:00
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext as _
from django.views import View
2020-02-18 21:28:47 +00:00
from django.views.decorators.cache import never_cache
2019-02-26 11:43:59 +00:00
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
2019-10-01 08:24:10 +00:00
from structlog import get_logger
from passbook.audit.models import Event, EventAction
2019-10-07 14:33:48 +00:00
from passbook.factors.otp.forms import OTPSetupForm
from passbook.factors.otp.utils import otpauth_url
from passbook.lib.config import CONFIG
2019-12-31 11:51:16 +00:00
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"""
2019-12-31 11:51:16 +00:00
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():
2019-12-31 11:51:16 +00:00
kwargs["static_tokens"] = StaticToken.objects.filter(
device=static.first()
).order_by("token")
totp_devices = TOTPDevice.objects.filter(user=self.request.user, confirmed=True)
2019-12-31 11:51:16 +00:00
kwargs["state"] = totp_devices.exists() and static.exists()
return kwargs
2019-02-26 11:35:24 +00:00
class DisableView(LoginRequiredMixin, View):
"""Disable TOTP for user"""
def get(self, request: HttpRequest) -> HttpResponse:
2019-02-26 11:35:24 +00:00
"""Delete all the devices for user"""
static = get_object_or_404(StaticDevice, user=request.user, confirmed=True)
2019-12-31 11:51:16 +00:00
static_tokens = StaticToken.objects.filter(device=static).order_by("token")
2019-02-26 11:35:24 +00:00
totp = TOTPDevice.objects.filter(user=request.user, confirmed=True)
static.delete()
totp.delete()
for token in static_tokens:
token.delete()
2019-12-31 11:51:16 +00:00
messages.success(request, "Successfully disabled OTP")
2019-02-26 11:35:24 +00:00
# Create event with email notification
2019-12-31 11:51:16 +00:00
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"""
2019-12-31 11:51:16 +00:00
title = _("Set up OTP")
form_class = OTPSetupForm
2019-12-31 11:51:16 +00:00
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):
2019-12-31 11:51:16 +00:00
kwargs["config"] = CONFIG.y("passbook")
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
2019-12-31 11:51:16 +00:00
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():
2019-12-31 11:51:16 +00:00
messages.error(request, _("You already have TOTP enabled!"))
del request.session[OTP_SETTING_UP_KEY]
2019-12-31 11:51:16 +00:00
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):
2019-12-31 11:51:16 +00:00
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
2019-12-31 11:51:16 +00:00
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
2019-12-31 11:51:16 +00:00
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()]
2019-12-31 11:51:16 +00:00
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]
2019-12-31 11:51:16 +00:00
Event.new(EventAction.CUSTOM, message="User enabled OTP.").from_http(
self.request
)
return redirect("passbook_factors_otp:otp-user-settings")
2020-02-18 21:28:47 +00:00
@method_decorator(never_cache, name="dispatch")
class QRView(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)
2019-12-31 11:51:16 +00:00
resp = HttpResponse(content_type="image/svg+xml; charset=utf-8")
img.save(resp)
return resp