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

  1. Send Authorization: Basic <base64('stanleyjobson:swordfish')>.
  2. Clear the browser's cached Basic credentials (close all tabs / restart) and re-authenticate.
  3. 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

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


AI-assisted analysis of tiangolo/fastapi@3e8d1526d8 (2026-08-11). Data as JSON: /api/errors/b794d7a3932b3334. Report an issue: GitHub.