NeXuS

Auth Flow

NeXuS uses JWT access tokens with httpOnly refresh token cookies for secure authentication.

Flow Diagram

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     POST /auth/login      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Browser  β”‚ ─────────────────────────▢│ Auth Service  β”‚
β”‚           │◀───────────────────────── β”‚              β”‚
β”‚           β”‚  accessToken (JSON body)  β”‚  PostgreSQL   β”‚
β”‚           β”‚  refresh_token (cookie)   β”‚  bcrypt hash  β”‚
β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜                           β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
      β”‚
      β”‚  Every 13 minutes:
      β”‚  POST /auth/refresh
      β”‚  (cookie sent automatically)
      β”‚
      β–Ό
  New accessToken returned
  Old refresh token rotated

Token Details

Token Storage Lifetime Format
Access token Memory (useState) 15 minutes JWT (HS256)
Refresh token httpOnly cookie 7 days JWT (HS256)

AuthProvider

The AuthProvider context manages the full auth lifecycle:

import { AuthProvider, useAuth } from '@/lib/AuthContext'

// In root layout
<AuthProvider>
  {children}
</AuthProvider>

// In any component
const { user, accessToken, loading, login, logout, register } = useAuth()

On Mount (Session Restore)

  1. AuthProvider calls POST /auth/refresh using the httpOnly cookie
  2. If successful, receives a new access token
  3. Calls GET /auth/me with the token to get user profile
  4. Sets user and accessToken in state

Auto-Refresh

An interval refreshes the access token every 13 minutes (before the 15-minute expiry):

useEffect(() => {
  if (!accessToken) return
  const id = setInterval(async () => {
    const token = await authApi.refresh()
    if (token) setAccessToken(token)
    else { setUser(null); setAccessToken(null) }
  }, 13 * 60 * 1000)
  return () => clearInterval(id)
}, [accessToken])

If the refresh fails, the user is logged out.

Token Rotation

Each call to /auth/refresh:

  1. Verifies the refresh token JWT
  2. Looks up the token hash in PostgreSQL
  3. Deletes the old token hash
  4. Issues a new refresh token
  5. Stores the new token hash in PostgreSQL
  6. Sets the new refresh token cookie

This ensures each refresh token can only be used once.

Auth API Client

The authApi object in lib/auth.ts handles all auth HTTP requests:

authApi.login(email, password)    // β†’ { accessToken, user }
authApi.register(username, email, password)  // β†’ void
authApi.refresh()                 // β†’ accessToken | null
authApi.me(accessToken)           // β†’ User | null
authApi.logout(accessToken)       // β†’ void

All requests use credentials: 'include' to send/receive httpOnly cookies.

Protected Routes

The ProtectedRoute component wraps dashboard pages:

Security Properties