odysseus-dev/odysseus · error · HTTPException
Invalid credentials
Error message
Invalid credentials
What it means
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.
Source
Thrown at routes/auth_routes.py:145
if len(body.password) < PASSWORD_MIN_LENGTH:
raise HTTPException(400, f"Password must be at least {PASSWORD_MIN_LENGTH} characters")
if len(body.username.strip()) < 1:
raise HTTPException(400, "Username is required")
if body.username.lower() in RESERVED_USERNAMES:
raise HTTPException(403, "Username is reserved")
ok = await asyncio.to_thread(auth_manager.create_user, body.username, body.password, is_admin=False)
if not ok:
raise HTTPException(409, "Username already taken")
return {"ok": True, "message": "Account created"}
@router.post("/login")
async def login(body: LoginRequest, request: Request, response: Response):
if not _login_limiter.check(request.client.host):
raise HTTPException(429, "Too many requests — try again later")
# Verify password first
username = body.username.strip().lower()
if not await asyncio.to_thread(auth_manager.verify_password, username, body.password):
raise HTTPException(401, "Invalid credentials")
# Check 2FA if enabled
if auth_manager.totp_enabled(username):
if not body.totp_code:
# Password OK but need TOTP — tell client to show code input
return {"ok": False, "requires_totp": True, "username": username}
if not auth_manager.totp_verify(username, body.totp_code):
raise HTTPException(401, "Invalid 2FA code")
# All checks passed — create session (password already verified above)
token = await asyncio.to_thread(auth_manager.create_session_trusted, username)
if not token:
raise HTTPException(401, "Invalid credentials")
cookie_kwargs = dict(
key=SESSION_COOKIE,
value=token,
httponly=True,
samesite="lax",
secure=os.getenv("SECURE_COOKIES", "false").lower() == "true",
path="/",View on GitHub (pinned to f9235ebbf1)
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.
Example fix
// before — assuming specific failure reasons
if (res.status === 401 && body.username === knownUser) alert('password wrong');
// after — treat as generic invalid credentials
if (res.status === 401) {
alert('Invalid username or password');
} Defensive patterns
Strategy: try-catch
Validate before calling
// Client-side sanity check only; existence cannot be verified without the server
if (!body.username.trim() || !body.password) return showError('Enter username and password'); Try / catch
catch (e) {
if (e.status === 401 && !e.body?.requires_totp) {
showGenericError('Invalid username or password'); // do NOT probe which part failed
incrementBackoff();
}
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/9dbc0bf92fbd841b.
Report an issue: GitHub.