bookwyrm/bookwyrm/views/preferences/two_factor_auth.py
2022-10-15 07:47:20 +11:00

178 lines
6.5 KiB
Python

""" class views for 2FA management """
from datetime import datetime, timedelta
import pyotp
import qrcode
import qrcode.image.svg
from django.contrib.auth import login
from django.contrib.auth.decorators import login_required
from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpResponseBadRequest
from django.template.response import TemplateResponse
from django.shortcuts import redirect
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.debug import sensitive_post_parameters
from bookwyrm import forms, models
from bookwyrm.settings import DOMAIN, TWO_FACTOR_LOGIN_MAX_SECONDS
from bookwyrm.views.helpers import set_language
# pylint: disable= no-self-use
@method_decorator(login_required, name="dispatch")
class Edit2FA(View):
"""change 2FA settings as logged in user"""
def get(self, request):
"""Two Factor auth page"""
data = {"form": forms.ConfirmPasswordForm()}
return TemplateResponse(request, "preferences/2fa.html", data)
@method_decorator(sensitive_post_parameters("password"))
def post(self, request):
"""check the user's password"""
form = forms.ConfirmPasswordForm(request.POST, instance=request.user)
if not form.is_valid():
data = {"form": form}
return TemplateResponse(request, "preferences/2fa.html", data)
qr_form = forms.Confirm2FAForm()
data = {
"password_confirmed": True,
"qrcode": self.create_qr_code(request.user),
"form": qr_form,
}
return TemplateResponse(request, "preferences/2fa.html", data)
def create_qr_code(self, user):
"""generate and save a qr code for 2FA"""
otp_secret = pyotp.random_base32()
# save the secret to the user record - we'll need it to check codes in future
user.otp_secret = otp_secret
user.save(broadcast=False, update_fields=["otp_secret"])
# now we create the qr code
provisioning_url = pyotp.totp.TOTP(otp_secret).provisioning_uri(
name=user.localname, issuer_name=DOMAIN
)
qr_code = qrcode.QRCode(image_factory=qrcode.image.svg.SvgPathImage)
qr_code.add_data(provisioning_url)
qr_code.make(fit=True)
img = qr_code.make_image(attrib={"fill": "black"})
return str(img.to_string(), "utf-8") # to_string() returns a byte string
@method_decorator(login_required, name="dispatch")
class Confirm2FA(View):
"""confirm user's 2FA settings"""
def post(self, request):
"""confirm the 2FA works before requiring it"""
form = forms.Confirm2FAForm(request.POST, instance=request.user)
if not form.is_valid():
data = {
"password_confirmed": True,
"qrcode": Edit2FA.create_qr_code(self, request.user),
"form": form,
}
return TemplateResponse(request, "preferences/2fa.html", data)
# set the user's 2FA setting on
request.user.two_factor_auth = True
request.user.save(broadcast=False, update_fields=["two_factor_auth"])
data = {"form": form, "success": True}
return TemplateResponse(request, "preferences/2fa.html", data)
@method_decorator(login_required, name="dispatch")
class Disable2FA(View):
"""Turn off 2FA on this user account"""
def get(self, request):
"""Confirmation page to turn off 2FA"""
return TemplateResponse(request, "preferences/disable-2fa.html")
def post(self, request):
"""Turn off 2FA on this user account"""
request.user.two_factor_auth = False
request.user.save(broadcast=False, update_fields=["two_factor_auth"])
data = {"form": forms.ConfirmPasswordForm(), "success": True}
return TemplateResponse(request, "preferences/2fa.html", data)
class LoginWith2FA(View):
"""Check 2FA code matches before allowing login"""
def get(self, request):
"""Display 2FA form"""
data = {"form": forms.Confirm2FAForm()}
return TemplateResponse(request, "two_factor_auth/two_factor_login.html", data)
def post(self, request):
"""Check 2FA code and allow/disallow login"""
try:
user = models.User.objects.get(username=request.session.get("2fa_user"))
except ObjectDoesNotExist:
request.session["2fa_auth_time"] = 0
return HttpResponseBadRequest("Invalid user")
session_time = (
int(request.session["2fa_auth_time"])
if request.session["2fa_auth_time"]
else 0
)
elapsed_time = datetime.now() - datetime.fromtimestamp(session_time)
form = forms.Confirm2FAForm(request.POST, instance=user)
# don't allow the login credentials to last too long before completing login
if elapsed_time > timedelta(seconds=TWO_FACTOR_LOGIN_MAX_SECONDS):
request.session["2fa_user"] = None
request.session["2fa_auth_time"] = 0
return redirect("/")
if not form.is_valid():
data = {"form": form, "2fa_user": user}
return TemplateResponse(
request, "two_factor_auth/two_factor_login.html", data
)
# log the user in - we are bypassing standard login
login(request, user)
user.update_active_date()
return set_language(user, redirect("/"))
@method_decorator(login_required, name="dispatch")
class GenerateBackupCodes(View):
"""Generate and display backup 2FA codes"""
def get(self, request):
"""Generate and display backup 2FA codes"""
data = {"backup_codes": self.generate_backup_codes(request.user)}
return TemplateResponse(request, "preferences/2fa.html", data)
def generate_backup_codes(self, user):
"""generate fresh backup codes for 2FA"""
# create fresh hotp secrets and count
hotp_secret = pyotp.random_base32()
user.hotp_count = 0
# save the secret to the user record
user.hotp_secret = hotp_secret
user.save(broadcast=False, update_fields=["hotp_count", "hotp_secret"])
# generate codes
hotp = pyotp.HOTP(hotp_secret)
counter = 0
codes = []
while counter < 10:
codes.append(hotp.at(counter))
counter = counter + 1
return codes
class Prompt2FA(View):
"""Alert user to the existence of 2FA"""
def get(self, request):
"""Alert user to the existence of 2FA"""
return TemplateResponse(request, "two_factor_auth/two_factor_prompt.html")