Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions apps/accounts/serializers.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
from allauth.account.models import EmailAddress
from base.utils import get_user_by_email
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import User
from participants.utils import (
has_participated_in_require_complete_profile_challenge,
)
from rest_auth.registration.serializers import RegisterSerializer
from rest_auth.serializers import PasswordResetSerializer
from rest_framework import serializers
from rest_framework.authtoken.serializers import AuthTokenSerializer
from rest_framework.exceptions import ValidationError

from .models import JwtToken, Profile

PASSWORD_MAX_LENGTH = getattr(settings, "PASSWORD_MAX_LENGTH", 128)

LOCKED_PROFILE_FIELDS = [
"first_name",
"last_name",
Expand Down Expand Up @@ -337,6 +342,29 @@ def create(self, validated_data):
return email_address


class CustomRegisterSerializer(RegisterSerializer):
"""Registration serializer with bounded password field lengths."""

password1 = serializers.CharField(
write_only=True, max_length=PASSWORD_MAX_LENGTH
)
password2 = serializers.CharField(
write_only=True, max_length=PASSWORD_MAX_LENGTH
)


class BoundedAuthTokenSerializer(AuthTokenSerializer):
"""Login serializer with bounded password field length."""

password = serializers.CharField(
label="Password",
style={"input_type": "password"},
write_only=True,
trim_whitespace=False,
max_length=PASSWORD_MAX_LENGTH,
)


class CustomPasswordResetSerializer(PasswordResetSerializer):
"""
Send password reset emails only for eligible accounts while always
Expand Down
17 changes: 17 additions & 0 deletions apps/accounts/throttles.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,23 @@ def get_cache_key(self, request, view):
return self.cache_format % {"scope": self.scope, "ident": email}


class RegistrationIPThrottle(_ThrottlingCacheMixin, SimpleRateThrottle):
"""
Limit registration requests to 10/hour per client IP address.

Requires REST_FRAMEWORK["NUM_PROXIES"] to be set when deployed behind
trusted reverse proxies so client IPs are derived correctly.
"""

scope = "registration_ip"

def get_cache_key(self, request, view):
return self.cache_format % {
"scope": self.scope,
"ident": self.get_ident(request),
}


class PasswordResetIPThrottle(_ThrottlingCacheMixin, SimpleRateThrottle):
"""
Limit password reset requests to 10/hour per client IP address.
Expand Down
31 changes: 31 additions & 0 deletions apps/accounts/validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from django.conf import settings
from django.core.exceptions import ValidationError
from django.utils.translation import gettext as _


class MaximumLengthValidator:
"""
Reject passwords longer than PASSWORD_MAX_LENGTH to prevent resource
exhaustion from oversized password payloads.
"""

def __init__(self, max_length=None):
self.max_length = max_length or getattr(
settings, "PASSWORD_MAX_LENGTH", 128
)

def validate(self, password, user=None):
if len(password) > self.max_length:
raise ValidationError(
_(
"This password is longer than the maximum of "
"%(max_length)d characters."
),
code="password_too_long",
params={"max_length": self.max_length},
)

def get_help_text(self):
return _(
"Your password must be at most %(max_length)d characters."
) % {"max_length": self.max_length}
37 changes: 36 additions & 1 deletion apps/accounts/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
throttle_classes,
)
from rest_framework.response import Response
from rest_framework.status import HTTP_400_BAD_REQUEST
from rest_framework.throttling import UserRateThrottle
from rest_framework_expiring_authtoken.models import ExpiringToken
from rest_framework_expiring_authtoken.views import ObtainExpiringAuthToken
from rest_framework_simplejwt.authentication import JWTAuthentication
from rest_framework_simplejwt.exceptions import TokenError
from rest_framework_simplejwt.token_blacklist.models import OutstandingToken
Expand All @@ -24,10 +27,15 @@
from .authentication import ExpiringTokenAuthentication
from .models import JwtToken
from .permissions import HasVerifiedEmail
from .serializers import JwtTokenSerializer, UpdateEmailSerializer
from .serializers import (
BoundedAuthTokenSerializer,
JwtTokenSerializer,
UpdateEmailSerializer,
)
from .throttles import (
PasswordResetEmailThrottle,
PasswordResetIPThrottle,
RegistrationIPThrottle,
ResendEmailThrottle,
)

Expand All @@ -40,6 +48,31 @@
)


class SafeObtainExpiringAuthToken(ObtainExpiringAuthToken):
"""Login view with bounded password field length."""

def post(self, request):
serializer = BoundedAuthTokenSerializer(data=request.data)

if serializer.is_valid():
token, _ = ExpiringToken.objects.get_or_create(
user=serializer.validated_data["user"]
)

if token.expired():
token.delete()
token = ExpiringToken.objects.create(
user=serializer.validated_data["user"]
)

return Response({"token": token.key})

return Response(serializer.errors, status=HTTP_400_BAD_REQUEST)


safe_obtain_expiring_auth_token = SafeObtainExpiringAuthToken.as_view()
Comment thread
coderabbitai[bot] marked this conversation as resolved.


class SafePasswordResetView(PasswordResetView):
"""Password reset view that returns a generic response to prevent
email enumeration."""
Expand All @@ -65,6 +98,8 @@ class SafeRegisterView(RegisterView):
slip past the check. This view acts as a safety net for that case.
"""

throttle_classes = (RegistrationIPThrottle,)

def create(self, request, *args, **kwargs):
try:
return super().create(request, *args, **kwargs)
Expand Down
6 changes: 5 additions & 1 deletion apps/challenges/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
DEFAULT_WORKER_PYTHON_VERSION,
SUPPORTED_WORKER_PYTHON_VERSIONS,
)
from django.conf import settings
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError as DjangoValidationError
from hosts.serializers import ChallengeHostTeamSerializer
Expand Down Expand Up @@ -615,7 +616,10 @@ class ChallengeInvitationRegisterSerializer(serializers.Serializer):
last_name = serializers.CharField(
max_length=150, required=False, allow_blank=True
)
password = serializers.CharField(write_only=True)
password = serializers.CharField(
write_only=True,
max_length=getattr(settings, "PASSWORD_MAX_LENGTH", 128),
)

def validate(self, attrs):
user = self.context.get("user")
Expand Down
9 changes: 6 additions & 3 deletions evalai/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
"""

from accounts.views import SafePasswordResetView, SafeRegisterView
from accounts.views import (
SafePasswordResetView,
SafeRegisterView,
safe_obtain_expiring_auth_token,
)
from allauth.account.views import ConfirmEmailView
from django.conf import settings
from django.conf.urls import include, url
Expand All @@ -26,7 +30,6 @@
SpectacularRedocView,
SpectacularSwaggerView,
)
from rest_framework_expiring_authtoken.views import obtain_expiring_auth_token
from web import views

handler404 = "web.views.page_not_found"
Expand All @@ -43,7 +46,7 @@
url(r"^api/admin/", admin.site.urls),
url(
r"^api/auth/login",
obtain_expiring_auth_token,
safe_obtain_expiring_auth_token,
name="obtain_expiring_auth_token",
),
url(
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/views/web/auth/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
</div>
<!-- password -->
<div class="input-field align-left">
<input type="{{canShowPassword ? 'text' : 'password'}}" id="password" class="dark-autofill" name="password" ng-model="auth.getUser.password" autocomplete="new-password" ng-minlength="8" required>
<input type="{{canShowPassword ? 'text' : 'password'}}" id="password" class="dark-autofill" name="password" ng-model="auth.getUser.password" autocomplete="new-password" ng-minlength="8" ng-maxlength="128" required>
<span class="form-icon form-icon-dark" ng-click="auth.togglePasswordVisibility()">
<i ng-if="!canShowPassword" class="fa fa-eye pointer"></i>
<i ng-if="canShowPassword" class="fa fa-eye-slash pointer"></i>
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/views/web/auth/reset-password-confirm.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<form name="resetconfirmForm" ng-submit="auth.resetPasswordConfirm(resetconfirmForm.$valid)" class="reset-password" novalidate>
<!-- email -->
<div class="input-field align-left">
<input type="{{canShowPassword ? 'text' : 'password'}}" id="new_password1" onpaste="return false;" class="dark-autofill" name="new_password1" ng-model="auth.getUser.new_password1" minlength="8" required>
<input type="{{canShowPassword ? 'text' : 'password'}}" id="new_password1" onpaste="return false;" class="dark-autofill" name="new_password1" ng-model="auth.getUser.new_password1" minlength="8" maxlength="128" required>
<span class="form-icon form-icon-dark" ng-click="auth.togglePasswordVisibility()">
<i ng-if="!canShowPassword" class="fa fa-eye pointer"></i>
<i ng-if="canShowPassword" class="fa fa-eye-slash pointer"></i>
Expand All @@ -31,7 +31,7 @@
</div>
</div>
<div class="input-field align-left">
<input type="{{canShowConfirmPassword ? 'text' : 'password'}}" id="new_password2" class="dark-autofill" name="new_password2" ng-model="auth.getUser.new_password2" minlength="8" compare-to="auth.getUser.new_password1" required>
<input type="{{canShowConfirmPassword ? 'text' : 'password'}}" id="new_password2" class="dark-autofill" name="new_password2" ng-model="auth.getUser.new_password2" minlength="8" maxlength="128" compare-to="auth.getUser.new_password1" required>
<span class="form-icon form-icon-dark" ng-click="auth.toggleConfirmPasswordVisibility()">
<i ng-if="!canShowConfirmPassword" class="fa fa-eye pointer"></i>
<i ng-if="canShowConfirmPassword" class="fa fa-eye-slash pointer"></i>
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/views/web/auth/signup.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
</div>
<!-- password -->
<div class="input-field align-left">
<input type="{{canShowPassword ? 'text' : 'password'}}" id="password" class="dark-autofill" name="password" ng-model="auth.regUser.password" ng-minlength="8" ng-change="auth.checkStrength(auth.regUser.password)" required>
<input type="{{canShowPassword ? 'text' : 'password'}}" id="password" class="dark-autofill" name="password" ng-model="auth.regUser.password" ng-minlength="8" ng-maxlength="128" ng-change="auth.checkStrength(auth.regUser.password)" required>
<span class="form-icon form-icon-dark" ng-click="auth.togglePasswordVisibility()">
<i ng-if="!canShowPassword" class="fa fa-eye pointer"></i>
<i ng-if="canShowPassword" class="fa fa-eye-slash pointer"></i>
Expand All @@ -41,12 +41,13 @@
</label>
<div class="wrn-msg text-highlight" ng-messages="signupForm.password.$error" ng-if="signupForm.password.$touched || signupForm.$submitted">
<p ng-message="minlength">Password is less than 8 characters.</p>
<p ng-message="maxlength">Password must be at most 128 characters.</p>
<p ng-message="required">Password is required.</p>
</div>
</div>
<!-- confirm password -->
<div class="input-field align-left">
<input type="{{canShowConfirmPassword ? 'text' : 'password'}}" id="confirm_password" class="dark-autofill" name="confirm_password" ng-model="auth.regUser.confirm_password" ng-minlength="8" compare-to="auth.regUser.password" required>
<input type="{{canShowConfirmPassword ? 'text' : 'password'}}" id="confirm_password" class="dark-autofill" name="confirm_password" ng-model="auth.regUser.confirm_password" ng-minlength="8" ng-maxlength="128" compare-to="auth.regUser.password" required>
<span class="form-icon form-icon-dark" ng-click="auth.toggleConfirmPasswordVisibility()">
<i ng-if="!canShowConfirmPassword" class="fa fa-eye pointer"></i>
<i ng-if="canShowConfirmPassword" class="fa fa-eye-slash pointer"></i>
Expand All @@ -55,6 +56,7 @@
<div class="wrn-msg text-highlight" ng-messages="signupForm.confirm_password.$error" ng-if="signupForm.confirm_password.$touched || signupForm.$submitted">
<p ng-message="compareTo">Passwords do not match</p>
<p ng-message="minlength">Password is less than 8 characters.</p>
<p ng-message="maxlength">Password must be at most 128 characters.</p>
<p ng-message="required">Password confirmation is required.</p>
</div>
</div>
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/views/web/challenge-invite.html
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
<div class="col s12 m12">
<div class="input-field align-left">
<input type="password" id="password" onpaste="return false;" class="dark-autofill" name="password"
ng-model="challenge_invitation.password" ng-minlength="8" required>
ng-model="challenge_invitation.password" ng-minlength="8" ng-maxlength="128" required>
<span class="form-icon form-icon-dark"><i class="fa fa-key"></i></span>
<label for="password">
<strong>Password-minlength:8 required*</strong>
Expand All @@ -79,6 +79,7 @@
<div class="wrn-msg text-highlight" ng-messages="registerChallengeParticipantForm.password.$error"
ng-if="registerChallengeParticipantForm.password.$touched || registerChallengeParticipantForm.$submitted">
<p ng-message="minlength">Password is less than 8 characters.</p>
<p ng-message="maxlength">Password must be at most 128 characters.</p>
<p ng-message="required">Password is required.</p>
</div>
</div>
Expand All @@ -88,13 +89,14 @@
<div class="col s12 m12">
<div class="input-field align-left">
<input type="password" id="confirm_password" onpaste="return false;" class="dark-autofill" name="confirm_password"
ng-model="challenge_invitation.confirm_password" ng-minlength="8" compare-to="challenge_invitation.password" required>
ng-model="challenge_invitation.confirm_password" ng-minlength="8" ng-maxlength="128" compare-to="challenge_invitation.password" required>
<span class="form-icon form-icon-dark"><i class="fa fa-check-circle"></i></span>
<label for="confirm_password">Confirm Password *</label>
<div class="wrn-msg text-highlight" ng-messages="registerChallengeParticipantForm.confirm_password.$error"
ng-if="registerChallengeParticipantForm.confirm_password.$touched || registerChallengeParticipantForm.$submitted">
<p ng-message="compareTo">Passwords do not match</p>
<p ng-message="minlength">Password is less than 8 characters.</p>
<p ng-message="maxlength">Password must be at most 128 characters.</p>
<p ng-message="required">Password confirmation is required.</p>
</div>
</div>
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/views/web/change-password.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<div class="pass-title">Change Password</div>
<form name="profileForm" ng-submit="profile.changePassword(profileForm.$valid)" novalidate>
<div class="input-field align-left">
<input id="old_password" name="old_password" type="{{canShowOldPassword ? 'text' : 'password'}}" class="text-dark-black dark-autofill w-400" ng-model="profile.user.old_password" ng-minlength="8" required focus-if>
<input id="old_password" name="old_password" type="{{canShowOldPassword ? 'text' : 'password'}}" class="text-dark-black dark-autofill w-400" ng-model="profile.user.old_password" ng-minlength="8" ng-maxlength="128" required focus-if>
<span class="form-icon form-icon-dark" ng-click="profile.toggleOldPasswordVisibility()">
<i ng-if="!canShowOldPassword" class="fa fa-eye pointer"></i>
<i ng-if="canShowOldPassword" class="fa fa-eye-slash pointer"></i>
Expand All @@ -19,7 +19,7 @@
</div>
</div>
<div class="input-field align-left">
<input id="new_password1" name="new_password1" type="{{canShowNewPassword ? 'text' : 'password'}}" class="text-dark-black dark-autofill w-400" ng-model="profile.user.new_password1" ng-minlength="8" required match="profile.user.old_password">
<input id="new_password1" name="new_password1" type="{{canShowNewPassword ? 'text' : 'password'}}" class="text-dark-black dark-autofill w-400" ng-model="profile.user.new_password1" ng-minlength="8" ng-maxlength="128" required match="profile.user.old_password">
<span class="form-icon form-icon-dark" ng-click="profile.toggleNewPasswordVisibility()">
<i ng-if="!canShowNewPassword" class="fa fa-eye pointer"></i>
<i ng-if="canShowNewPassword" class="fa fa-eye-slash pointer"></i>
Expand All @@ -32,7 +32,7 @@
</div>
</div>
<div class="input-field align-left">
<input id="new_password2" name="new_password2" type="{{canShowNewConfirmPassword ? 'text' : 'password'}}" class="text-dark-black dark-autofill w-400" ng-model="profile.user.new_password2" ng-minlength="8" compare-to="profile.user.new_password1" required>
<input id="new_password2" name="new_password2" type="{{canShowNewConfirmPassword ? 'text' : 'password'}}" class="text-dark-black dark-autofill w-400" ng-model="profile.user.new_password2" ng-minlength="8" ng-maxlength="128" compare-to="profile.user.new_password1" required>
<span class="form-icon form-icon-dark" ng-click="profile.toggleNewConfirmVisibility()">
<i ng-if="!canShowNewConfirmPassword" class="fa fa-eye pointer"></i>
<i ng-if="canShowNewConfirmPassword" class="fa fa-eye-slash pointer"></i>
Expand Down
Loading
Loading