Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 35 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,43 @@ This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-

## Auth flow & protected routes

Authentication uses a **dual-storage strategy**:
### Token storage strategy

- **`localStorage`** (`auth_token`) — used by the browser API client to attach `Authorization: Bearer <token>` headers.
- **`auth_token` cookie** — mirrored on login/logout so Next.js middleware can read the session on the edge without `localStorage` (which is unavailable in middleware).
Authentication uses a **dual-storage strategy** for the access token and a separate localStorage-only store for the refresh token:

After a successful login, `api.setToken()` writes to both stores. `api.clearToken()` removes both.
| Token | Storage | Purpose |
|---|---|---|
| `access_token` | `localStorage` (`auth_token`) + cookie (`auth_token`) | Attached as `Authorization: Bearer` on every API request. Cookie mirrors localStorage so Next.js middleware can check auth server-side without accessing localStorage (unavailable in middleware). |
| `refresh_token` | `localStorage` (`refresh_token`) only | Used exclusively by the token-refresh flow to obtain a new access token. Never sent to cookies or exposed to middleware. |

Why not store the refresh token in a cookie? The refresh token is only consumed by the client-side JavaScript that calls `/auth/refresh`. Keeping it out of cookies reduces its exposure surface — middleware and server components never need it.

### Silent token refresh

When any API request returns HTTP 401 with `code: "ACCESS_TOKEN_EXPIRED"`, the client automatically:

1. Calls `POST /auth/refresh` with the stored refresh token.
2. Stores the new access token (and rotates the refresh token if the backend returns a new one).
3. Retries the original request with the new token.

Concurrent 401s are handled via a **single-flight pattern** (`src/lib/auth/refresh.ts`): only one refresh request is ever in flight at a time. Requests that arrive while a refresh is pending are queued and resolved with the same new token once the refresh completes.

On refresh failure (invalid/expired refresh token):
- All stored tokens are cleared.
- The user is redirected to `/login`.
- All queued requests are rejected.

Infinite refresh loops are prevented by the `_retry` flag on retried requests — a retried request that receives another 401 is thrown as an `ApiError` rather than triggering another refresh.

### Relevant source files

| File | Role |
|---|---|
| `src/lib/auth/token-store.ts` | Read/write/clear for both token types |
| `src/lib/auth/refresh.ts` | Single-flight refresh logic |
| `src/lib/api/client.ts` | HTTP client with 401 interceptor |
| `src/lib/auth/session.ts` | Cookie helpers + middleware token reader |
| `src/lib/auth/session.server.ts` | Server Component token reader |

### Protected routes

Expand All @@ -29,18 +60,6 @@ These paths are reachable without a token:

Authenticated users who visit `/login` or `/signup` are redirected to `/dashboard`.

### Session helpers

`src/lib/auth/session.ts` exports:

- `getTokenFromRequest(request)` / `isAuthenticatedFromRequest(request)` — for middleware
- `setAuthCookie()` / `clearAuthCookie()` — client-side cookie sync

`src/lib/auth/session.server.ts` exports:

- `getToken()` — async, for Server Components (`cookies()`)
- `isAuthenticated()`

## Getting Started

First, run the development server:
Expand Down
Loading