Skip to content

Authorization flow

model User {
id String @id @default(cuid())
legacyId Int? @unique
username String
email String @unique
emailVerified DateTime?
password String
passwordVersion String
createdAt DateTime @default(now())
updatedAt DateTime @default(now())
emailVerificationToken EmailVerificationToken?
resetPasswordToken ResetPasswordToken?
// user_preferences user_preferences?
}
model ResetPasswordToken {
id String @id @default(cuid())
token String @unique
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
userId String @unique
}
model EmailVerificationToken {
id String @id @default(cuid())
token String @unique
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
userId String @unique
}

Auth is seperate from User.

  • Auth calls user services.
  • On creation of new user, username and email are returned, awaitng email verification. No useres without verification*!

Why access tokens are NOT refreshed inline

Section titled “Why access tokens are NOT refreshed inline”

Even if an access token is expired and the refresh token is still valid, the server must not refresh tokens during the same request.

A request that fails authentication must never mutate authentication state.


  1. Client sends request with access token
  2. Server verifies access token
    • ✅ valid → request proceeds
    • ❌ expired/invalid → 401 Unauthorized
  3. Client explicitly calls /auth/refresh
  4. Server verifies refresh token
  5. Server issues new access token (and optionally rotates refresh token)
  6. Client retries original request

  • Expired token ≠ authenticated request
  • Auto-refresh upgrades an unauthenticated request silently
  • Enables replay and abuse scenarios
  • Refresh tokens are long-lived and high-privilege
  • Inline refresh uses them on every failed request
  • Increases attack surface and CSRF risk
  • Auth failure should return 401
  • Refreshing during GET mutates auth state
  • Breaks idempotency, caching, proxies, and debugging

Refresh endpoints are where you:

  • Validate passwordVersion
  • Rotate refresh tokens
  • Enforce global logout
  • Rate-limit and audit refresh attempts

Inline refresh weakens or bypasses these controls.

  • One extra request every ~15 minutes
  • Cost is insignificant compared to normal API work
  • Security tradeoff is not worth it

Use client-side silent refresh:

  • On 401, client calls /auth/refresh
  • Retry original request automatically
  • Appears seamless without compromising security

Inline refreshExplicit refresh
Silent privilege escalationExplicit intent
Refresh token always activeRefresh token isolated
Breaks HTTP semanticsStandards-compliant
Hard to secure and auditEasy to secure and audit

Conclusion:
The multi-step refresh flow is intentional and necessary for secure, predictable authentication.