Cheat SheetsFull-Stack IntegrationAuth in the Browser

Auth in the Browser — Cheat Sheet

Full-Stack Integration · 4 topics. Download the PDF or the Instagram carousel and share it.

Cheat Sheet · AiCanCode.org
Auth in the Browser
Full-Stack Integration4 topicsQuick revision reference
1

Session Cookies, JWTs and OAuth

Three mechanisms with different trade-offs. The real question is not which is more modern, it is whether you can revoke a credential before it expires.

  • A session cookie is an opaque id whose meaning lives server-side, which makes revocation immediate
  • A JWT carries readable claims verified by signature and cannot be revoked before it expires
  • Never put secrets in a JWT and always pass algorithms explicitly when decoding
  • Short-lived access tokens plus refresh tokens are the standard workaround for JWT revocation
  • Use the Authorization Code flow with PKCE, exchange the code server-side, and then issue your own session
Opaque id, state on the server
# Login
session_id = secrets.token_urlsafe(32)
await redis.setex(f"session:{session_id}", 86400, json.dumps({"user_id": u.id}))
response.set_cookie("sid", session_id, httponly=True, secure=True,
                    samesite="lax", max_age=86400)

# Every request
data = await redis.get(f"session:{request.cookies['sid']}")

# Logout — genuinely gone, immediately
await redis.delete(f"session:{session_id}")

# Properties:
#   + revoke instantly, log out everywhere, change a role mid-session
#   + the cookie is opaque, so nothing leaks if it is read
#   + no size limit on what you store
#   - a lookup per request (Redis: sub-millisecond, rarely the issue)
#   - state to run and scale

# "Sessions do not scale" is repeated far more often than it is true.
# Redis handles this trivially at the size almost every product is.
2

Where the Credential Lives — XSS and CSRF

localStorage is exposed to any script on the page; a cookie is sent automatically by any site. Each storage choice picks which attack you have to defend against.

  • localStorage is readable by any script, so one XSS — including one in a dependency — takes every session
  • An httpOnly cookie cannot be read by JavaScript but is sent automatically, which is what CSRF exploits
  • XSS defeats every storage option, so a Content-Security-Policy and escaping are the primary defence
  • SameSite=Lax blocks cross-site POSTs; a cross-site API needs SameSite=None with Secure plus CSRF tokens
  • A cookie that works on localhost can fail in production because two real domains are cross-site while two localhost ports are not
Every option loses to XSS; httpOnly limits the damage
// localStorage
//   readable by any script on the origin — one XSS takes everything
//   survives a tab close, so a shared machine keeps the session
//   immune to CSRF, easy across origins
localStorage.setItem('token', jwt)          // convenient, and exposed

// httpOnly cookie
//   JavaScript cannot read it, so XSS cannot exfiltrate it
//   sent automatically -> needs SameSite and/or a CSRF token
Set-Cookie: sid=...; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=86400

// In-memory (a JS variable or React state)
//   safest against persistence — gone on refresh, which is the point
//   needs a refresh cookie to restore the session on reload
//   still readable by an XSS while the page is open

// sessionStorage
//   per-tab, cleared on close. Same XSS exposure as localStorage.

// The honest summary:
//   XSS defeats every client-side storage option. httpOnly only
//   stops the token being READ — an XSS can still make requests as
//   the user from the page itself.
//   So preventing XSS is the primary defence, and storage choice is
//   damage limitation.
3

Refresh Tokens, Expiry and Logout

Short access tokens buy back revocation, at the cost of a refresh flow that has to handle concurrent requests, rotation and theft detection.

  • Short access tokens plus a long refresh token restore revocation to a stateless scheme
  • Scope the refresh cookie by path and store only its hash — it is a credential
  • Rotate the refresh token on every use, and treat reuse of a rotated token as theft: revoke the whole family
  • Concurrent 401s must share a single in-flight refresh promise or rotation logs the user out
  • A complete logout revokes server-side, clears client state and clears the query cache; "log out everywhere" needs a version or a revoke-all
Short access, long path-scoped refresh
# Access token — short, sent with every request
#   5-15 minutes. In memory on the client, or a Bearer header.
# Refresh token — long, sent only to /auth/refresh
#   7-30 days. httpOnly cookie, Path=/auth/refresh, and stored
#   server-side so it can be revoked.

@router.post("/auth/login")
async def login(body: LoginIn, response: Response):
    user = await authenticate(body.email, body.password)
    access = create_jwt(user.id, ttl=timedelta(minutes=15))
    refresh = secrets.token_urlsafe(48)
    await db.store_refresh(user.id, hash(refresh),          # HASH it —
                           expires=timedelta(days=30))       # it is a credential
    response.set_cookie("rt", refresh, httponly=True, secure=True,
                        samesite="lax", path="/auth/refresh")
    return {"access_token": access, "user": public(user)}

# Path scoping means the refresh cookie is not sent with ordinary API
# calls at all, which shrinks its exposure considerably.

# Store a hash, never the token itself — a leaked database should not
# hand over live sessions.
4

Authorisation — Who Enforces What

The frontend decides what to show; the backend decides what is allowed. Every rule needs to exist on both sides, and only one of them is security.

  • The frontend hides what a user cannot use; only the backend enforces it, because the bundle and the API are public
  • Gating in the UI still ships the data — strip solutions, prices and other users' fields server-side
  • Checking authentication without checking ownership is the most common API vulnerability there is
  • Put ownership and tenant filters into the query itself, and return 404 rather than 403 to avoid confirming existence
  • Send server-computed permissions to the client so both sides agree, and remember roles inside a JWT go stale until it expires
Hide in the UI, enforce and strip on the server
// Frontend — so users are not shown doors they cannot open
{user.role === 'admin' && <NavLink to="/admin">Admin</NavLink>}
{isPro ? <Solution /> : <UpgradePrompt />}

// Backend — so the rule is actually true
@router.delete("/problems/{slug}")
async def delete_problem(slug: str, user = Depends(require_role("admin"))):
    ...

// Skipping the frontend check: a poor experience, users hitting 403s.
// Skipping the backend check: a vulnerability.

// The two failures to internalise:
//   1. Hiding a link hides nothing. curl reaches the endpoint.
//   2. Gating in the client means the DATA was still sent. If the
//      API returns the solution and the UI hides it behind a Pro
//      gate, the solution is in the network tab.
//      Strip it server-side:
if not user.is_pro:
    problem.solution = None
    problem.hints = []

// Same for prices, other users' emails, internal notes — anything
// the response contains is public to whoever requested it.
Learn this free with Aria, your AI tutor → AiCanCode.org/learn/full-stack