2020-01-21 06:35:58 +00:00
|
|
|
import base64
|
|
|
|
from io import BytesIO
|
|
|
|
|
|
|
|
import pyotp
|
|
|
|
import qrcode
|
|
|
|
from django.conf import settings
|
|
|
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
|
|
from django.contrib.auth.views import SuccessURLAllowedHostsMixin
|
|
|
|
from django.http import HttpResponseBadRequest, HttpResponseRedirect
|
|
|
|
from django.urls import reverse
|
|
|
|
from django.utils.http import is_safe_url
|
|
|
|
from django.utils.translation import gettext as _
|
|
|
|
from django.views.generic import FormView
|
|
|
|
|
|
|
|
from judge.forms import TOTPForm
|
|
|
|
from judge.utils.views import TitleMixin
|
|
|
|
|
|
|
|
|
|
|
|
class TOTPView(TitleMixin, LoginRequiredMixin, FormView):
|
|
|
|
form_class = TOTPForm
|
|
|
|
|
|
|
|
def get_form_kwargs(self):
|
|
|
|
result = super(TOTPView, self).get_form_kwargs()
|
2022-05-14 17:57:27 +00:00
|
|
|
result["totp_key"] = self.profile.totp_key
|
2020-01-21 06:35:58 +00:00
|
|
|
return result
|
|
|
|
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
|
|
if request.user.is_authenticated:
|
|
|
|
self.profile = request.profile
|
|
|
|
if self.check_skip():
|
|
|
|
return self.next_page()
|
|
|
|
return super(TOTPView, self).dispatch(request, *args, **kwargs)
|
|
|
|
|
|
|
|
def check_skip(self):
|
|
|
|
raise NotImplementedError()
|
|
|
|
|
|
|
|
def next_page(self):
|
2022-05-14 17:57:27 +00:00
|
|
|
return HttpResponseRedirect(reverse("user_edit_profile"))
|
2020-01-21 06:35:58 +00:00
|
|
|
|
|
|
|
|
|
|
|
class TOTPEnableView(TOTPView):
|
2022-05-14 17:57:27 +00:00
|
|
|
title = _("Enable Two Factor Authentication")
|
|
|
|
template_name = "registration/totp_enable.html"
|
2020-01-21 06:35:58 +00:00
|
|
|
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
|
|
profile = self.profile
|
|
|
|
if not profile.totp_key:
|
|
|
|
profile.totp_key = pyotp.random_base32(length=32)
|
|
|
|
profile.save()
|
|
|
|
return self.render_to_response(self.get_context_data())
|
|
|
|
|
|
|
|
def check_skip(self):
|
|
|
|
return self.profile.is_totp_enabled
|
|
|
|
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
|
|
if not self.profile.totp_key:
|
2022-05-14 17:57:27 +00:00
|
|
|
return HttpResponseBadRequest("No TOTP key generated on server side?")
|
2020-01-21 06:35:58 +00:00
|
|
|
return super(TOTPEnableView, self).post(request, *args, **kwargs)
|
|
|
|
|
|
|
|
def get_context_data(self, **kwargs):
|
|
|
|
context = super(TOTPEnableView, self).get_context_data(**kwargs)
|
2022-05-14 17:57:27 +00:00
|
|
|
context["totp_key"] = self.profile.totp_key
|
|
|
|
context["qr_code"] = self.render_qr_code(
|
|
|
|
self.request.user.username, self.profile.totp_key
|
|
|
|
)
|
2020-01-21 06:35:58 +00:00
|
|
|
return context
|
|
|
|
|
|
|
|
def form_valid(self, form):
|
|
|
|
self.profile.is_totp_enabled = True
|
|
|
|
self.profile.save()
|
|
|
|
# Make sure users don't get prompted to enter code right after enabling:
|
2022-05-14 17:57:27 +00:00
|
|
|
self.request.session["2fa_passed"] = True
|
2020-01-21 06:35:58 +00:00
|
|
|
return self.next_page()
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def render_qr_code(cls, username, key):
|
|
|
|
totp = pyotp.TOTP(key)
|
|
|
|
uri = totp.provisioning_uri(username, settings.SITE_NAME)
|
|
|
|
|
|
|
|
qr = qrcode.QRCode(box_size=1)
|
|
|
|
qr.add_data(uri)
|
|
|
|
qr.make(fit=True)
|
|
|
|
|
2022-05-14 17:57:27 +00:00
|
|
|
image = qr.make_image(fill_color="black", back_color="white")
|
2020-01-21 06:35:58 +00:00
|
|
|
buf = BytesIO()
|
2022-05-14 17:57:27 +00:00
|
|
|
image.save(buf, format="PNG")
|
|
|
|
return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode(
|
|
|
|
"ascii"
|
|
|
|
)
|
2020-01-21 06:35:58 +00:00
|
|
|
|
|
|
|
|
|
|
|
class TOTPDisableView(TOTPView):
|
2022-05-14 17:57:27 +00:00
|
|
|
title = _("Disable Two Factor Authentication")
|
|
|
|
template_name = "registration/totp_disable.html"
|
2020-01-21 06:35:58 +00:00
|
|
|
|
|
|
|
def check_skip(self):
|
|
|
|
if not self.profile.is_totp_enabled:
|
|
|
|
return True
|
|
|
|
return settings.DMOJ_REQUIRE_STAFF_2FA and self.request.user.is_staff
|
|
|
|
|
|
|
|
def form_valid(self, form):
|
|
|
|
self.profile.is_totp_enabled = False
|
|
|
|
self.profile.totp_key = None
|
|
|
|
self.profile.save()
|
|
|
|
return self.next_page()
|
|
|
|
|
|
|
|
|
|
|
|
class TOTPLoginView(SuccessURLAllowedHostsMixin, TOTPView):
|
2022-05-14 17:57:27 +00:00
|
|
|
title = _("Perform Two Factor Authentication")
|
|
|
|
template_name = "registration/totp_auth.html"
|
2020-01-21 06:35:58 +00:00
|
|
|
|
|
|
|
def check_skip(self):
|
2022-05-14 17:57:27 +00:00
|
|
|
return not self.profile.is_totp_enabled or self.request.session.get(
|
|
|
|
"2fa_passed", False
|
|
|
|
)
|
2020-01-21 06:35:58 +00:00
|
|
|
|
|
|
|
def next_page(self):
|
2022-05-14 17:57:27 +00:00
|
|
|
redirect_to = self.request.GET.get("next", "")
|
2020-01-21 06:35:58 +00:00
|
|
|
url_is_safe = is_safe_url(
|
|
|
|
url=redirect_to,
|
|
|
|
allowed_hosts=self.get_success_url_allowed_hosts(),
|
|
|
|
require_https=self.request.is_secure(),
|
|
|
|
)
|
2022-05-14 17:57:27 +00:00
|
|
|
return HttpResponseRedirect(
|
|
|
|
(redirect_to if url_is_safe else "") or reverse("user_page")
|
|
|
|
)
|
2020-01-21 06:35:58 +00:00
|
|
|
|
|
|
|
def form_valid(self, form):
|
2022-05-14 17:57:27 +00:00
|
|
|
self.request.session["2fa_passed"] = True
|
2020-01-21 06:35:58 +00:00
|
|
|
return self.next_page()
|