FastAPI Password Hashing

A database that stores passwords in plain text turns every breach into a disaster. Anyone who copies the table can log in as any user, and many people reuse the same password on other sites. Hashing solves the problem. A hash function scrambles a password into a fixed string that nobody can turn back into the original. Think of a meat grinder: you can put a steak in and get mince out, yet no machine rebuilds the steak.

Plain Text Versus Hashed Storage

Plain text (dangerous)           Hashed (safe)
───────────────────────          ─────────────────────────────────────────
email        password            email        password_hash
asha@x.com   sunshine123         asha@x.com   $argon2id$v=19$m=65536,t=3...
ravi@x.com   sunshine123         ravi@x.com   $argon2id$v=19$m=65536,t=3...
(same password visible)          (different hashes even for the same password)

Strong hashing adds a random value called a salt to each password. Two users with the same password still end up with different hashes.

Choosing an Algorithm

Fast algorithms such as MD5 or plain SHA-256 suit file checks but fail for passwords, because attackers can try billions of guesses per second. Password algorithms such as Argon2 and bcrypt run slowly on purpose. A tenth of a second feels instant for one login and punishes mass guessing.

Installing pwdlib

The pwdlib package offers a clean interface with Argon2 support.

pip install "pwdlib[argon2]"

Hashing and Verifying

from pwdlib import PasswordHash

password_hash = PasswordHash.recommended()

hashed = password_hash.hash("MySecret#2024")
print(hashed)   # $argon2id$v=19$m=65536,t=3,p=4$...

print(password_hash.verify("MySecret#2024", hashed))   # True
print(password_hash.verify("wrong-guess", hashed))     # False

The hash method creates the scrambled string. The verify method hashes the typed password with the stored salt and compares the results.

The Signup and Login Flow

SIGNUP
  User types password ──▶ hash() ──▶ store hash in database

LOGIN
  User types password ──▶ verify(typed, stored hash)
                               │
                  ┌────────────┴────────────┐
                  ▼                         ▼
               True: allow login      False: reject (401)

The server never stores or sees the original password after hashing.

Helper Functions

Wrap the library calls so the rest of the code stays tidy.

def hash_password(plain: str) -> str:
    return password_hash.hash(plain)

def verify_password(plain: str, hashed: str) -> bool:
    return password_hash.verify(plain, hashed)

A Registration Route

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()
users_db = {}

class UserCreate(BaseModel):
    email: str
    password: str

@app.post("/register", status_code=201)
def register(data: UserCreate):
    if data.email in users_db:
        raise HTTPException(status_code=400, detail="Email already registered")
    users_db[data.email] = {"email": data.email, "password_hash": hash_password(data.password)}
    return {"email": data.email}

The response returns the email only. The hash stays inside the database.

A Login Check

@app.post("/login")
def login(data: UserCreate):
    user = users_db.get(data.email)
    if not user or not verify_password(data.password, user["password_hash"]):
        raise HTTPException(status_code=401, detail="Wrong email or password")
    return {"message": "Login successful"}

The error message never reveals which part failed. A vague message stops attackers from discovering registered emails.

Password Rules Worth Enforcing

  • Require a minimum length of at least eight characters.
  • Reject passwords that appear on common-password lists.
  • Allow long passphrases and spaces.
  • Limit login attempts to slow down guessing.

Common Mistakes

Logging the Password

Request logs that print the body expose secrets. Exclude password fields from every log.

Inventing Your Own Algorithm

Home-made schemes hide weaknesses. Trust vetted libraries.

Returning the Hash

Use a response model without the hash field so it never leaves the server.

What a Breach Looks Like

Imagine an attacker copies your user table. With plain text passwords, the attacker logs in as every user within seconds. With strong hashes, the attacker holds a pile of scrambled strings. Each guess costs real computing time, and each user needs a separate effort because every hash carries its own salt.

Good hashing buys your users time. People can change their passwords on other sites before an attacker cracks anything. Strong password rules multiply the protection, since long and unusual passwords resist guessing for years.

Plan for upgrades as well. Hash libraries record the algorithm and settings inside the hash string. When security guidance changes, the app can detect an outdated hash at login, rehash the typed password with the newer settings, and save the fresh value.

Key Points

  • Hashing turns passwords into one-way strings.
  • Salts make identical passwords produce different hashes.
  • Argon2 and bcrypt suit passwords because they run slowly on purpose.
  • Use hash at signup and verify at login.
  • Keep error messages vague and hashes private.