"""passbook OTP Views""" from base64 import b32encode from binascii import unhexlify from logging import getLogger from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin from django.http import Http404, HttpRequest, HttpResponse from django.shortcuts import 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 passbook.lib.boilerplate import NeverCacheMixin from passbook.lib.config import CONFIG from passbook.otp.forms import OTPSetupForm from passbook.otp.utils import otpauth_url OTP_SESSION_KEY = 'passbook_otp_key' OTP_SETTING_UP_KEY = 'passbook_otp_setup' LOGGER = getLogger(__name__) 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, TemplateView): """Disable TOTP for user""" # TODO: Use Django DeleteView with custom delete? # def # # 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 TOTP') # # 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_core:overview')) 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.get('passbook') 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 # 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_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