Cheatsheet for auth. Long-form: Textbook Ch 6 .

Hashing

from passlib.context import CryptContext
pwd = CryptContext(schemes=["argon2"], deprecated="auto")

hashed = pwd.hash(plain)
ok = pwd.verify(plain, hashed)

argon2 default in 2026; bcrypt acceptable.

OAuth2 password flow

from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from datetime import datetime, timedelta
import jwt

SECRET = settings.secret_key
ALG = "HS256"
ACCESS_MIN = 30

oauth2 = OAuth2PasswordBearer(tokenUrl="token")

@app.post("/token")
async def token(form: OAuth2PasswordRequestForm = Depends(), db = Depends(get_db)):
    user = await find_user(db, form.username)
    if not user or not pwd.verify(form.password, user.hashed_password):
        raise HTTPException(401, "Invalid creds")
    expire = datetime.utcnow() + timedelta(minutes=ACCESS_MIN)
    t = jwt.encode({"sub": str(user.id), "exp": expire}, SECRET, ALG)
    return {"access_token": t, "token_type": "bearer"}

async def current_user(token: str = Depends(oauth2), db = Depends(get_db)) -> User:
    try:
        payload = jwt.decode(token, SECRET, algorithms=[ALG])   # always pin algorithms!
    except jwt.PyJWTError:
        raise HTTPException(401, "Invalid token")
    user = await db.get(User, int(payload["sub"]))
    if not user: raise HTTPException(401, "User missing")
    return user

Refresh tokens

class RefreshToken(Base):
    id: Mapped[int] = mapped_column(primary_key=True)
    user_id: Mapped[int]
    token: Mapped[str] = mapped_column(unique=True, index=True)
    expires_at: Mapped[datetime]
    revoked: Mapped[bool] = mapped_column(default=False)

@app.post("/refresh")
async def refresh(req: RefreshIn, db = Depends(get_db)):
    rec = await db.scalar(select(RefreshToken).where(
        RefreshToken.token == req.refresh_token,
        RefreshToken.revoked == False,
        RefreshToken.expires_at > datetime.utcnow(),
    ))
    if not rec: raise HTTPException(401)
    return {"access_token": make_access_token(rec.user_id), "token_type": "bearer"}

Short-lived access (15 min) + long-lived refresh (30d, revocable).

Sessions (alternative)

from itsdangerous import URLSafeSerializer
serializer = URLSafeSerializer(SECRET)

@app.post("/login")
async def login(form: OAuth2PasswordRequestForm = Depends(), response: Response = None, db = Depends(get_db)):
    user = await authenticate(form.username, form.password, db)
    sid = secrets.token_urlsafe(32)
    await redis.set(f"sess:{sid}", str(user.id), ex=86400 * 30)
    response.set_cookie("session", sid, httponly=True, secure=True, samesite="lax")
    return {"ok": True}

async def current_user_session(session: str = Cookie(...), db = Depends(get_db)) -> User:
    uid = await redis.get(f"sess:{session}")
    if not uid: raise HTTPException(401)
    return await db.get(User, int(uid))

API keys

from fastapi.security import APIKeyHeader

api_key_h = APIKeyHeader(name="X-API-Key")

async def get_api_key(key: str = Depends(api_key_h), db = Depends(get_db)) -> ApiKey:
    rec = await db.scalar(select(ApiKey).where(
        ApiKey.key_hash == sha256(key), ApiKey.revoked == False,
    ))
    if not rec: raise HTTPException(401, "Invalid key")
    return rec

Hash keys at rest like passwords.

Scopes

oauth2 = OAuth2PasswordBearer(
    tokenUrl="token",
    scopes={"read": "...", "write": "...", "admin": "..."},
)

from fastapi import Security
from fastapi.security import SecurityScopes

async def user_with_scope(security_scopes: SecurityScopes, token: str = Depends(oauth2)):
    payload = jwt.decode(token, SECRET, algorithms=[ALG])
    granted = payload.get("scopes", [])
    for needed in security_scopes.scopes:
        if needed not in granted:
            raise HTTPException(403, f"missing scope: {needed}")
    return await db.get(User, int(payload["sub"]))

@app.post("/items", dependencies=[Security(user_with_scope, scopes=["write"])])
async def create_(...): ...

RBAC

from enum import Enum
class Role(str, Enum):
    USER = "user"; EDITOR = "editor"; ADMIN = "admin"

def require_role(*roles: Role):
    def dep(u: User = Depends(current_user)):
        if u.role not in roles: raise HTTPException(403)
        return u
    return dep

@app.delete("/posts/{id}")
async def delete_(id: int, u: User = Depends(require_role(Role.EDITOR, Role.ADMIN))): ...

Resource-level authz

async def get_editable_post(id: int, u: User = Depends(current_user), db = Depends(get_db)) -> Post:
    p = await db.get(Post, id)
    if not p: raise HTTPException(404)
    if p.author_id != u.id and not u.is_admin:
        raise HTTPException(403)
    return p

@app.patch("/posts/{id}")
async def update_(p: Post = Depends(get_editable_post), data: PostUpdate = Body(...)):
    ...

OIDC (Google et al.)

from authlib.integrations.starlette_client import OAuth

oauth = OAuth()
oauth.register(
    name="google",
    server_metadata_url="https://accounts.google.com/.well-known/openid-configuration",
    client_id=settings.google_client_id,
    client_secret=settings.google_client_secret,
    client_kwargs={"scope": "openid email profile"},
)

@app.get("/auth/google")
async def google_login(req: Request):
    return await oauth.google.authorize_redirect(req, req.url_for("google_callback"))

@app.get("/auth/google/callback", name="google_callback")
async def google_cb(req: Request, db = Depends(get_db)):
    token = await oauth.google.authorize_access_token(req)
    info = token.get("userinfo")
    user = await find_or_create_user(db, info["email"])
    # issue your own session/JWT
    return RedirectResponse("/")

Rate limit on login

from slowapi import Limiter
limiter = Limiter(key_func=lambda req: req.client.host)

@app.post("/token")
@limiter.limit("5/minute")
async def token(...): ...

Plus: account lockout after N failures.

Password reset (no enumeration)

@app.post("/forgot-password")
async def forgot(req: ForgotIn, db = Depends(get_db)):
    user = await db.scalar(select(User).where(User.email == req.email))
    if user:                       # always same response shape
        t = secrets.token_urlsafe(32)
        await redis.set(f"pwreset:{t}", str(user.id), ex=3600)
        await send_email(user.email, f"...?token={t}")
    return {"ok": True}            # don't leak whether email exists

Security headers

@app.middleware("http")
async def sec(req, call_next):
    resp = await call_next(req)
    resp.headers["X-Content-Type-Options"] = "nosniff"
    resp.headers["X-Frame-Options"] = "DENY"
    resp.headers["Strict-Transport-Security"] = "max-age=63072000; includeSubDomains; preload"
    return resp

from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://yourdomain.com"],
    allow_credentials=True, allow_methods=["*"], allow_headers=["*"],
)

Read this next

If you want my auth starter (passkeys + OIDC + JWT + sessions), it’s at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .