Cloned DMOJ
This commit is contained in:
parent
f623974b58
commit
49dc9ff10c
513 changed files with 132349 additions and 39 deletions
122
judge/views/totp.py
Normal file
122
judge/views/totp.py
Normal file
|
@ -0,0 +1,122 @@
|
|||
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()
|
||||
result['totp_key'] = self.profile.totp_key
|
||||
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):
|
||||
return HttpResponseRedirect(reverse('user_edit_profile'))
|
||||
|
||||
|
||||
class TOTPEnableView(TOTPView):
|
||||
title = _('Enable Two Factor Authentication')
|
||||
template_name = 'registration/totp_enable.html'
|
||||
|
||||
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:
|
||||
return HttpResponseBadRequest('No TOTP key generated on server side?')
|
||||
return super(TOTPEnableView, self).post(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(TOTPEnableView, self).get_context_data(**kwargs)
|
||||
context['totp_key'] = self.profile.totp_key
|
||||
context['qr_code'] = self.render_qr_code(self.request.user.username, self.profile.totp_key)
|
||||
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:
|
||||
self.request.session['2fa_passed'] = True
|
||||
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)
|
||||
|
||||
image = qr.make_image(fill_color='black', back_color='white')
|
||||
buf = BytesIO()
|
||||
image.save(buf, format='PNG')
|
||||
return 'data:image/png;base64,' + base64.b64encode(buf.getvalue()).decode('ascii')
|
||||
|
||||
|
||||
class TOTPDisableView(TOTPView):
|
||||
title = _('Disable Two Factor Authentication')
|
||||
template_name = 'registration/totp_disable.html'
|
||||
|
||||
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):
|
||||
title = _('Perform Two Factor Authentication')
|
||||
template_name = 'registration/totp_auth.html'
|
||||
|
||||
def check_skip(self):
|
||||
return not self.profile.is_totp_enabled or self.request.session.get('2fa_passed', False)
|
||||
|
||||
def next_page(self):
|
||||
redirect_to = self.request.GET.get('next', '')
|
||||
url_is_safe = is_safe_url(
|
||||
url=redirect_to,
|
||||
allowed_hosts=self.get_success_url_allowed_hosts(),
|
||||
require_https=self.request.is_secure(),
|
||||
)
|
||||
return HttpResponseRedirect((redirect_to if url_is_safe else '') or reverse('user_page'))
|
||||
|
||||
def form_valid(self, form):
|
||||
self.request.session['2fa_passed'] = True
|
||||
return self.next_page()
|
Loading…
Add table
Add a link
Reference in a new issue