codingstairs
NotesEDULifeContact
⌕Search⌘K
koen

Navigation

  • Intro
  • Blog
  • Life

Get in touch

Send without signing in. Add your email if you'd like a reply.

  • Leave a message anonymously →
  • ✉ warragon112@gmail.com
  • KakaoTalk Open Chat ↗

© 2026 codingstairs

  • Notes
  • EDU
  • Search
  • Life
  • Contact
  • Legal
  • RSS
  • GitHub
EDU›Web security foundations — JWT · OAuth · OWASP›Step 2

Step 2

JWT · refresh · rotation

0 views

JWT · refresh · rotation

The practical standard for session management — provided you get the algorithm, expiry, and blacklist right.

1. JWT anatomy

<base64url header>.<base64url payload>.<base64url signature>

Payload is encoded, not encrypted. Do not put secrets there.

2. HS256 vs RS256

Alg Key Use
HS256 symmetric single service
RS256 key pair microservices · OAuth

Use HS256 + 32-byte secret for a single app.

3. Issue (jose)

import { SignJWT } from "jose";
const SECRET = new TextEncoder().encode(process.env.JWT_SECRET!);

async function issueToken(userId: string) {
  return new SignJWT({ sub: userId })
    .setProtectedHeader({ alg: "HS256" })
    .setIssuedAt()
    .setExpirationTime("15m")
    .sign(SECRET);
}

4. Short access + long refresh

access → 15 min
refresh → 30 days

Stolen access token, max 15 min window. Refresh stays in HTTP-only cookie.

async function refreshTokens(oldRefresh: string) {
  const { payload } = await jwtVerify(oldRefresh, SECRET);
  await db.query("INSERT INTO jwt_blacklist (jti, exp) VALUES ($1, $2)",
    [payload.jti, payload.exp]);
  return { access: await issueToken(payload.sub), refresh: await issueRefresh(payload.sub) };
}

5. Blacklist or whitelist

  • Blacklist — store logged-out jtis. Smaller.
  • Whitelist — store all issued jtis. Stricter.

Blacklist is usually smaller in practice.

6. HTTP-only cookie vs Authorization header

Store XSS CSRF Convenience
HTTP-only cookie safe at risk auto for SSR
localStorage unsafe safe needs CORS config

Cookie + sameSite=lax + CSRF token is the practical combo.

7. Middleware

export async function requireAuth(req: Request) {
  const token = req.headers.get("authorization")?.replace("Bearer ", "")
    ?? req.cookies?.get("access_token")?.value;
  if (!token) throw new Error("unauthorized");
  const { payload } = await jwtVerify(token, SECRET);
  if (await isBlacklisted(payload.jti)) throw new Error("revoked");
  return payload;
}

8. Gotchas

  • Short secrets — 32 bytes minimum
  • Secrets in payload — it is only encoding
  • Too-long expiry — >1h access is risky
  • Alg downgrade ({"alg":"none"}) — whitelist algorithms
  • No TTL on blacklist → table bloat

9. Checklist

  • 32+ byte secret
  • access 15 min · refresh 30 days
  • Rotate refresh, add old to blacklist
  • Delete cookies on logout
  • HS256 only (algorithm whitelist)

Closing

JWT is "convenient but risky without expiry and rotation". Short access + refresh covers the gap.

Next

  • 03-oauth-state-pkce

← Step 1

Threat model · OWASP at a glance

Step 3 →

OAuth + state · PKCE