refactor: update authentication error handling to prevent user enumeration and improve error messages

This commit is contained in:
Anish Sarkar 2026-02-09 12:57:32 +05:30
parent 2add106296
commit c1016591da
3 changed files with 14 additions and 44 deletions

View file

@ -62,7 +62,7 @@ def _rate_limit_exceeded_handler(request: Request, exc: RateLimitExceeded):
# ============================================================================
# Stricter per-IP limits on auth endpoints to prevent:
# - Brute force password attacks
# - User enumeration via LOGIN_USER_NOT_FOUND / REGISTER_USER_ALREADY_EXISTS
# - User enumeration via REGISTER_USER_ALREADY_EXISTS
# - Email spam via forgot-password
#
# These use direct Redis INCR+EXPIRE for simplicity and reliability.

View file

@ -2,7 +2,7 @@ import logging
import uuid
import httpx
from fastapi import Depends, HTTPException, Request, Response
from fastapi import Depends, Request, Response
from fastapi.responses import JSONResponse, RedirectResponse
from fastapi_users import BaseUserManager, FastAPIUsers, UUIDIDMixin, models
from fastapi_users.authentication import (
@ -47,6 +47,14 @@ if config.AUTH_TYPE == "GOOGLE":
class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
"""
Custom user manager extending fastapi-users BaseUserManager.
Authentication returns a generic error for both non-existent accounts
and incorrect passwords to comply with OWASP WSTG-IDNT-04 and
prevent user enumeration attacks.
"""
reset_password_token_secret = SECRET
verification_token_secret = SECRET
@ -180,36 +188,6 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
):
print(f"Verification requested for user {user.id}. Verification token: {token}")
async def authenticate(self, credentials):
"""
Override to return a specific error when user account doesn't exist,
instead of a generic LOGIN_BAD_CREDENTIALS.
"""
from fastapi_users.exceptions import UserNotExists
try:
user = await self.get_by_email(credentials.username)
except UserNotExists:
# Still hash the password to mitigate timing attacks
self.password_helper.hash(credentials.password)
logger.warning(
f"Login attempt for non-existent account: {credentials.username}"
)
raise HTTPException(
status_code=400,
detail="LOGIN_USER_NOT_FOUND",
) from None
verified, updated_password_hash = self.password_helper.verify_and_update(
credentials.password, user.hashed_password
)
if not verified:
logger.warning(f"Failed login attempt (wrong password) for user: {user.id}")
return None
if updated_password_hash is not None:
await self.user_db.update(user, {"hashed_password": updated_password_hash})
return user
async def get_user_manager(user_db: SQLAlchemyUserDatabase = Depends(get_user_db)):
yield UserManager(user_db)

View file

@ -20,8 +20,8 @@ const AUTH_ERROR_MESSAGES: AuthErrorMapping = {
description: "Your account may be suspended or restricted",
},
"404": {
title: "Account not found",
description: "No account exists with this email address",
title: "Not found",
description: "The requested resource was not found",
},
"409": {
title: "Account conflict",
@ -46,12 +46,8 @@ const AUTH_ERROR_MESSAGES: AuthErrorMapping = {
// FastAPI specific errors
LOGIN_BAD_CREDENTIALS: {
title: "Invalid credentials",
description: "The email or password you entered is incorrect",
},
LOGIN_USER_NOT_FOUND: {
title: "Account not found",
description: "No account exists with this email address. Please sign up first.",
title: "Login failed",
description: "Invalid email or password. If you don't have an account, please sign up.",
},
LOGIN_USER_NOT_VERIFIED: {
title: "Account not verified",
@ -147,10 +143,6 @@ export function getAuthErrorMessage(errorCode: string, returnTitle: boolean = fa
if (!errorInfo) {
const patterns = [
{ pattern: /credential|password|email/i, code: "LOGIN_BAD_CREDENTIALS" },
{
pattern: /not found|no account|does not exist|user not found/i,
code: "LOGIN_USER_NOT_FOUND",
},
{ pattern: /verify|verification/i, code: "LOGIN_USER_NOT_VERIFIED" },
{ pattern: /inactive|disabled|suspended/i, code: "USER_INACTIVE" },
{ pattern: /exists|duplicate/i, code: "REGISTER_USER_ALREADY_EXISTS" },