feat: implement API security measures phase 1
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
from typing import Any
|
||||
|
||||
import re
|
||||
|
||||
from django.contrib.auth import authenticate
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.contrib.auth.password_validation import validate_password
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
from rest_framework import serializers
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
|
||||
@@ -12,14 +15,27 @@ EMAIL_REGEX: re.Pattern[str] = re.compile(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[
|
||||
MIN_PASSWORD_LENGTH: int = 8
|
||||
|
||||
|
||||
class SanitizedCharField(serializers.CharField):
|
||||
"""CharField that strips control characters on deserialization."""
|
||||
|
||||
def to_internal_value(self, data: object) -> object:
|
||||
value = super().to_internal_value(data)
|
||||
if isinstance(value, str):
|
||||
value = value.strip()
|
||||
if "\x00" in value:
|
||||
raise serializers.ValidationError("Input contains invalid characters.")
|
||||
value = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f]", "", value)
|
||||
return value
|
||||
|
||||
|
||||
class RegisterSerializer(serializers.Serializer):
|
||||
"""Validate and create a new user account."""
|
||||
|
||||
email = serializers.EmailField(max_length=254)
|
||||
password = serializers.CharField(write_only=True)
|
||||
password_confirm = serializers.CharField(write_only=True)
|
||||
first_name = serializers.CharField(max_length=150, required=False, allow_blank=True, default="")
|
||||
last_name = serializers.CharField(max_length=150, required=False, allow_blank=True, default="")
|
||||
first_name = SanitizedCharField(max_length=150, required=False, allow_blank=True, default="")
|
||||
last_name = SanitizedCharField(max_length=150, required=False, allow_blank=True, default="")
|
||||
|
||||
def validate_email(self, value: str) -> str:
|
||||
"""Validate email format and check for duplicates."""
|
||||
@@ -30,11 +46,15 @@ class RegisterSerializer(serializers.Serializer):
|
||||
return value.lower()
|
||||
|
||||
def validate_password(self, value: str) -> str:
|
||||
"""Enforce minimum password length."""
|
||||
"""Enforce minimum password length and Django validators."""
|
||||
if len(value) < MIN_PASSWORD_LENGTH:
|
||||
raise serializers.ValidationError(
|
||||
f"Password must be at least {MIN_PASSWORD_LENGTH} characters."
|
||||
)
|
||||
try:
|
||||
validate_password(value)
|
||||
except DjangoValidationError as e:
|
||||
raise serializers.ValidationError(" ".join(e.messages))
|
||||
return value
|
||||
|
||||
def validate(self, attrs: dict[str, object]) -> dict[str, object]:
|
||||
@@ -51,7 +71,6 @@ class RegisterSerializer(serializers.Serializer):
|
||||
"""Create and return the new user."""
|
||||
validated_data.pop("password_confirm")
|
||||
validated_data["password"] = make_password(validated_data["password"])
|
||||
# Ensure username is blank rather than None for unique constraint
|
||||
validated_data.setdefault("username", "")
|
||||
return User.objects.create(**validated_data)
|
||||
|
||||
@@ -89,8 +108,30 @@ class LoginSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
"""Public user profile serializer."""
|
||||
"""Public user profile serializer — limited fields, no email."""
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ("id", "email", "first_name", "last_name")
|
||||
fields = ("id", "first_name", "last_name")
|
||||
|
||||
|
||||
class RefreshRequestSerializer(serializers.Serializer):
|
||||
"""Validate a refresh token request."""
|
||||
|
||||
refresh = serializers.CharField(required=True)
|
||||
|
||||
def validate_refresh(self, value: str) -> str:
|
||||
if not value or not value.strip():
|
||||
raise serializers.ValidationError("Refresh token is required.")
|
||||
return value.strip()
|
||||
|
||||
|
||||
class LogoutSerializer(serializers.Serializer):
|
||||
"""Validate a logout request."""
|
||||
|
||||
refresh = serializers.CharField(required=True)
|
||||
|
||||
def validate_refresh(self, value: str) -> str:
|
||||
if not value or not value.strip():
|
||||
raise serializers.ValidationError("Refresh token is required.")
|
||||
return value.strip()
|
||||
Reference in New Issue
Block a user