Authentication is the part of every backend where I slow down and stop trusting frameworks to handle everything for me. It's the attack surface that gets abused most, and it's the one where developers cut the most corners.
The JWT trap
Most people reach for JWTs because they're stateless and feel modern. The problem is that stateless also means you can't revoke them. If a token is compromised, you're stuck waiting for it to expire. I've seen systems with 30-day JWTs and no refresh rotation — that's basically a session that can't be killed.
"Stateless auth is a tradeoff, not a default. Know what you're trading before you ship it."
What I actually do
Short-lived access tokens (15 min), refresh tokens stored httpOnly, and a token family invalidation strategy. When a refresh token is used, issue a new pair and invalidate the old one. If a revoked refresh token is ever used — assume the refresh token was stolen, kill the entire family. It adds a round trip but it's worth it.
# Refresh token rotation with family invalidation
async def rotate_refresh_token(old_token: str, db: AsyncSession):
token = await db.get(RefreshToken, old_token)
if not token:
raise HTTPException(401, "Invalid token")
if token.revoked:
# Reuse detected — invalidate entire family
await db.execute(
update(RefreshToken)
.where(RefreshToken.family_id == token.family_id)
.values(revoked=True)
)
await db.commit()
raise HTTPException(401, "Token reuse detected")
token.revoked = True
new_token = RefreshToken(family_id=token.family_id, user_id=token.user_id)
db.add(new_token)
await db.commit()
return new_tokenThe stuff people forget
Rate limit your auth endpoints. Not just login — password reset too. Timing-safe compare for secrets. Never log tokens. Rotate your signing keys on a schedule. These aren't exotic — they're just the baseline that gets skipped when you're moving fast.