tiangolo/fastapi · error · HTTPException
Incorrect username or password
Error message
Incorrect username or password
What it means
Raised in the /token handler when fake_users_db.get(form_data.username) returns None, i.e. the submitted username is not a known account. The message is intentionally identical to the password-mismatch branch at line 85 so that an attacker cannot distinguish 'user does not exist' from 'wrong password' (anti user-enumeration). The tutorial uses HTTP 400; the OAuth2/RFC 6749 convention is 401 with WWW-Authenticate.
Solutions
- POST with username exactly matching a fake_users_db key (johndoe or alice) and the matching password.
- Confirm the form field is named 'username' and the Content-Type is application/x-www-form-urlencoded.
- If case-insensitive logins are desired, normalize input with username.strip().lower() before the lookup.
Example fix
// before
user_dict = fake_users_db.get(form_data.username)
if not user_dict:
raise HTTPException(status_code=400, detail="Incorrect username or password")
// after
user_dict = fake_users_db.get(form_data.username.strip())
if not user_dict:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username or password", headers={"WWW-Authenticate": "Bearer"}) Defensive patterns
Strategy: validation
Validate before calling
# Validate before POST /token
KNOWN_USERS = {"johndoe", "alice"}
def valid_login_form(username: str, password: str) -> bool:
return (
isinstance(username, str) and isinstance(password, str)
and username.strip() in KNOWN_USERS
and len(password) > 0
) Type guard
from typing import TypeGuard
def is_non_empty_creds(pair: tuple) -> TypeGuard[tuple[str, str]]:
u, p = pair
return isinstance(u, str) and isinstance(p, str) and u.strip() != "" and p != "" Try / catch
import httpx
try:
r = httpx.post("/token", data={"username": u, "password": p})
except httpx.HTTPStatusError as e:
if e.response.status_code in (400, 401):
# treat both branches identically (server hides which failed)
show_generic_login_error() Prevention
- Trim and (if your domain allows) case-normalize the username before sending.
- Send the form field as 'username' (not 'user'/'email') with Content-Type application/x-www-form-urlencoded.
- Treat 400 and 401 identically on the client; never infer which field was wrong.
When it happens
Trigger: POST /token with an OAuth2PasswordRequestForm whose username field is not exactly 'johndoe' or 'alice' (case/whitespace sensitive): typos, 'JohnDoe', a leading space, or the field named 'user'/'email' instead of 'username'.
Common situations: Case mismatch on a case-sensitive store; copy-paste whitespace; client sending JSON body instead of application/x-www-form-urlencoded; integration tests using a stale fixture username after the db dict was edited.
Related errors
AI-assisted analysis of tiangolo/fastapi@3e8d1526d8 (2026-08-11).
Data as JSON: /api/errors/b7bb8d01417dc191.
Report an issue: GitHub.
Appendix: source
Thrown at docs_src/security/tutorial003_an_py310.py:81
detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
return user
async def get_current_active_user(
current_user: Annotated[User, Depends(get_current_user)],
):
if current_user.disabled:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user
@app.post("/token")
async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):
user_dict = fake_users_db.get(form_data.username)
if not user_dict:
raise HTTPException(status_code=400, detail="Incorrect username or password")
user = UserInDB(**user_dict)
hashed_password = fake_hash_password(form_data.password)
if not hashed_password == user.hashed_password:
raise HTTPException(status_code=400, detail="Incorrect username or password")
return {"access_token": user.username, "token_type": "bearer"}
@app.get("/users/me")
async def read_users_me(
current_user: Annotated[User, Depends(get_current_active_user)],
):
return current_user
View on GitHub (pinned to 3e8d1526d8)