Difficulty: Advanced
Storing passwords securely is one of the most critical security requirements for any application. Passwords must never be stored as plain text - if your database is compromised, all user passwords would be exposed. Instead, passwords are hashed using a one-way cryptographic function that converts them into a fixed-length string that cannot be reversed.
bcrypt is the recommended hashing algorithm for passwords. It is specifically designed for password hashing with built-in salting and configurable computational cost. A salt is a random string added to each password before hashing, ensuring that identical passwords produce different hashes. The cost factor (or 'rounds') controls how slow the hashing is - slower is more secure because it makes brute-force attacks impractical.
In Python, the passlib library provides a high-level interface for password hashing. Its CryptContext class supports multiple hashing schemes and handles versioning. You configure it with the bcrypt scheme and use hash() to create a hash and verify() to check a password against a hash. Passlib also supports deprecated schemes, allowing you to migrate from weaker algorithms over time.
The typical flow is: during registration, hash the password and store only the hash in the database. During login, retrieve the stored hash and use verify() to check if the provided password matches. The verify function re-hashes the provided password with the same salt and compares the results. It also handles timing-safe comparison to prevent timing attacks.
Password hashing should be the last step before storage and the first step during verification. Never log passwords, never transmit them in URL parameters, and always use HTTPS to encrypt the transmission. The backend should receive the plain password (over HTTPS), immediately hash it, and discard the plain text.
Note that bcrypt has a maximum input length of 72 bytes. For passwords longer than 72 characters, only the first 72 bytes are used. If you need to support longer passwords, pre-hash with SHA-256 before bcrypt (this is what passlib's bcrypt_sha256 scheme does).
from passlib.context import CryptContext
# Configure password hashing
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# Hash a password
plain_password = "my-secure-password"
hashed = pwd_context.hash(plain_password)
print(hashed)
# $2b$12$LJ3m4ys2ZzK5v6Q8K5v6Q8K5v6Q8K5v6Q8K5v6Q8K5...
# Different every time due to random salt!
# Verify a password against a hash
is_valid = pwd_context.verify("my-secure-password", hashed)
print(is_valid) # True
is_valid = pwd_context.verify("wrong-password", hashed)
print(is_valid) # False
# Hash is different each time (different salt)
hash1 = pwd_context.hash("same-password")
hash2 = pwd_context.hash("same-password")
print(hash1 == hash2) # False (different salts)
# But both verify correctly:
print(pwd_context.verify("same-password", hash1)) # True
print(pwd_context.verify("same-password", hash2)) # True
CryptContext provides hash() and verify() methods. Each hash() call produces a different result because of the random salt. verify() extracts the salt from the stored hash and re-hashes to compare. Never compare hashes directly.
from fastapi import FastAPI, Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from passlib.context import CryptContext
from pydantic import BaseModel
from jose import jwt
from datetime import datetime, timedelta, timezone
app = FastAPI()
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
SECRET_KEY = "your-secret-key"
ALGORITHM = "HS256"
# In-memory user store (use a database in production)
users_db: dict[str, dict] = {}
class UserCreate(BaseModel):
username: str
password: str
email: str
@app.post("/register", status_code=201)
def register(user: UserCreate):
if user.username in users_db:
raise HTTPException(status_code=409, detail="Username taken")
hashed_password = pwd_context.hash(user.password)
users_db[user.username] = {
"username": user.username,
"email": user.email,
"hashed_password": hashed_password,
}
return {"username": user.username, "email": user.email}
@app.post("/token")
def login(form_data: OAuth2PasswordRequestForm = Depends()):
user = users_db.get(form_data.username)
if not user:
raise HTTPException(status_code=401, detail="Invalid credentials")
if not pwd_context.verify(form_data.password, user["hashed_password"]):
raise HTTPException(status_code=401, detail="Invalid credentials")
token = jwt.encode(
{"sub": user["username"], "exp": datetime.now(timezone.utc) + timedelta(minutes=30)},
SECRET_KEY, algorithm=ALGORITHM,
)
return {"access_token": token, "token_type": "bearer"}
Registration hashes the password before storage. Login retrieves the stored hash and uses verify() to check the password. The same error message is used for both wrong username and wrong password to prevent username enumeration.
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash_password(password: str) -> str:
"""Hash a plain text password."""
return pwd_context.hash(password)
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Verify a plain password against a hash."""
return pwd_context.verify(plain_password, hashed_password)
# Usage in a user service
class UserService:
def __init__(self, db: dict):
self.db = db
def create_user(self, username: str, password: str, email: str):
if username in self.db:
raise ValueError("Username exists")
self.db[username] = {
"username": username,
"email": email,
"hashed_password": hash_password(password),
}
return self.db[username]
def authenticate(self, username: str, password: str):
user = self.db.get(username)
if not user:
return None
if not verify_password(password, user["hashed_password"]):
return None
return user
Extract hashing logic into helper functions for reuse. The UserService encapsulates user creation (with hashing) and authentication (with verification). This separation makes the code testable and maintainable.
bcrypt, Hashing, Salt, Passlib, Verification, Security