Security and Authentication Schemas
The security architecture of this project relies on a set of specialized schemas defined in backend/app/models.py. These models facilitate a standard OAuth2 authentication flow while enforcing strict data validation for sensitive operations like password updates and resets.
OAuth2 Compliance and the Token Model
The project implements the OAuth2 "Password Flow," which requires a specific response format upon successful authentication. The Token model serves this purpose:
class Token(SQLModel):
access_token: str
token_type: str = "bearer"
This design ensures compatibility with standard OAuth2 clients. By defaulting token_type to "bearer", the API adheres to the common practice of using Bearer tokens for authorization headers. This model is used as the return type for the /login/access-token endpoint in backend/app/api/routes/login.py, providing the client with the necessary credentials for subsequent requests.
JWT Structure and Identity Verification
Once a token is issued, the server must be able to decode and verify it. The TokenPayload model defines the expected structure of the decoded JSON Web Token (JWT):
class TokenPayload(SQLModel):
sub: str | None = None
The "sub" Claim Design
The choice to include only the sub (subject) claim in the TokenPayload reflects a minimalist approach to JWT design. In this project, the sub field stores the user's unique identifier.
A notable implementation detail is found in backend/app/core/security.py, where the create_access_token function explicitly casts the subject to a string:
to_encode = {"exp": expire, "sub": str(subject)}.
This is a critical design choice because the project uses UUIDs for user IDs. By standardizing the sub claim as a string, the system maintains compatibility with the JWT specification while allowing the get_current_user dependency in backend/app/api/deps.py to retrieve the user from the database using session.get(User, token_data.sub).
Tradeoffs of Minimalist Payloads
By only storing the user ID in the token, the project prioritizes security and token size over performance. Since roles or permissions are not embedded in the JWT, the application must perform a database lookup for every authenticated request to verify the user's status and privileges (e.g., is_active or is_superuser). This ensures that if a user is deactivated or their permissions change, the effect is immediate, rather than waiting for a token to expire.
Password Management Schemas
The project distinguishes between two different password-related actions: updating a known password and resetting a forgotten one. This distinction is handled by UpdatePassword and NewPassword.
Authenticated Updates
The UpdatePassword model is used when a logged-in user wants to change their password. It requires the current password to prevent account takeover via an unattended session:
class UpdatePassword(SQLModel):
current_password: str = Field(min_length=8, max_length=128)
new_password: str = Field(min_length=8, max_length=128)
Recovery Flow
The NewPassword model is used in the password recovery flow. Unlike the update flow, it requires a temporary reset token instead of the current password:
class NewPassword(SQLModel):
token: str
new_password: str = Field(min_length=8, max_length=128)
In backend/app/api/routes/login.py, this model is consumed by the /reset-password/ endpoint. A key security decision here is passing the reset token in the request body rather than as a query parameter. This prevents the sensitive token from being recorded in web server access logs.
Validation and Constraints
Both password models enforce a consistent validation policy using Pydantic's Field:
- Minimum Length (8): Ensures a baseline level of complexity.
- Maximum Length (128): Protects against "Long Password" Denial of Service (DoS) attacks, where an attacker sends an extremely long string to overwhelm the hashing algorithm (Argon2/Bcrypt).
These constraints are applied at the schema level, ensuring that invalid data is rejected by FastAPI before it ever reaches the database or the hashing logic in backend/app/core/security.py.