Token Validation and Security
The authentication and security architecture of this project is designed around OAuth2 compatible JWT (JSON Web Token) flows. It prioritizes defense-in-depth by implementing protections against common vulnerabilities such as timing attacks, email enumeration, and weak password hashing algorithms.
Credential Validation and Timing Attack Protection
The core of the authentication logic resides in the backend's authenticate function within backend/app/crud.py. This function is responsible for verifying user credentials before a token is issued.
A critical design choice here is the prevention of timing attacks. In many systems, a login request for a non-existent user returns faster than one for an existing user because the system skips the expensive password hashing step. This project mitigates this by using a DUMMY_HASH.
# backend/app/crud.py
# Dummy hash to use for timing attack prevention when user is not found
DUMMY_HASH = "$argon2id$v=19$m=65536,t=3,p=4$MjQyZWE1MzBjYjJlZTI0Yw$YTU4NGM5ZTZmYjE2NzZlZjY0ZWY3ZGRkY2U2OWFjNjk"
def authenticate(*, session: Session, email: str, password: str) -> User | None:
db_user = get_user_by_email(session=session, email=email)
if not db_user:
# Prevent timing attacks by running password verification even when user doesn't exist
verify_password(password, DUMMY_HASH)
return None
verified, updated_password_hash = verify_password(password, db_user.hashed_password)
# ... logic continues
By calling verify_password even when the user is not found, the backend ensures that the response time remains consistent, preventing attackers from using time differentials to enumerate valid email addresses.
Automatic Password Hash Migration
The project utilizes pwdlib to handle password hashing, defaulting to the Argon2id algorithm. The authenticate function includes a mechanism to automatically upgrade password hashes. If a user logs in with a valid password that was hashed using an older or less secure algorithm (like Bcrypt), the system detects this through the updated_password_hash return value and transparently updates the database record.
# backend/app/crud.py
verified, updated_password_hash = verify_password(password, db_user.hashed_password)
if not verified:
return None
if updated_password_hash:
db_user.hashed_password = updated_password_hash
session.add(db_user)
session.commit()
session.refresh(db_user)
return db_user
This design ensures that the security of user credentials improves over time without requiring users to manually reset their passwords.
Token Validation and Dependency Injection
Once a user is authenticated via LoginService.loginAccessToken, the backend issues a JWT. Subsequent requests are validated using the get_current_user dependency found in backend/app/api/deps.py.
This dependency performs several layers of validation:
- JWT Integrity: It decodes the token using the
SECRET_KEYand verifies the signature. - Schema Validation: It parses the payload into a
TokenPayloadPydantic model to ensure thesub(subject/user ID) is present. - User Existence: It fetches the user from the database using the ID from the token.
- Account Status: It checks if the user is still active (
user.is_active).
# backend/app/api/deps.py
def get_current_user(session: SessionDep, token: TokenDep) -> User:
try:
payload = jwt.decode(
token, settings.SECRET_KEY, algorithms=[security.ALGORITHM]
)
token_data = TokenPayload(**payload)
except (InvalidTokenError, ValidationError):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Could not validate credentials",
)
user = session.get(User, token_data.sub)
if not user:
raise HTTPException(status_code=404, detail="User not found")
if not user.is_active:
raise HTTPException(status_code=400, detail="Inactive user")
return user
By using FastAPI's dependency injection system (Annotated[User, Depends(get_current_user)]), the project ensures that protected routes have immediate access to a validated User object, or fail early with a 403 Forbidden or 401 Unauthorized error.
Frontend Token Management
On the frontend, the LoginService (generated in frontend/src/client/sdk.gen.ts) provides the interface for these security operations. The testToken method is specifically designed to verify the validity of a stored token.
// frontend/src/client/sdk.gen.ts
public static testToken(): CancelablePromise<LoginTestTokenResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/api/v1/login/test-token'
});
}
In practice, the frontend (specifically in frontend/src/hooks/useAuth.ts) stores the resulting access token in localStorage. While localStorage is vulnerable to Cross-Site Scripting (XSS) compared to HttpOnly cookies, it is a common tradeoff in Single Page Applications (SPAs) for ease of implementation with standard OAuth2 flows. The project mitigates risk by ensuring the token has a configurable expiration (ACCESS_TOKEN_EXPIRE_MINUTES) and by providing a clear logout mechanism that purges the token from the browser.
Email Enumeration Prevention in Recovery
The password recovery flow in backend/app/api/routes/login.py is designed to prevent attackers from discovering which emails are registered. The recover_password endpoint returns a generic success message regardless of whether the email exists in the database.
@router.post("/password-recovery/{email}")
def recover_password(email: str, session: SessionDep) -> Message:
"""
Password Recovery
"""
user = crud.get_user_by_email(session=session, email=email)
if user:
password_reset_token = generate_password_reset_token(email=email)
send_reset_password_email(
email_to=user.email, email=email, token=password_reset_token
)
return Message(message="Password recovery email sent")
Even if the user is not found, the function returns the same Message object, ensuring that an external observer cannot distinguish between a valid and an invalid email address based on the API response.