affaan-m/ECC · error · HTTPException

Incorrect username or password

Error message

Incorrect username or password

What it means

In the `login` route (OAuth2 password flow), `UserService.authenticate(username, password)` returns `None` when credentials do not validate; the handler raises `HTTPException(401, "Incorrect username or password", headers={"WWW-Authenticate":"Bearer"})`. The deliberate vague message does not distinguish "no such user" from "wrong password" to prevent user enumeration.

Source

Thrown at skills/fastapi-patterns/SKILL.md:295

    service = UserService(db)
    try:
        user = await service.update(user_id, payload)
    except DuplicateUserError:
        raise HTTPException(status_code=400, detail="Email already registered")
    if user is None:
        raise HTTPException(status_code=404, detail="User not found")
    return user


@router.post("/token")
async def login(
    form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
    db: DbDep,
) -> dict[str, str]:
    service = UserService(db)
    token = await service.authenticate(form_data.username, form_data.password)
    if token is None:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )
    return {"access_token": token, "token_type": "bearer"}
```

---

## Service Layer

```python
# app/services/user_service.py
from datetime import datetime, timedelta, timezone

from jose import jwt
from passlib.context import CryptContext
from sqlalchemy import func, select

View on GitHub (pinned to 01e15490f0)

Solutions

  1. Front-end: show the same "invalid credentials" message for both unknown-user and wrong-password; never reveal which.
  2. Ensure `UserService.authenticate` runs the password hash verify even when the user is not found, so timing does not leak existence.
  3. Confirm the client sends `application/x-www-form-urlencoded` with `username` and `password` fields (OAuth2 form), not JSON.
  4. Check rate limiting on `/token` to slow brute force.

Example fix

# before
token = await service.authenticate(form_data.username, form_data.password)
if token is None:
    raise HTTPException(status_code=401, detail="Incorrect username or password", headers={"WWW-Authenticate":"Bearer"})

# after — identical path; add brute-force throttle
from slowapi import Limiter
@router.post("/token")
@limiter.limit("5/minute")
async def login(request: Request, form_data: ...):
    ...
    if token is None:
        raise HTTPException(status_code=401, detail="Incorrect username or password", headers={"WWW-Authenticate":"Bearer"})
Defensive patterns

Strategy: try-catch

Validate before calling

null

Type guard

null

Try / catch

from fastapi import HTTPException
try:
    token = await client.post('/token', data={'username':u,'password':p})
except HTTPException as e:
    if e.status_code == 401:
        show_error('Incorrect username or password')  # do NOT reveal which
    raise

Prevention

When it happens

Trigger: POST `/token` with `OAuth2PasswordRequestForm` whose `username` does not exist OR whose `password` does not hash-match. Either case yields `token is None` → 401.

Common situations: Wrong password typo. User registered with email but tries username. Account was deleted. Timing-attack-prone compare in `authenticate` (use `secrets.compare_digest` / `pwd_ctx.verify`).

Related errors


AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13). Data as JSON: /api/errors/bb875db63562b689. Report an issue: GitHub.