Password Hashing & User Management · learncode.live

Why Hash Passwords?

Storing plain-text passwords is the most critical security mistake you can make. Hashing transforms a password into a fixed-length string that cannot be reversed.

ApproachSecurityReversible
Plain text❌ CriticalYes
MD5 / SHA-1❌ BrokenWith lookup tables
bcrypt / argon2✅ StrongPractically impossible

FastAPI + passlib with bcrypt provides industry-standard password hashing out of the box.

Setting Up passlib + bcrypt

pip install passlib[bcrypt] bcrypt
from passlib.context import CryptContext

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
  • CryptContext manages multiple hashing schemes (you can migrate algorithms later)
  • deprecated="auto" automatically flags old algorithms so they are re-hashed on login
  • bcrypt includes a salt automatically - no need to manage salts manually

hash_password()

def hash_password(password: str) -> str:
    return pwd_context.hash(password)
  • bcrypt output includes the algorithm, cost factor, salt, and hash in a single string: $2b$12$LJ3m4ys3Lk0TSwHnbfdZqu3dQmRV1sLXzBwlCqE.b3eXKz0N0Hbwq
  • The $2b$12$ prefix identifies bcrypt with cost factor 12
  • Each call produces a different hash because of the random salt

verify_password()

def verify_password(plain_password: str, hashed_password: str) -> bool:
    return pwd_context.verify(plain_password, hashed_password)
  • Extracts the salt and algorithm from the stored hash
  • Hashes the plain-text password with the same salt and compares
  • Constant-time comparison prevents timing attacks

User Registration Flow

Pydantic Models

from pydantic import BaseModel, EmailStr

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

class UserOut(BaseModel):
    username: str
    email: EmailStr
    disabled: bool = False

Registration Endpoint

from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session

router = APIRouter()

@router.post("/register", response_model=UserOut, status_code=status.HTTP_201_CREATED)
async def register(user: UserCreate, db: Session = Depends(get_db)):
    existing = db.query(User).filter(
        (User.username == user.username) | (User.email == user.email)
    ).first()
    if existing:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Username or email already registered",
        )
    hashed = hash_password(user.password)
    db_user = User(
        username=user.username,
        email=user.email,
        hashed_password=hashed,
    )
    db.add(db_user)
    db.commit()
    db.refresh(db_user)
    return db_user
  • Never return the hashed password in the response
  • Validate uniqueness of username and email before inserting
  • Always hash the password before storing in the database
  • Return 201 Created for successful registration

get_current_user Dependency

from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")

async def get_current_user(token: str = Depends(oauth2_scheme)):
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
    except JWTError:
        raise credentials_exception

    user = get_user(username=username)
    if user is None:
        raise credentials_exception
    return user

User Response Model

Always separate input models from output models to avoid leaking sensitive data:

class UserInDB(UserOut):
    hashed_password: str

class UserOut(BaseModel):
    username: str
    email: EmailStr
    disabled: bool = False

    model_config = {"from_attributes": True}
@app.get("/users/me", response_model=UserOut)
async def read_users_me(current_user: User = Depends(get_current_user)):
    return current_user
  • UserInDB extends the output model with hashed_password for internal use
  • The endpoint’s response_model=UserOut strips the password automatically
  • Never expose UserInDB in any API response schema

Complete Auth Flow

Client                    FastAPI                    Database
  │                         │                          │
  │── POST /register ──────►│                          │
  │   {username,email,pw}   │── hash_password(pw) ───►│
  │                         │── INSERT user ──────────►│
  │◄── 201 {username,email}─│                          │
  │                         │                          │
  │── POST /token ─────────►│                          │
  │   {username,password}   │── verify_password() ────►│
  │                         │── create JWT ───────────►│
  │◄── {access_token} ──────│                          │
  │                         │                          │
  │── GET /users/me ───────►│                          │
  │   Authorization: Bearer │── jwt.decode() ─────────►│
  │◄── 200 {username,email}─│                          │

Key Takeaways

  • Use passlib with bcrypt scheme - never write custom hashing logic
  • hash_password() salts and hashes in one call; verify_password() compares in constant time
  • The registration endpoint must validate uniqueness and return a safe UserOut model
  • get_current_user decodes the JWT and fetches the user - inject it into any protected route
  • Separate UserCreate (input), UserOut (safe output), and UserInDB (internal) models
  • Never return hashed_password from any API endpoint
  • Always hash passwords before storing, never before comparison
Courses