{"record":{"id":"9dbc0bf92fbd841b","repo":"odysseus-dev/odysseus","slug":"invalid-credentials","errorCode":null,"errorMessage":"Invalid credentials","messagePattern":"Invalid credentials","errorType":"http","errorClass":"HTTPException","httpStatus":401,"severity":"error","filePath":"routes/auth_routes.py","lineNumber":145,"sourceCode":"        if len(body.password) < PASSWORD_MIN_LENGTH:\n            raise HTTPException(400, f\"Password must be at least {PASSWORD_MIN_LENGTH} characters\")\n        if len(body.username.strip()) < 1:\n            raise HTTPException(400, \"Username is required\")\n        if body.username.lower() in RESERVED_USERNAMES:\n            raise HTTPException(403, \"Username is reserved\")\n        ok = await asyncio.to_thread(auth_manager.create_user, body.username, body.password, is_admin=False)\n        if not ok:\n            raise HTTPException(409, \"Username already taken\")\n        return {\"ok\": True, \"message\": \"Account created\"}\n\n    @router.post(\"/login\")\n    async def login(body: LoginRequest, request: Request, response: Response):\n        if not _login_limiter.check(request.client.host):\n            raise HTTPException(429, \"Too many requests — try again later\")\n        # Verify password first\n        username = body.username.strip().lower()\n        if not await asyncio.to_thread(auth_manager.verify_password, username, body.password):\n            raise HTTPException(401, \"Invalid credentials\")\n        # Check 2FA if enabled\n        if auth_manager.totp_enabled(username):\n            if not body.totp_code:\n                # Password OK but need TOTP — tell client to show code input\n                return {\"ok\": False, \"requires_totp\": True, \"username\": username}\n            if not auth_manager.totp_verify(username, body.totp_code):\n                raise HTTPException(401, \"Invalid 2FA code\")\n        # All checks passed — create session (password already verified above)\n        token = await asyncio.to_thread(auth_manager.create_session_trusted, username)\n        if not token:\n            raise HTTPException(401, \"Invalid credentials\")\n        cookie_kwargs = dict(\n            key=SESSION_COOKIE,\n            value=token,\n            httponly=True,\n            samesite=\"lax\",\n            secure=os.getenv(\"SECURE_COOKIES\", \"false\").lower() == \"true\",\n            path=\"/\",","sourceCodeStart":127,"sourceCodeEnd":163,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/auth_routes.py#L127-L163","documentation":"Raised as HTTP 401 by POST /login when auth_manager.verify_password(username, body.password) returns falsy. Verification fails when the (stripped, lowercased) username does not exist or the password hash does not match. The message is deliberately generic so it cannot be used to enumerate valid usernames.","triggerScenarios":"Logging in with a wrong password; logging in with a username that was never registered; username casing/whitespace mismatches after strip().lower() normalization; or a user record whose stored hash was corrupted or created by a different hashing scheme after an upgrade.","commonSituations":"Typos or caps-lock during password entry, stale saved credentials in a browser/password manager after a password change, accounts created before a hashing-scheme migration, or clients that send the username with surrounding whitespace or mixed case expecting an exact match.","solutions":["Re-enter the username and password carefully; note the username is trimmed and lowercased server-side.","If the password was forgotten and an admin is available, have them reset it; otherwise use the app's password-reset flow.","Confirm the account actually exists (e.g. signup returns 409 'Username already taken' if you try to recreate it).","If it broke after an auth-manager upgrade, re-hash or migrate stored password hashes."],"exampleFix":"// before — assuming specific failure reasons\nif (res.status === 401 && body.username === knownUser) alert('password wrong');\n// after — treat as generic invalid credentials\nif (res.status === 401) {\n  alert('Invalid username or password');\n}","handlingStrategy":"try-catch","validationCode":"// Client-side sanity check only; existence cannot be verified without the server\nif (!body.username.trim() || !body.password) return showError('Enter username and password');","typeGuard":null,"tryCatchPattern":"catch (e) {\n  if (e.status === 401 && !e.body?.requires_totp) {\n    showGenericError('Invalid username or password'); // do NOT probe which part failed\n    incrementBackoff();\n  }\n}","preventionTips":["Trim and lowercase the username client-side to match server normalization.","Keep password managers updated after every password change.","Show one generic message; never enumerate whether the username exists."],"tags":["auth","http-401","login","credentials","fastapi"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}