2019-02-25 11:29:40 +00:00
|
|
|
"""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
|
2019-02-25 11:29:40 +00:00
|
|
|
from django.urls import reverse
|
|
|
|
from django.utils.translation import ugettext as _
|
|
|
|
from django.views import View
|
2019-02-26 11:43:59 +00:00
|
|
|
from django.views.generic import FormView, TemplateView
|
2019-02-25 11:29:40 +00:00
|
|
|
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
|
2019-02-25 11:29:40 +00:00
|
|
|
|
2019-10-07 14:33:48 +00:00
|
|
|
from passbook.factors.otp.forms import OTPSetupForm
|
|
|
|
from passbook.factors.otp.utils import otpauth_url
|
2019-02-25 11:29:40 +00:00
|
|
|
from passbook.lib.boilerplate import NeverCacheMixin
|
|
|
|
from passbook.lib.config import CONFIG
|
|
|
|
|
|
|
|
OTP_SESSION_KEY = 'passbook_otp_key'
|
|
|
|
OTP_SETTING_UP_KEY = 'passbook_otp_setup'
|
2019-10-04 08:08:53 +00:00
|
|
|
LOGGER = get_logger()
|
2019-02-25 11:29:40 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2019-02-26 11:35:24 +00:00
|
|
|
class DisableView(LoginRequiredMixin, View):
|
2019-02-25 11:29:40 +00:00
|
|
|
"""Disable TOTP for user"""
|
|
|
|
|
2019-02-26 11:35:24 +00:00
|
|
|
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.create(
|
|
|
|
# user=request.user,
|
|
|
|
# message=_('You disabled TOTP.'),
|
|
|
|
# current=True,
|
|
|
|
# request=request,
|
|
|
|
# send_notification=True)
|
|
|
|
return redirect(reverse('passbook_otp:otp-user-settings'))
|
2019-02-25 11:29:40 +00:00
|
|
|
|
|
|
|
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):
|
2019-09-30 16:04:04 +00:00
|
|
|
kwargs['config'] = CONFIG.y('passbook')
|
2019-02-25 11:29:40 +00:00
|
|
|
kwargs['is_login'] = True
|
|
|
|
kwargs['title'] = _('Configue 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_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
|
2019-02-25 12:02:50 +00:00
|
|
|
# TODO: Send static tokens via E-Mail
|
|
|
|
for _counter in range(0, 9):
|
2019-02-25 11:29:40 +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
|
|
|
|
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_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]
|
|
|
|
# Create event with email notification
|
|
|
|
# TODO: Create Audit Log entry
|
|
|
|
# Event.create(
|
|
|
|
# user=self.request.user,
|
|
|
|
# message=_('You activated TOTP.'),
|
|
|
|
# current=True,
|
|
|
|
# request=self.request,
|
|
|
|
# send_notification=True)
|
|
|
|
return redirect('passbook_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
|