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

  1. Re-enter the username and password carefully; note the username is trimmed and lowercased server-side.
  2. If the password was forgotten and an admin is available, have them reset it; otherwise use the app's password-reset flow.
  3. Confirm the account actually exists (e.g. signup returns 409 'Username already taken' if you try to recreate it).
  4. 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

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

Related errors


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/9dbc0bf92fbd841b. Report an issue: GitHub.