tiangolo/fastapi · error · HTTPException
Incorrect username or password
Error message
Incorrect username or password
What it means
HTTP Basic auth tutorial. get_current_username uses secrets.compare_digest (constant-time) on both the username ('stanleyjobson') and the password ('swordfish'), combining them in a single AND so that neither check short-circuits — preventing timing-based credential discovery. If either fails it raises HTTP 401 with WWW-Authenticate: Basic, which makes browsers show the native login prompt. Credentials are hardcoded in source for the demo.
Source
Thrown at docs_src/security/tutorial007_an_py310.py:26
security = HTTPBasic()
def get_current_username(
credentials: Annotated[HTTPBasicCredentials, Depends(security)],
):
current_username_bytes = credentials.username.encode("utf8")
correct_username_bytes = b"stanleyjobson"
is_correct_username = secrets.compare_digest(
current_username_bytes, correct_username_bytes
)
current_password_bytes = credentials.password.encode("utf8")
correct_password_bytes = b"swordfish"
is_correct_password = secrets.compare_digest(
current_password_bytes, correct_password_bytes
)
if not (is_correct_username and is_correct_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Basic"},
)
return credentials.username
@app.get("/users/me")
def read_current_user(username: Annotated[str, Depends(get_current_username)]):
return {"username": username}
View on GitHub (pinned to 3e8d1526d8)
Solutions
- Send Authorization: Basic <base64('stanleyjobson:swordfish')>.
- Clear the browser's cached Basic credentials (close all tabs / restart) and re-authenticate.
- Move credentials to environment variables or a secrets store and never hardcode them in source.
Example fix
// before
correct_username_bytes = b"stanleyjobson"
correct_password_bytes = b"swordfish"
// after (load from env, keep constant-time compare)
import os
_correct_u = os.environ["BASIC_USER"].encode("utf8")
_correct_p = os.environ["BASIC_PASSWORD"].encode("utf8")
is_correct_username = secrets.compare_digest(current_username_bytes, _correct_u)
is_correct_password = secrets.compare_digest(current_password_bytes, _correct_p) Defensive patterns
Strategy: validation
Validate before calling
# Build the Basic header locally and sanity-check before sending
import base64, secrets
USER, PW = b"stanleyjobson", b"swordfish"
def basic_header(username: str, password: str) -> str | None:
if not (secrets.compare_digest(username.encode(), USER)
and secrets.compare_digest(password.encode(), PW)):
return None
token = base64.b64encode(f"{username}:{password}".encode()).decode()
return f"Basic {token}" Type guard
from typing import TypeGuard
def is_valid_basic_pair(pair: tuple) -> TypeGuard[tuple[str, str]]:
u, p = pair
return isinstance(u, str) and isinstance(p, str) and u and p Try / catch
import httpx
try:
r = httpx.get("/users/me", headers={"Authorization": basic_header(u, p)})
r.raise_for_status()
except httpx.HTTPStatusError as e:
if e.response.status_code == 401:
# browser will re-prompt via WWW-Authenticate: Basic
prompt_credentials() Prevention
- Construct the Basic header with base64('user:pass'); verify it round-trips.
- Clear cached Basic credentials in the browser when they change.
- Keep credentials in env/secrets, never hardcoded in source.
When it happens
Trigger: GET /users/me with no Authorization header, or with Basic credentials whose base64-decoded username is not 'stanleyjobson' or whose password is not 'swordfish'. A browser prompted by WWW-Authenticate: Basic will resubmit whatever the user types.
Common situations: Typo in credentials; browser cached old Basic creds and keeps replaying them; mis-base64-encoded test header; demo creds rotated but the hardcoded bytes were not updated.
Related errors
- Incorrect username or password
- Inactive user
- Incorrect username or password
- Not authenticated
- Inactive user
AI-assisted analysis of tiangolo/fastapi@3e8d1526d8 (2026-08-11).
Data as JSON: /api/errors/b794d7a3932b3334.
Report an issue: GitHub.