mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-09-29 14:51:54 +00:00
173 lines
6.4 KiB
Python
173 lines
6.4 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.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"""
|
|
if "2fa_user" not in request.session:
|
|
request.session["2fa_auth_time"] = 0
|
|
return redirect("/")
|
|
user = models.User.objects.get(username=request.session["2fa_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")
|