expephalon/core/views/auth.py

213 lines
8.9 KiB
Python
Raw Normal View History

from django.conf import settings
2020-05-23 16:48:37 +00:00
from django.views.generic import FormView, View, TemplateView
from django.contrib.auth import authenticate, login, logout, get_user_model
from django.shortcuts import redirect
from django.core.exceptions import PermissionDenied
from django.contrib import messages
from django.utils import timezone
from core.forms import LoginForm, OTPSelectorForm, OTPVerificationForm, PWResetForm, PWRequestForm
2020-05-23 16:48:37 +00:00
from core.models.auth import LoginSession, PWResetToken, IPLimit, LoginLog
from core.helpers.otp import get_user_otps, get_otp_choices, get_otp_by_name
2020-05-21 12:54:59 +00:00
from core.helpers.mail import simple_send_mail
2020-05-23 13:30:21 +00:00
from core.helpers.auth import generate_pwreset_mail, login_fail, login_success
2020-05-23 16:48:37 +00:00
from core.helpers.request import get_client_ip
from dbsettings.functions import getValue
2020-05-23 16:48:37 +00:00
class RateLimitedView(TemplateView):
template_name = f"{settings.EXPEPHALON_BACKEND}/auth/ratelimit.html"
def dispatch(self, request, *args, **kwargs):
for iplimit in list(IPLimit.objects.filter(ip=get_client_ip(request))):
if iplimit.end >= timezone.now():
messages.error(request, f"Sorry, there have been to many failed login attempts from your IP. Please try again after {str(iplimit.end)}, or contact support if you need help getting into your account.")
return super().dispatch(request, *args, **kwargs)
return redirect("login")
2020-05-23 16:48:37 +00:00
class AuthView(FormView):
def dispatch(self, request, *args, **kwargs):
limits = list(IPLimit.objects.filter(ip=get_client_ip(request)))
for limit in limits:
if limit.end > timezone.now():
return redirect("ratelimited")
period = timezone.now() - timezone.timedelta(seconds=int(getValue("core.auth.ratelimit.period", 600)))
failures = LoginLog.objects.filter(ip=get_client_ip(request), success=False, timestamp__gte=period)
if len(failures) >= int(getValue("core.auth.ratelimit.attempts", 5)):
IPLimit.objects.create(ip=get_client_ip(request))
return redirect("ratelimited")
2020-05-23 16:48:37 +00:00
return super().dispatch(request, *args, **kwargs)
class LoginView(AuthView):
2020-04-15 20:43:40 +00:00
template_name = f"{settings.EXPEPHALON_BACKEND}/auth/login.html"
form_class = LoginForm
def get(self, request, *args, **kwargs):
if request.user.is_authenticated:
return redirect(request.GET.get("next", "dashboard"))
return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Login"
return context
2020-05-23 13:30:21 +00:00
def form_invalid(self, form):
try:
2020-05-23 16:48:37 +00:00
user = get_user_model().objects.get(username=form.cleaned_data.get("email", ""))
2020-05-23 13:30:21 +00:00
except get_user_model().DoesNotExist:
user = None
login_fail(self.request, user, "The credentials you entered are invalid. Please try again.")
return super().form_invalid(form)
def form_valid(self, form):
2020-05-22 17:10:36 +00:00
user = authenticate(username=form.cleaned_data['email'], password=form.cleaned_data['password'])
if user:
if not get_user_otps(user):
login(self.request, user)
return redirect("dashboard")
session = LoginSession.objects.create(user=user)
self.request.session["otpsession"] = str(session.uuid)
self.request.session["next"] = self.request.GET.get("next", "dashboard")
return redirect("otpselector")
2020-05-23 13:30:21 +00:00
return self.form_invalid(form)
2020-05-23 16:48:37 +00:00
class OTPSelectorView(AuthView):
2020-04-15 20:43:40 +00:00
template_name = f"{settings.EXPEPHALON_BACKEND}/auth/otp_selector.html"
form_class = OTPSelectorForm
def clean_session(self):
for key in ("otpsession", "otpprovider", "next"):
try:
del self.request.session[key]
except:
pass
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
try:
assert self.request.session["otpsession"]
except:
raise PermissionDenied()
user = LoginSession.objects.get(uuid=self.request.session["otpsession"]).user
kwargs["otp_choices"] = get_otp_choices(user)
return kwargs
def form_valid(self, form):
self.request.session["otpprovider"] = form.cleaned_data["provider"]
return redirect("otpvalidator")
def form_invalid(self, form):
self.clean_session()
2020-05-23 13:30:21 +00:00
messages.error("Something went wrong selecting the OTP provider. Please try again.")
return redirect("login")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Two-Factor Authentication"
2020-05-23 10:04:09 +00:00
user = LoginSession.objects.get(uuid=self.request.session["otpsession"]).user
context["first_name"] = user.profile.get_internal_name
return context
2020-05-23 16:48:37 +00:00
class OTPValidatorView(AuthView):
2020-04-15 20:43:40 +00:00
template_name = f"{settings.EXPEPHALON_BACKEND}/auth/otp_verifier.html"
form_class = OTPVerificationForm
def clean_session(self):
for key in ("otpsession", "otpprovider", "next"):
try:
del self.request.session[key]
except:
pass
def validate_session(self, request):
try:
assert request.session["otpsession"]
assert request.session["otpprovider"]
user = LoginSession.objects.get(uuid=request.session["otpsession"]).user
assert request.session["otpprovider"] in get_user_otps(user).keys()
provider = get_otp_by_name(request.session["otpprovider"])()
return user, provider
except:
self.clean_session()
raise PermissionDenied()
def get(self, request, *args, **kwargs):
user, provider = self.validate_session(request)
response = provider.start_authentication(user)
messages.info(request, response)
return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
self.validate_session(request)
return super().post(request, *args, **kwargs)
def form_invalid(self, form):
2020-05-23 13:30:21 +00:00
user, provider = self.validate_session(self.request)
self.clean_session()
2020-05-23 13:30:21 +00:00
login_fail(self.request, user, "Incorrect token entered. Please try again. If the issue persists, contact support to regain access to your account.")
return redirect("login")
def form_valid(self, form):
user, provider = self.validate_session(self.request)
if provider.validate_token(user, form.cleaned_data["token"]):
login(self.request, user)
ret = redirect(self.request.session.get("next", "dashboard"))
self.clean_session()
return ret
2020-05-23 13:30:21 +00:00
return self.form_invalid(form)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Two-Factor Authentication"
2020-05-23 10:04:09 +00:00
user, provider = self.validate_session(self.request)
context["first_name"] = user.profile.get_internal_name
return context
class LogoutView(View):
def get(self, request, *args, **kwargs):
logout(request)
return redirect("login")
2020-05-23 16:48:37 +00:00
class PWResetView(AuthView):
template_name = f"{settings.EXPEPHALON_BACKEND}/auth/pwreset.html"
form_class = PWResetForm
def validate_session(self):
try:
token = PWResetToken.objects.get(token=self.kwargs["pk"])
max_age = int(getValue("core.auth.pwreset.max_age", "86400"))
assert token.creation > timezone.now() - timezone.timedelta(seconds=max_age)
return token.user
except:
messages.error(self.request, "Incorrect or expired password reset link.")
raise PermissionDenied()
def form_valid(self, form):
user = self.validate_session()
if not form.cleaned_data["password1"] == form.cleaned_data["password2"]:
messages.error(self.request, "Entered passwords do not match - please try again.")
return self.form_invalid(form)
user.set_password(form.cleaned_data["password1"])
user.save()
messages.success(self.request, "Your password has been changed. You can now login with your new password.")
return redirect("login")
2020-05-23 16:48:37 +00:00
class PWRequestView(AuthView):
template_name = f"{settings.EXPEPHALON_BACKEND}/auth/pwrequest.html"
form_class = PWRequestForm
def form_valid(self, form):
try:
user = get_user_model().objects.get(username=form.cleaned_data["email"])
token = PWResetToken.objects.create(user=user)
2020-05-21 12:54:59 +00:00
mail = generate_pwreset_mail(user, token)
simple_send_mail("Password Reset", mail, user.email)
finally:
messages.success(self.request, "If a matching account was found, you should shortly receive an email containing password reset instructions. If you have not received this message after five minutes, please verify that you have entered the correct email address, or contact support.")
return redirect("login")