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.
Core rule
Section titled “Core rule”A request that fails authentication must never mutate authentication state.
Correct flow
Section titled “Correct flow”- Client sends request with access token
- Server verifies access token
- ✅ valid → request proceeds
- ❌ expired/invalid →
401 Unauthorized
- Client explicitly calls
/auth/refresh - Server verifies refresh token
- Server issues new access token (and optionally rotates refresh token)
- Client retries original request
Why inline refresh is wrong
Section titled “Why inline refresh is wrong”1. Breaks security boundaries
Section titled “1. Breaks security boundaries”- Expired token ≠ authenticated request
- Auto-refresh upgrades an unauthenticated request silently
- Enables replay and abuse scenarios
2. Overexposes refresh tokens
Section titled “2. Overexposes refresh tokens”- Refresh tokens are long-lived and high-privilege
- Inline refresh uses them on every failed request
- Increases attack surface and CSRF risk
3. Violates HTTP semantics
Section titled “3. Violates HTTP semantics”- Auth failure should return
401 - Refreshing during
GETmutates auth state - Breaks idempotency, caching, proxies, and debugging
4. Skips critical security checks
Section titled “4. Skips critical security checks”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.
5. Performance gain is negligible
Section titled “5. Performance gain is negligible”- One extra request every ~15 minutes
- Cost is insignificant compared to normal API work
- Security tradeoff is not worth it
Correct optimization
Section titled “Correct optimization”Use client-side silent refresh:
- On
401, client calls/auth/refresh - Retry original request automatically
- Appears seamless without compromising security
Summary
Section titled “Summary”| Inline refresh | Explicit refresh |
|---|---|
| Silent privilege escalation | Explicit intent |
| Refresh token always active | Refresh token isolated |
| Breaks HTTP semantics | Standards-compliant |
| Hard to secure and audit | Easy to secure and audit |
Conclusion:
The multi-step refresh flow is intentional and necessary for secure, predictable authentication.