Bcrypt silently truncates passwords at 72 bytes, so passwords longer than 72 characters provide no additional security. This commit enforces the 72-byte maximum across the authentication and setup flows. Changes: - Add max_length=72 to LoginRequest.password and SetupRequest.master_password - Update field validator in SetupRequest to explicitly check max_length - Add comprehensive tests for password length validation (6 new test cases) - Document the 72-byte limitation in Features.md (master password options) - Add new section 12 'Password Hashing' in Backend-Development.md explaining: - The bcrypt truncation behavior - Why the limit is enforced - The validation flow from frontend to backend - What happens when passwords exceed the limit All existing tests pass, no regressions introduced. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
51 lines
1.5 KiB
Python
51 lines
1.5 KiB
Python
"""Authentication Pydantic models.
|
|
|
|
Request, response, and domain models used by the auth router and service.
|
|
"""
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
|
|
|
|
class LoginRequest(BaseModel):
|
|
"""Payload for ``POST /api/auth/login``."""
|
|
|
|
model_config = ConfigDict(strict=True)
|
|
|
|
password: str = Field(
|
|
...,
|
|
max_length=72,
|
|
description="Master password to authenticate with (max 72 bytes due to bcrypt truncation).",
|
|
)
|
|
|
|
|
|
class LoginResponse(BaseModel):
|
|
"""Successful login response.
|
|
|
|
The session token is also set as an ``HttpOnly`` cookie by the router.
|
|
This model documents the JSON body for API-first consumers.
|
|
"""
|
|
|
|
model_config = ConfigDict(strict=True)
|
|
|
|
token: str = Field(..., description="Session token for use in subsequent requests.")
|
|
expires_at: str = Field(..., description="ISO 8601 UTC expiry timestamp.")
|
|
|
|
|
|
class LogoutResponse(BaseModel):
|
|
"""Response body for ``POST /api/auth/logout``."""
|
|
|
|
model_config = ConfigDict(strict=True)
|
|
|
|
message: str = Field(default="Logged out successfully.")
|
|
|
|
|
|
class Session(BaseModel):
|
|
"""Internal domain model representing a persisted session record."""
|
|
|
|
model_config = ConfigDict(strict=True)
|
|
|
|
id: int = Field(..., description="Auto-incremented row ID.")
|
|
token: str = Field(..., description="Opaque session token.")
|
|
created_at: str = Field(..., description="ISO 8601 UTC creation timestamp.")
|
|
expires_at: str = Field(..., description="ISO 8601 UTC expiry timestamp.")
|