Features: 1) Enforced password validation using Django's validate_password in password reset flows; 2) Added handling for ValidationError during password validation;

Fixes: 1) Removed redundant import for `ValidationError` from `rest_framework.exceptions`; 2) Fixed request data handling in `confirm_password_reset` to directly use incoming data instead of serialized data;

Extra: 1) Minor adjustments to error messages for consistency; 2) Cleaned up unused variables in `confirm_password_reset`.
This commit is contained in:
Egor Pavlovich Gorbunov 2025-05-29 22:42:13 +03:00
parent 0db69018e2
commit 7b5585ea9f
2 changed files with 16 additions and 14 deletions

View file

@ -3,7 +3,7 @@ from hmac import compare_digest
from django.contrib.auth.password_validation import validate_password from django.contrib.auth.password_validation import validate_password
from django.contrib.auth.tokens import PasswordResetTokenGenerator from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.core.exceptions import BadRequest, PermissionDenied from django.core.exceptions import BadRequest, PermissionDenied, ValidationError
from django.db import IntegrityError from django.db import IntegrityError
from django.http import Http404 from django.http import Http404
from django.utils.http import urlsafe_base64_decode from django.utils.http import urlsafe_base64_decode
@ -11,7 +11,6 @@ from django.utils.translation import gettext_lazy as _
from graphene import UUID, Boolean, Field, List, String from graphene import UUID, Boolean, Field, List, String
from graphene.types.generic import GenericScalar from graphene.types.generic import GenericScalar
from graphene_file_upload.scalars import Upload from graphene_file_upload.scalars import Upload
from rest_framework.exceptions import ValidationError
from core.graphene import BaseMutation from core.graphene import BaseMutation
from core.utils.messages import permission_denied_message from core.utils.messages import permission_denied_message
@ -123,8 +122,8 @@ class UpdateUser(BaseMutation):
password = kwargs.get("password", "") password = kwargs.get("password", "")
confirm_password = kwargs.get("confirm_password", "") confirm_password = kwargs.get("confirm_password", "")
if compare_digest(password.lower(), email.lower()): if password:
raise BadRequest(_("password too weak")) validate_password(password=password, user=user)
if not compare_digest(password, "") and compare_digest(password, confirm_password): if not compare_digest(password, "") and compare_digest(password, confirm_password):
user.set_password(password) user.set_password(password)
@ -314,13 +313,15 @@ class ConfirmResetPassword(BaseMutation):
if not password_reset_token.check_token(user, token): if not password_reset_token.check_token(user, token):
raise BadRequest(_("token is invalid!")) raise BadRequest(_("token is invalid!"))
validate_password(password=password, user=user)
user.set_password(password) user.set_password(password)
user.save() user.save()
return ConfirmResetPassword(success=True) return ConfirmResetPassword(success=True)
except (TypeError, ValueError, OverflowError, User.DoesNotExist) as e: except (TypeError, ValueError, OverflowError, ValidationError, User.DoesNotExist) as e:
raise BadRequest(_(f"something went wrong: {e!s}")) raise BadRequest(_(f"something went wrong: {e!s}"))

View file

@ -3,7 +3,9 @@ import traceback
from contextlib import suppress from contextlib import suppress
from secrets import compare_digest from secrets import compare_digest
from django.contrib.auth.password_validation import validate_password
from django.contrib.auth.tokens import PasswordResetTokenGenerator from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.core.exceptions import ValidationError
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.utils.http import urlsafe_base64_decode from django.utils.http import urlsafe_base64_decode
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -20,7 +22,6 @@ from evibes.settings import DEBUG
from vibes_auth.docs.drf.viewsets import USER_SCHEMA from vibes_auth.docs.drf.viewsets import USER_SCHEMA
from vibes_auth.models import User from vibes_auth.models import User
from vibes_auth.serializers import ( from vibes_auth.serializers import (
ConfirmPasswordResetSerializer,
UserSerializer, UserSerializer,
) )
from vibes_auth.utils.emailing import send_reset_password_email_task from vibes_auth.utils.emailing import send_reset_password_email_task
@ -65,32 +66,32 @@ class UserViewSet(
@action(detail=False, methods=["post"]) @action(detail=False, methods=["post"])
@method_decorator(ratelimit(key="ip", rate="2/h" if not DEBUG else "888/h")) @method_decorator(ratelimit(key="ip", rate="2/h" if not DEBUG else "888/h"))
def confirm_password_reset(self, request, *args, **kwargs): def confirm_password_reset(self, request, *args, **kwargs):
serializer_data = None
try: try:
serializer_data = ConfirmPasswordResetSerializer(request.data).data
if not compare_digest(serializer_data.get("password"), serializer_data.get("confirm_password")): if not compare_digest(request.data.get("password"), request.data.get("confirm_password")):
return Response( return Response(
{"error": _("passwords do not match")}, {"error": _("passwords do not match")},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
uuid = urlsafe_base64_decode(serializer_data.get("uidb64")).decode() uuid = urlsafe_base64_decode(request.data.get("uidb64")).decode()
user = User.objects.get(pk=uuid) user = User.objects.get(pk=uuid)
validate_password(password=request.data.get("password"), user=user)
password_reset_token = PasswordResetTokenGenerator() password_reset_token = PasswordResetTokenGenerator()
if not password_reset_token.check_token(user, serializer_data.get("token")): if not password_reset_token.check_token(user, request.data.get("token")):
return Response({"error": _("token is invalid!")}, status=status.HTTP_400_BAD_REQUEST) return Response({"error": _("token is invalid!")}, status=status.HTTP_400_BAD_REQUEST)
user.set_password(serializer_data.get("password")) user.set_password(request.data.get("password"))
user.save() user.save()
return Response({"message": _("password reset successfully")}, status=status.HTTP_200_OK) return Response({"message": _("password reset successfully")}, status=status.HTTP_200_OK)
except (TypeError, ValueError, OverflowError, User.DoesNotExist) as e: except (TypeError, ValueError, OverflowError, ValidationError, User.DoesNotExist) as e:
data = {"error": str(e)} data = {"error": str(e)}
if DEBUG: if DEBUG:
data["detail"] = str(traceback.format_exc()) data["detail"] = str(traceback.format_exc())
data["received"] = str(serializer_data) data["received"] = str(request.data)
return Response(data, status=status.HTTP_400_BAD_REQUEST) return Response(data, status=status.HTTP_400_BAD_REQUEST)
@method_decorator(ratelimit(key="ip", rate="3/h" if not DEBUG else "888/h")) @method_decorator(ratelimit(key="ip", rate="3/h" if not DEBUG else "888/h"))