diff --git a/surfsense_backend/app/app.py b/surfsense_backend/app/app.py index 541393564..e35b310e0 100644 --- a/surfsense_backend/app/app.py +++ b/surfsense_backend/app/app.py @@ -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. diff --git a/surfsense_backend/app/users.py b/surfsense_backend/app/users.py index b68461fc0..7ec657781 100644 --- a/surfsense_backend/app/users.py +++ b/surfsense_backend/app/users.py @@ -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) diff --git a/surfsense_web/lib/auth-errors.ts b/surfsense_web/lib/auth-errors.ts index c1f0a3be3..d1816cbdd 100644 --- a/surfsense_web/lib/auth-errors.ts +++ b/surfsense_web/lib/auth-errors.ts @@ -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" },