next-token-auth 1.0.14 → 1.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,53 +1,41 @@
1
1
  # next-token-auth
2
2
 
3
- A production-grade authentication library for Next.js. Handles access tokens, refresh tokens, session management, and route protection — so you don't have to wire it all up yourself.
3
+ A production-grade authentication library for Next.js that handles the hard parts of auth so you can focus on building features.
4
4
 
5
- Works with both the App Router and Pages Router. Fully typed with TypeScript.
5
+ Works with both App Router and Pages Router. Fully typed with TypeScript.
6
6
 
7
- ---
8
-
9
- ## The Problem
10
-
11
- Authentication in Next.js involves a lot of moving parts:
7
+ > **Breaking change in v1.1.0:** The secret is now server-side only for security. You'll need to split your config and add one Route Handler file. See the Quick Start below — it takes 5 minutes.
12
8
 
13
- - Storing tokens securely
14
- - Refreshing access tokens before they expire
15
- - Keeping client and server sessions in sync
16
- - Protecting routes on both the client and server
17
- - Wiring up login, logout, and user fetching from scratch
9
+ ---
18
10
 
19
- Most projects end up with hundreds of lines of boilerplate before a single feature is built.
11
+ ## Why This Exists
20
12
 
21
- ---
13
+ Authentication in Next.js is tedious. You need to:
22
14
 
23
- ## The Solution
15
+ - Store tokens securely
16
+ - Refresh access tokens before they expire
17
+ - Keep client and server sessions in sync
18
+ - Protect routes on both the client and server
19
+ - Handle login, logout, and session restoration
20
+ - Wire up API calls with Bearer tokens
24
21
 
25
- `next-token-auth` gives you a single `AuthProvider` and a set of hooks that handle the entire auth lifecycle. You configure your API endpoints once, and the library takes care of the rest:
22
+ Most projects spend days on auth boilerplate before shipping a single feature.
26
23
 
27
- - Tokens are stored in cookies or memory
28
- - Access tokens are automatically refreshed before they expire
29
- - Sessions are restored on page load from stored tokens
30
- - Routes can be protected client-side with a hook or server-side with middleware
31
- - Every API request made through the built-in fetch wrapper gets a `Bearer` token injected automatically
24
+ `next-token-auth` gives you all of this in a single `AuthProvider` and a few hooks. Configure your API endpoints once, and the library handles the rest.
32
25
 
33
26
  ---
34
27
 
35
- ## Features
36
-
37
- - `AuthProvider` React context provider that initializes and manages auth state
38
- - `useAuth` — login, logout, refresh, and session in one hook
39
- - `useSession` read-only access to the current session
40
- - `useRequireAuth` redirects unauthenticated users, works in App Router and Pages Router
41
- - Token storage in cookies or in-memory
42
- - AES-GCM encrypted session cookies (server-side)
43
- - Automatic access token refresh on a 30-second interval (when `autoRefresh` is enabled)
44
- - 401 refresh retry built into the HTTP client
45
- - `getServerSession` read and validate the session in server components and API routes
46
- - `withAuth` — higher-order function to protect App Router route handlers
47
- - `authMiddleware` — Next.js middleware factory for edge-level route protection with guest-only route support
48
- - Flexible expiry parsing: `"15m"`, `"2h"`, `"2d"`, `"7d"`, `"1w"`, or plain seconds
49
- - Three expiry strategies: `backend`, `config`, `hybrid`
50
- - Fully typed with TypeScript generics for custom user shapes
28
+ ## What You Get
29
+
30
+ - One `<AuthProvider>` wrapper for your entire app
31
+ - Three hooks: `useAuth()`, `useSession()`, `useRequireAuth()`
32
+ - Automatic token refresh before expiry
33
+ - HttpOnly encrypted cookies (tokens never exposed to JavaScript)
34
+ - Server-side session validation via `getServerSession()`
35
+ - Next.js middleware for edge-level route protection
36
+ - Guest-only routes (redirect authenticated users away from login pages)
37
+ - Flexible expiry formats: `"15m"`, `"2h"`, `"2d"`, `"7d"`, or plain seconds
38
+ - Full TypeScript support with generics for your custom user shape
51
39
 
52
40
  ---
53
41
 
@@ -55,27 +43,20 @@ Most projects end up with hundreds of lines of boilerplate before a single featu
55
43
 
56
44
  ```bash
57
45
  npm install next-token-auth
58
- # or
59
- yarn add next-token-auth
60
- # or
61
- pnpm add next-token-auth
62
- # or
63
- bun add next-token-auth
64
46
  ```
65
47
 
66
- **Peer dependencies** (already installed in any Next.js project):
67
-
68
- ```
69
- next >= 13
70
- react >= 18
71
- react-dom >= 18
72
- ```
48
+ Peer dependencies (already in any Next.js project):
49
+ - `next >= 15.5.14`
50
+ - `react >= 18`
51
+ - `react-dom >= 18`
73
52
 
74
53
  ---
75
54
 
76
55
  ## Quick Start
77
56
 
78
- ### 1. Create your config
57
+ ### Step 1: Create your server config
58
+
59
+ This file contains your `secret` and backend API URL. Never import it in client components.
79
60
 
80
61
  ```ts
81
62
  // lib/auth.ts
@@ -88,22 +69,27 @@ interface User {
88
69
  }
89
70
 
90
71
  export const authConfig: AuthConfig<User> = {
91
- baseUrl: process.env.NEXT_PUBLIC_API_URL!,
72
+ // Your backend API base URL (no NEXT_PUBLIC_ prefix needed)
73
+ baseUrl: process.env.API_URL!,
92
74
 
75
+ // Your backend auth endpoints
93
76
  endpoints: {
94
- login: "/auth/login",
95
- refresh: "/auth/refresh",
96
- logout: "/auth/logout",
97
- me: "/auth/me",
77
+ login: "/auth/login", // POST { email, password } → returns tokens + user
78
+ refresh: "/auth/refresh", // POST { refreshToken } → returns new tokens
79
+ logout: "/auth/logout", // POST (optional)
80
+ me: "/auth/me", // GET → returns user profile (optional)
98
81
  },
99
82
 
83
+ // Route protection rules
100
84
  routes: {
101
- public: ["/", "/about"],
102
- guestOnly: ["/login", "/register"],
103
- protected: ["/dashboard/*"],
104
- redirectAuthenticatedTo: "/dashboard",
85
+ public: ["/", "/about"], // always accessible
86
+ guestOnly: ["/login", "/register"], // only when NOT logged in
87
+ protected: ["/dashboard*", "/profile*"], // requires auth
88
+ loginPath: "/login", // where to send unauthenticated users
89
+ redirectAuthenticatedTo: "/dashboard", // where to send authenticated users who hit guestOnly routes
105
90
  },
106
91
 
92
+ // Token storage settings
107
93
  token: {
108
94
  storage: "cookie",
109
95
  cookieName: "myapp.session",
@@ -111,58 +97,133 @@ export const authConfig: AuthConfig<User> = {
111
97
  sameSite: "lax",
112
98
  },
113
99
 
100
+ // Encryption secret (32+ random characters)
114
101
  secret: process.env.AUTH_SECRET!,
115
102
 
103
+ // Auto-refresh tokens before they expire
116
104
  autoRefresh: true,
117
105
 
106
+ // Token expiry (matches your backend JWT settings)
118
107
  expiry: {
119
- accessTokenExpiresIn: "2d",
108
+ accessTokenExpiresIn: "2d", // can also be a number in seconds
120
109
  refreshTokenExpiresIn: "7d",
121
- strategy: "hybrid",
110
+ strategy: "hybrid", // backend first, fallback to config
111
+ },
112
+ };
113
+ ```
114
+
115
+ **Important:** Use any route names you want. The library doesn't enforce `/login` or `/dashboard` — everything is driven by your config.
116
+
117
+ ---
118
+
119
+ ### Step 2: Create your client config
120
+
121
+ This is safe to import anywhere, including client components. It doesn't contain secrets.
122
+
123
+ ```ts
124
+ // lib/auth.client.ts
125
+ import type { ClientAuthConfig } from "next-token-auth";
126
+
127
+ export const clientAuthConfig: ClientAuthConfig = {
128
+ token: {
129
+ cookieName: "myapp.session", // must match server config
122
130
  },
131
+ autoRefresh: true,
123
132
  };
124
133
  ```
125
134
 
126
- ### 2. Wrap your app with `AuthProvider`
135
+ ---
136
+
137
+ ### Step 3: Mount the Route Handlers
138
+
139
+ Create this file to handle login, logout, refresh, and session endpoints automatically.
140
+
141
+ ```ts
142
+ // app/api/auth/[action]/route.ts
143
+ import { createAuthHandlers } from "next-token-auth/server";
144
+ import { authConfig } from "@/lib/auth";
145
+
146
+ export const { GET, POST } = createAuthHandlers(authConfig);
147
+ ```
148
+
149
+ This creates four internal endpoints:
150
+ - `POST /api/auth/login` — authenticates and sets HttpOnly cookie
151
+ - `POST /api/auth/logout` — clears the session cookie
152
+ - `POST /api/auth/refresh` — refreshes the access token
153
+ - `GET /api/auth/session` — returns current user and auth status
154
+
155
+ Your `AuthProvider` calls these automatically. You never call them directly.
156
+
157
+ ---
158
+
159
+ ### Step 4: Wrap your app
127
160
 
128
161
  ```tsx
129
162
  // app/layout.tsx
130
163
  import { AuthProvider } from "next-token-auth/react";
131
- import { authConfig } from "@/lib/auth";
164
+ import { clientAuthConfig } from "@/lib/auth.client";
132
165
 
133
166
  export default function RootLayout({ children }: { children: React.ReactNode }) {
134
167
  return (
135
168
  <html lang="en">
136
169
  <body>
137
- <AuthProvider config={authConfig}>{children}</AuthProvider>
170
+ <AuthProvider config={clientAuthConfig}>
171
+ {children}
172
+ </AuthProvider>
138
173
  </body>
139
174
  </html>
140
175
  );
141
176
  }
142
177
  ```
143
178
 
144
- ### 3. Use the hooks
179
+ ---
180
+
181
+ ### Step 5: Build your login page
145
182
 
146
183
  ```tsx
184
+ // app/login/page.tsx
147
185
  "use client";
148
186
 
149
187
  import { useAuth } from "next-token-auth/react";
188
+ import { useState } from "react";
150
189
 
151
190
  export default function LoginPage() {
152
191
  const { login, isLoading } = useAuth();
192
+ const [email, setEmail] = useState("");
193
+ const [password, setPassword] = useState("");
194
+ const [error, setError] = useState("");
153
195
 
154
- async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
196
+ async function handleSubmit(e: React.FormEvent) {
155
197
  e.preventDefault();
156
- const form = new FormData(e.currentTarget);
157
- await login({ email: form.get("email"), password: form.get("password") });
198
+ setError("");
199
+
200
+ try {
201
+ await login({ email, password });
202
+ window.location.href = "/dashboard";
203
+ } catch (err) {
204
+ setError(err instanceof Error ? err.message : "Login failed");
205
+ }
158
206
  }
159
207
 
160
208
  return (
161
209
  <form onSubmit={handleSubmit}>
162
- <input name="email" type="email" required />
163
- <input name="password" type="password" required />
210
+ <input
211
+ type="email"
212
+ value={email}
213
+ onChange={(e) => setEmail(e.target.value)}
214
+ placeholder="Email"
215
+ required
216
+ />
217
+ <input
218
+ type="password"
219
+ value={password}
220
+ onChange={(e) => setPassword(e.target.value)}
221
+ placeholder="Password"
222
+ required
223
+ />
224
+ {error && <p style={{ color: "red" }}>{error}</p>}
164
225
  <button type="submit" disabled={isLoading}>
165
- {isLoading ? "Signing in" : "Sign in"}
226
+ {isLoading ? "Signing in..." : "Sign in"}
166
227
  </button>
167
228
  </form>
168
229
  );
@@ -171,80 +232,89 @@ export default function LoginPage() {
171
232
 
172
233
  ---
173
234
 
174
- ## Usage
235
+ ### Step 6: Protect a page
175
236
 
176
- ### `AuthProvider`
237
+ ```tsx
238
+ // app/dashboard/page.tsx
239
+ "use client";
177
240
 
178
- Wrap your application once at the root. It initializes the `AuthClient`, restores any existing session from stored tokens on mount, and subscribes all child hooks to session changes.
241
+ import { useRequireAuth, useSession } from "next-token-auth/react";
179
242
 
180
- ```tsx
181
- <AuthProvider config={authConfig}>
182
- {children}
183
- </AuthProvider>
243
+ export default function DashboardPage() {
244
+ // Redirects to /login if not authenticated
245
+ useRequireAuth();
246
+
247
+ const { user, isAuthenticated } = useSession();
248
+
249
+ if (!isAuthenticated) return null; // while redirecting
250
+
251
+ return (
252
+ <main>
253
+ <h1>Dashboard</h1>
254
+ <p>Welcome, {user?.name}</p>
255
+ </main>
256
+ );
257
+ }
184
258
  ```
185
259
 
186
- When `autoRefresh: true` is set, the provider checks every 30 seconds whether the access token is near expiry and refreshes it silently in the background.
260
+ ---
261
+
262
+ ### Step 7: Add middleware (optional but recommended)
187
263
 
188
- #### Full config reference
264
+ Protect routes at the edge for better performance and security.
189
265
 
190
266
  ```ts
191
- interface AuthConfig<User = unknown> {
192
- // Base URL of your backend API
193
- baseUrl: string;
267
+ // middleware.ts (project root, next to app/)
268
+ import { authMiddleware } from "next-token-auth/server";
269
+ import { authConfig } from "@/lib/auth";
194
270
 
195
- endpoints: {
196
- login: string; // required
197
- refresh: string; // required
198
- register?: string;
199
- logout?: string;
200
- me?: string; // fetched after login/session restore to populate user
201
- };
271
+ export const middleware = authMiddleware(authConfig);
202
272
 
203
- routes?: {
204
- public: string[]; // always accessible regardless of auth state
205
- protected: string[]; // require auth, supports wildcard: "/dashboard*"
206
- guestOnly?: string[]; // only accessible when NOT authenticated — any route name works
207
- loginPath?: string; // where to redirect unauthenticated users (default: "/login")
208
- redirectAuthenticatedTo?: string; // where to send authenticated users who hit a guestOnly route (default: "/dashboard")
209
- };
273
+ export const config = {
274
+ // Run middleware on these routes
275
+ matcher: ["/login", "/register", "/dashboard*", "/profile*"],
276
+ };
277
+ ```
210
278
 
211
- token: {
212
- storage: "cookie" | "memory";
213
- cookieName?: string; // default: "next-token-auth.session"
214
- secure?: boolean; // default: true
215
- sameSite?: "strict" | "lax" | "none"; // default: "lax"
216
- };
279
+ **Next.js 16+ users:** Rename the file to `proxy.ts` and export `proxy` instead of `middleware`:
217
280
 
218
- // Used to AES-GCM encrypt session cookies server-side
219
- secret: string;
281
+ ```ts
282
+ // proxy.ts
283
+ export const proxy = authMiddleware(authConfig);
284
+ ```
220
285
 
221
- // Automatically refresh the access token before it expires
222
- autoRefresh?: boolean;
286
+ ---
223
287
 
224
- // Seconds before expiry to trigger a proactive refresh (default: 60)
225
- refreshThreshold?: number;
288
+ ## How It Works
226
289
 
227
- expiry?: {
228
- accessTokenExpiresIn?: number | string; // e.g. "2d", 3600
229
- refreshTokenExpiresIn?: number | string; // e.g. "7d"
230
- strategy?: "backend" | "config" | "hybrid"; // default: "hybrid"
231
- };
290
+ ### The Flow
232
291
 
233
- // Provide a custom fetch implementation (e.g. for testing)
234
- fetchFn?: typeof fetch;
292
+ 1. User submits login form `useAuth().login()` is called
293
+ 2. Client sends credentials to `POST /api/auth/login` (your Route Handler)
294
+ 3. Route Handler calls your backend API, gets tokens back
295
+ 4. Route Handler encrypts tokens with your `secret` and sets an HttpOnly cookie
296
+ 5. Client receives `{ ok: true, user }` (no tokens — they're in the cookie)
297
+ 6. `AuthProvider` updates state → `session.isAuthenticated = true`
298
+ 7. On page reload, `AuthProvider` calls `GET /api/auth/session` to restore the session
299
+ 8. Route Handler decrypts the cookie, validates expiry, fetches user profile, returns `{ user, isAuthenticated }`
235
300
 
236
- // Lifecycle callbacks
237
- onLogin?: (session: AuthSession<User>) => void;
238
- onLogout?: () => void;
239
- onRefreshError?: (error: unknown) => void;
240
- }
241
- ```
301
+ ### Why HttpOnly Cookies?
302
+
303
+ Tokens stored in HttpOnly cookies cannot be read by JavaScript, which protects against XSS attacks. The browser automatically sends the cookie with every request to your domain, so you don't need to manually attach tokens.
304
+
305
+ The downside: you can't call external APIs directly from the client with the access token. Instead, proxy through your own API routes (see "Making Authenticated API Requests" below).
306
+
307
+ ### Why Split the Config?
308
+
309
+ If `secret` is in the client config, it gets bundled into your JavaScript and exposed to the browser. Splitting the config ensures the secret only exists server-side.
242
310
 
243
311
  ---
244
312
 
245
- ### `useAuth`
313
+ ## API Reference
246
314
 
247
- The primary hook. Gives you everything you need to build auth flows.
315
+ ### `useAuth()`
316
+
317
+ The main hook for authentication operations.
248
318
 
249
319
  ```ts
250
320
  const { session, login, logout, refresh, isLoading } = useAuth<User>();
@@ -252,27 +322,37 @@ const { session, login, logout, refresh, isLoading } = useAuth<User>();
252
322
 
253
323
  | Property | Type | Description |
254
324
  |-------------|-----------------------------------------|--------------------------------------------------|
255
- | `session` | `AuthSession<User>` | Current auth session |
256
- | `login` | `(input: LoginInput) => Promise<void>` | POST to your login endpoint, stores tokens |
257
- | `logout` | `() => Promise<void>` | Clears tokens, calls logout endpoint if set |
258
- | `refresh` | `() => Promise<void>` | Manually trigger a token refresh |
259
- | `isLoading` | `boolean` | `true` while initializing or during login |
325
+ | `session` | `AuthSession<User>` | Current user and auth status |
326
+ | `login` | `(input: LoginInput) => Promise<void>` | Authenticate user, sets HttpOnly cookie |
327
+ | `logout` | `() => Promise<void>` | Clears session, calls backend logout if configured |
328
+ | `refresh` | `() => Promise<void>` | Manually refresh the access token |
329
+ | `isLoading` | `boolean` | `true` during initialization or login |
330
+
331
+ `LoginInput` is flexible — pass any fields your backend expects:
260
332
 
261
- `LoginInput` is an open object (`{ [key: string]: unknown }`), so you can pass any fields your backend expects.
333
+ ```ts
334
+ await login({ email, password });
335
+ await login({ username, password, rememberMe: true });
336
+ ```
262
337
 
263
338
  ---
264
339
 
265
- ### `useSession`
340
+ ### `useSession()`
266
341
 
267
- Read-only access to the current session. Use this in components that only need to display user data.
342
+ Read-only access to the current session. Use this in components that only display user data.
268
343
 
269
344
  ```ts
270
- const { user, tokens, isAuthenticated } = useSession<User>();
345
+ const { user, isAuthenticated } = useSession<User>();
271
346
  ```
272
347
 
348
+ Returns:
349
+ - `user` — your user object (or `null` if not authenticated)
350
+ - `tokens` — always `null` on the client (tokens are HttpOnly)
351
+ - `isAuthenticated` — `true` if the user is logged in
352
+
273
353
  ---
274
354
 
275
- ### `useRequireAuth`
355
+ ### `useRequireAuth(options?)`
276
356
 
277
357
  Redirects unauthenticated users. Call it at the top of any protected client component.
278
358
 
@@ -280,223 +360,272 @@ Redirects unauthenticated users. Call it at the top of any protected client comp
280
360
  useRequireAuth({ redirectTo: "/login" });
281
361
  ```
282
362
 
283
- You can also pass a custom handler instead of a redirect path:
363
+ Options:
364
+
365
+ | Option | Type | Default | Description |
366
+ |---------------------|--------------|------------|--------------------------------------------------|
367
+ | `redirectTo` | `string` | `"/login"` | Where to send unauthenticated users |
368
+ | `onUnauthenticated` | `() => void` | — | Custom handler instead of redirect |
369
+
370
+ The hook waits for `isLoading` to finish before redirecting, so you won't see a flash.
371
+
372
+ Custom redirect example:
284
373
 
285
374
  ```ts
375
+ import { useRouter } from "next/navigation";
376
+
286
377
  useRequireAuth({
287
378
  onUnauthenticated: () => router.push("/login?from=/dashboard"),
288
379
  });
289
380
  ```
290
381
 
291
- The hook waits for `isLoading` to be `false` before acting, so it won't flash a redirect during the initial session restore.
292
-
293
- | Option | Type | Default |
294
- |---------------------|--------------|------------|
295
- | `redirectTo` | `string` | `"/login"` |
296
- | `onUnauthenticated` | `() => void` | — |
297
-
298
382
  ---
299
383
 
300
384
  ### Making Authenticated API Requests
301
385
 
302
- When you need to call your own backend endpoints that require an access token, use `client.fetch` from the `useAuth` hook. It automatically injects `Authorization: Bearer <token>` and handles 401 → refresh → retry for you.
386
+ Since tokens are in HttpOnly cookies (inaccessible to JavaScript), you can't add `Authorization` headers from the client. Instead, proxy through your own API routes.
387
+
388
+ **Client-side:**
303
389
 
304
390
  ```ts
305
391
  "use client";
306
392
 
307
- import { useAuth } from "next-token-auth/react";
308
-
309
- export default function Orders() {
310
- const { client } = useAuth();
311
-
312
- async function fetchOrders() {
313
- const res = await client.fetch("https://api.example.com/orders");
393
+ export default function OrdersPage() {
394
+ async function loadOrders() {
395
+ // The session cookie is automatically sent with this request
396
+ const res = await fetch("/api/orders");
314
397
  const data = await res.json();
315
398
  console.log(data);
316
399
  }
317
400
 
318
- return <button onClick={fetchOrders}>Load Orders</button>;
401
+ return <button onClick={loadOrders}>Load Orders</button>;
319
402
  }
320
403
  ```
321
404
 
322
- You can also read the token directly from the session if you need to pass it manually:
405
+ **Server-side (your API route):**
323
406
 
324
407
  ```ts
325
- const { session } = useAuth();
408
+ // app/api/orders/route.ts
409
+ import { getServerSession } from "next-token-auth/server";
410
+ import { authConfig } from "@/lib/auth";
411
+ import { cookies } from "next/headers";
326
412
 
327
- const res = await fetch("https://api.example.com/orders", {
328
- headers: {
329
- Authorization: `Bearer ${session.tokens?.accessToken}`,
330
- },
331
- });
413
+ export async function GET() {
414
+ const cookieStore = await cookies();
415
+ const session = await getServerSession(
416
+ { cookies: { get: (name) => cookieStore.get(name) } },
417
+ authConfig
418
+ );
419
+
420
+ if (!session.isAuthenticated) {
421
+ return Response.json({ error: "Unauthorized" }, { status: 401 });
422
+ }
423
+
424
+ // Now call your backend with the access token
425
+ const res = await fetch(`${authConfig.baseUrl}/orders`, {
426
+ headers: {
427
+ Authorization: `Bearer ${session.tokens!.accessToken}`,
428
+ },
429
+ });
430
+
431
+ return Response.json(await res.json());
432
+ }
332
433
  ```
333
434
 
334
- `client.fetch` is the recommended approach it keeps your requests resilient to token expiry without any extra work on your end.
435
+ This keeps tokens securethey never leave the server.
335
436
 
336
437
  ---
337
438
 
338
- ### Protecting API Routes with `withAuth`
439
+ ### `getServerSession(req, config)`
339
440
 
340
- Wrap App Router route handlers to require authentication:
441
+ Reads and validates the session in server components and API routes.
341
442
 
342
443
  ```ts
343
- // app/api/profile/route.ts
344
- import { withAuth } from "next-token-auth/server";
444
+ // app/dashboard/page.tsx (server component)
445
+ import { redirect } from "next/navigation";
446
+ import { cookies } from "next/headers";
447
+ import { getServerSession } from "next-token-auth/server";
345
448
  import { authConfig } from "@/lib/auth";
346
449
 
347
- export const GET = withAuth(authConfig, async (req, session) => {
348
- return Response.json({ user: session.user });
349
- });
450
+ export default async function DashboardPage() {
451
+ const cookieStore = await cookies();
452
+ const session = await getServerSession(
453
+ { cookies: { get: (name) => cookieStore.get(name) } },
454
+ authConfig
455
+ );
456
+
457
+ if (!session.isAuthenticated) {
458
+ redirect("/login");
459
+ }
460
+
461
+ return <h1>Welcome, {session.user.name}</h1>;
462
+ }
350
463
  ```
351
464
 
352
- Unauthenticated requests are redirected to `/login` by default. Pass `{ redirectTo: "/your-path" }` as the third argument to override.
465
+ What it does:
466
+ 1. Reads the encrypted session cookie
467
+ 2. Decrypts it using your `secret`
468
+ 3. Validates token expiry
469
+ 4. Optionally fetches the user profile from your backend
470
+ 5. Returns `{ user, tokens, isAuthenticated }`
353
471
 
354
472
  ---
355
473
 
356
- ### Middleware (Edge Route Protection)
474
+ ### `withAuth(config, handler, options?)`
357
475
 
358
- Protect entire route groups at the edge using Next.js middleware. The middleware supports three route categories:
476
+ Wraps App Router route handlers to require authentication.
359
477
 
360
- - `public` — always accessible, no auth check
361
- - `protected` — requires authentication, redirects to `loginPath` if not
362
- - `guestOnly` accessible only when NOT authenticated; authenticated users are redirected to `redirectAuthenticatedTo`
478
+ ```ts
479
+ // app/api/profile/route.ts
480
+ import { withAuth } from "next-token-auth/server";
481
+ import { authConfig } from "@/lib/auth";
482
+
483
+ export const GET = withAuth(authConfig, async (req, session) => {
484
+ // session.user is guaranteed to exist here
485
+ return Response.json({ user: session.user });
486
+ });
487
+ ```
363
488
 
364
- You can use any route naming convention you want — the library doesn't enforce `/login`, `/dashboard`, or any specific path. Everything is driven by your config.
489
+ Unauthenticated requests are redirected to `/login` by default. Override with:
365
490
 
366
491
  ```ts
367
- // lib/auth.ts
368
- export const authConfig: AuthConfig = {
369
- // ...
370
- routes: {
371
- public: ["/", "/about"],
372
- guestOnly: ["/sign-in", "/sign-up"], // any names you want
373
- protected: ["/app*", "/account*"],
374
- loginPath: "/sign-in", // where unauthenticated users are sent
375
- redirectAuthenticatedTo: "/app/home", // where authenticated users are sent from guestOnly routes
376
- },
377
- };
492
+ export const GET = withAuth(
493
+ authConfig,
494
+ async (req, session) => { /* ... */ },
495
+ { redirectTo: "/sign-in" }
496
+ );
378
497
  ```
379
498
 
499
+ ---
500
+
501
+ ### `authMiddleware(config)`
502
+
503
+ Creates a Next.js middleware function for edge-level route protection.
504
+
380
505
  ```ts
381
- // middleware.ts (project root)
506
+ // middleware.ts (project root, next to app/)
382
507
  import { authMiddleware } from "next-token-auth/server";
383
508
  import { authConfig } from "@/lib/auth";
384
509
 
385
510
  export const middleware = authMiddleware(authConfig);
386
511
 
387
512
  export const config = {
388
- matcher: ["/sign-in", "/sign-up", "/app*", "/account*"],
513
+ matcher: ["/login", "/register", "/dashboard*", "/profile*"],
389
514
  };
390
515
  ```
391
516
 
392
- Some other valid setups:
517
+ **Next.js 16+ users:** Rename the file to `proxy.ts` and change the export:
393
518
 
394
519
  ```ts
395
- // Using /auth/* convention
396
- routes: {
397
- guestOnly: ["/auth/login", "/auth/register"],
398
- protected: ["/dashboard*"],
399
- loginPath: "/auth/login",
400
- redirectAuthenticatedTo: "/dashboard",
401
- }
520
+ // proxy.ts
521
+ export const proxy = authMiddleware(authConfig);
522
+ ```
523
+
524
+ ---
525
+
526
+ ## Route Protection Explained
527
+
528
+ The middleware supports three route categories:
402
529
 
403
- // Using a portal pattern
530
+ ### 1. Public routes
531
+
532
+ Always accessible, no auth check. Example: homepage, about page.
533
+
534
+ ```ts
404
535
  routes: {
405
- guestOnly: ["/portal"],
406
- protected: ["/admin*", "/workspace*"],
407
- loginPath: "/portal",
408
- redirectAuthenticatedTo: "/admin",
536
+ public: ["/", "/about", "/pricing"],
409
537
  }
410
538
  ```
411
539
 
412
- Route resolution order inside the middleware:
540
+ ### 2. Protected routes
413
541
 
414
- 1. `guestOnly` if authenticated, redirect to `redirectAuthenticatedTo`
415
- 2. `public` — always allow through
416
- 3. `protected` — require valid session, redirect to `loginPath` if missing
542
+ Require authentication. Unauthenticated users are redirected to `loginPath`.
417
543
 
418
- Two things to keep in mind:
544
+ ```ts
545
+ routes: {
546
+ protected: ["/dashboard*", "/settings*"],
547
+ loginPath: "/login", // where to send unauthenticated users
548
+ }
549
+ ```
419
550
 
420
- - Wildcard patterns use `*` at the end: `"/dashboard*"` matches `/dashboard`, `/dashboard/`, and `/dashboard/settings`
421
- - The `matcher` in `export const config` controls which routes Next.js runs the middleware on at all — make sure it covers both your protected and guest-only routes
422
- - `loginPath` defaults to `"/login"` if not set
423
- - `redirectAuthenticatedTo` defaults to `"/dashboard"` if not set
551
+ Wildcard matching:
552
+ - `"/dashboard*"` matches `/dashboard`, `/dashboard/`, `/dashboard/settings`
553
+ - `"/api/admin*"` matches `/api/admin`, `/api/admin/users`
424
554
 
425
- ---
555
+ ### 3. Guest-only routes
426
556
 
427
- ## Session and Token Handling
557
+ Only accessible when NOT authenticated. Authenticated users are redirected away.
428
558
 
429
- ### How tokens are stored
559
+ Use this for login and register pages so logged-in users can't access them.
430
560
 
431
- | Storage mode | Where |
432
- |--------------|-----------------------------------------------------------------------|
433
- | `"cookie"` | Serialized as JSON in a browser cookie with `Secure` + `SameSite` |
434
- | `"memory"` | Held in a JavaScript variable — cleared on page refresh |
561
+ ```ts
562
+ routes: {
563
+ guestOnly: ["/login", "/register"],
564
+ redirectAuthenticatedTo: "/dashboard", // where to send authenticated users
565
+ }
566
+ ```
435
567
 
436
- Server-side (in `getServerSession` and `authMiddleware`), the cookie value is expected to be AES-GCM encrypted using your `secret`. The `TokenManager` provides `encryptTokens` / `decryptTokens` helpers for this.
568
+ ### Resolution order
437
569
 
438
- ### Session restore on page load
570
+ When a request hits the middleware:
439
571
 
440
- When `AuthProvider` mounts, it calls `client.initialize()`, which:
572
+ 1. Check if it's a `guestOnly` route → if authenticated, redirect to `redirectAuthenticatedTo`
573
+ 2. Check if it's a `public` route → always allow through
574
+ 3. Check if it's a `protected` route → if not authenticated, redirect to `loginPath`
441
575
 
442
- 1. Reads tokens from the configured storage
443
- 2. Checks whether the access token is expired (accounting for `refreshThreshold`)
444
- 3. If the access token is near expiry but the refresh token is still valid, it silently refreshes
445
- 4. If a `me` endpoint is configured, it fetches the user profile to populate `session.user`
446
- 5. Updates React state — `isLoading` flips to `false` once complete
576
+ ### Matcher vs routes config
447
577
 
448
- ### Automatic refresh
578
+ The `matcher` in your middleware file controls which routes Next.js runs the middleware on at all:
449
579
 
450
- When `autoRefresh: true`, the provider runs a check every 30 seconds. If the access token is within `refreshThreshold` seconds of expiry (default: 60s) and the refresh token is still valid, it calls the refresh endpoint automatically.
580
+ ```ts
581
+ export const config = {
582
+ matcher: ["/login", "/dashboard*"],
583
+ };
584
+ ```
451
585
 
452
- The HTTP client also handles 401 responses: it attempts a token refresh and retries the original request once. Multiple concurrent 401s share a single refresh request (deduplicated via a shared promise).
586
+ If a route isn't in the `matcher`, the middleware never runs for it so your `routes.protected` list won't help. Make sure the `matcher` covers all routes you want to protect or mark as guest-only.
453
587
 
454
- ### Refresh flow
588
+ ---
455
589
 
456
- ```
457
- Request → 401 → refresh endpoint → new tokens stored → original request retried
458
- ```
590
+ ## Session and Token Handling
459
591
 
460
- If the refresh token is expired, the user is logged out and the session is cleared.
592
+ ### How tokens are stored
461
593
 
462
- ---
594
+ Tokens are stored in HttpOnly cookies, encrypted with AES-GCM. The cookie is set by the `/api/auth/login` Route Handler and read by the middleware and `getServerSession`.
463
595
 
464
- ## Server-Side Session (`getServerSession`)
596
+ JavaScript in the browser cannot read the cookie — only the server can decrypt it.
465
597
 
466
- Use this in App Router server components and API routes to read the session without going through the client:
598
+ ### Session restore on page load
467
599
 
468
- ```ts
469
- // app/dashboard/page.tsx
470
- import { redirect } from "next/navigation";
471
- import { cookies } from "next/headers";
472
- import { getServerSession } from "next-token-auth/server";
473
- import { authConfig } from "@/lib/auth";
600
+ When `AuthProvider` mounts, it calls `GET /api/auth/session`, which:
474
601
 
475
- export default async function DashboardPage() {
476
- const cookieStore = await cookies();
602
+ 1. Reads the encrypted session cookie server-side
603
+ 2. Decrypts it using your `secret`
604
+ 3. Checks if the refresh token is expired
605
+ 4. Fetches the user profile from your backend (if `me` endpoint is configured)
606
+ 5. Returns `{ user, isAuthenticated }` to the client
477
607
 
478
- const session = await getServerSession(
479
- { cookies: { get: (name) => cookieStore.get(name) } },
480
- authConfig
481
- );
608
+ The client never sees the raw tokens — only the user object and auth status.
482
609
 
483
- if (!session.isAuthenticated) {
484
- redirect("/login");
485
- }
610
+ ### Automatic token refresh
486
611
 
487
- return <h1>Welcome, {session.user.name}</h1>;
488
- }
489
- ```
612
+ When `autoRefresh: true`, the provider periodically calls `POST /api/auth/refresh` (based on `refreshThreshold`, default 60 seconds before expiry).
613
+
614
+ The Route Handler:
615
+ 1. Reads the encrypted cookie
616
+ 2. Checks if the refresh token is still valid
617
+ 3. Calls your backend's refresh endpoint with the refresh token
618
+ 4. Encrypts the new tokens and updates the HttpOnly cookie
490
619
 
491
- `getServerSession` decrypts the session cookie, validates expiry, and attempts a server-side token refresh if the access token is near expiry but the refresh token is still valid.
620
+ If the refresh token is expired, the session is cleared.
492
621
 
493
622
  ---
494
623
 
495
- ## Backend Requirements
624
+ ## Backend API Requirements
496
625
 
497
- Your API needs to implement the following contract:
626
+ Your backend needs to implement these endpoints:
498
627
 
499
- ### `POST /auth/login`
628
+ ### `POST /auth/login` (or whatever you set in `endpoints.login`)
500
629
 
501
630
  Request body: whatever fields you pass to `login()` (e.g. `{ email, password }`)
502
631
 
@@ -504,9 +633,13 @@ Response:
504
633
 
505
634
  ```json
506
635
  {
507
- "accessToken": "eyJ...",
508
- "refreshToken": "eyJ...",
509
- "user": { "id": "1", "email": "user@example.com", "name": "Jane" },
636
+ "accessToken": "eyJhbGc...",
637
+ "refreshToken": "eyJhbGc...",
638
+ "user": {
639
+ "id": "123",
640
+ "email": "user@example.com",
641
+ "name": "Jane Doe"
642
+ },
510
643
 
511
644
  // Optional — used by "backend" and "hybrid" expiry strategies
512
645
  "accessTokenExpiresIn": "2d",
@@ -522,39 +655,53 @@ Response:
522
655
  Request body:
523
656
 
524
657
  ```json
525
- { "refreshToken": "eyJ..." }
658
+ {
659
+ "refreshToken": "eyJhbGc..."
660
+ }
526
661
  ```
527
662
 
528
663
  Response: same shape as the login response (new `accessToken` + `refreshToken`).
529
664
 
530
- ### `GET /auth/me` _(optional)_
665
+ ### `GET /auth/me` (optional)
531
666
 
532
667
  Returns the current user object. Called after login and on session restore if the `me` endpoint is configured.
533
668
 
534
- ### `POST /auth/logout` _(optional)_
669
+ Response:
670
+
671
+ ```json
672
+ {
673
+ "id": "123",
674
+ "email": "user@example.com",
675
+ "name": "Jane Doe"
676
+ }
677
+ ```
678
+
679
+ ### `POST /auth/logout` (optional)
535
680
 
536
- Called on logout. Failure is silently ignored — tokens are always cleared locally regardless.
681
+ Called on logout. Failure is silently ignored — the session cookie is always cleared locally regardless.
537
682
 
538
683
  ---
539
684
 
540
685
  ## Expiry Formats
541
686
 
542
- The `parseExpiry` utility accepts:
687
+ The library accepts expiry values in multiple formats:
543
688
 
544
- | Input | Seconds |
545
- |---------|-----------|
546
- | `900` | 900 |
547
- | `"15m"` | 900 |
548
- | `"2h"` | 7 200 |
549
- | `"2d"` | 172 800 |
550
- | `"7d"` | 604 800 |
551
- | `"1w"` | 604 800 |
689
+ | Input | Seconds | Human-readable |
690
+ |---------|-----------|----------------|
691
+ | `900` | 900 | 15 minutes |
692
+ | `"15m"` | 900 | 15 minutes |
693
+ | `"2h"` | 7,200 | 2 hours |
694
+ | `"2d"` | 172,800 | 2 days |
695
+ | `"7d"` | 604,800 | 7 days |
696
+ | `"1w"` | 604,800 | 1 week |
697
+
698
+ Supported units: `s` (seconds), `m` (minutes), `h` (hours), `d` (days), `w` (weeks)
552
699
 
553
700
  ### Expiry strategies
554
701
 
555
- | Strategy | Behaviour |
702
+ | Strategy | Behavior |
556
703
  |-----------|------------------------------------------------------------------|
557
- | `backend` | Use only the expiry values returned by the API |
704
+ | `backend` | Use only the expiry values returned by your API |
558
705
  | `config` | Use only the values set in `expiry` config |
559
706
  | `hybrid` | API response first; fall back to config if not present (default) |
560
707
 
@@ -562,12 +709,85 @@ The `parseExpiry` utility accepts:
562
709
 
563
710
  ---
564
711
 
712
+ ## Configuration Reference
713
+
714
+ ### `ClientAuthConfig` (for `AuthProvider`)
715
+
716
+ ```ts
717
+ interface ClientAuthConfig {
718
+ token?: {
719
+ cookieName?: string; // default: "next-token-auth.session"
720
+ };
721
+
722
+ routes?: {
723
+ loginPath?: string; // default: "/login"
724
+ redirectAuthenticatedTo?: string; // default: "/dashboard"
725
+ };
726
+
727
+ autoRefresh?: boolean; // default: false
728
+ refreshThreshold?: number; // seconds before expiry to refresh (default: 60)
729
+
730
+ onLogin?: (session: AuthSession) => void;
731
+ onLogout?: () => void;
732
+ }
733
+ ```
734
+
735
+ ### `AuthConfig` (for server-side functions)
736
+
737
+ ```ts
738
+ interface AuthConfig<User = unknown> {
739
+ baseUrl: string; // Your backend API base URL
740
+
741
+ endpoints: {
742
+ login: string; // required
743
+ refresh: string; // required
744
+ register?: string;
745
+ logout?: string;
746
+ me?: string;
747
+ };
748
+
749
+ routes?: {
750
+ public: string[]; // always accessible
751
+ protected: string[]; // require auth
752
+ guestOnly?: string[]; // only when NOT authenticated
753
+ loginPath?: string; // default: "/login"
754
+ redirectAuthenticatedTo?: string; // default: "/dashboard"
755
+ };
756
+
757
+ token: {
758
+ storage: "cookie" | "memory";
759
+ cookieName?: string;
760
+ secure?: boolean; // default: true
761
+ sameSite?: "strict" | "lax" | "none"; // default: "lax"
762
+ };
763
+
764
+ secret: string; // AES-GCM encryption key
765
+
766
+ autoRefresh?: boolean;
767
+ refreshThreshold?: number;
768
+
769
+ expiry?: {
770
+ accessTokenExpiresIn?: number | string;
771
+ refreshTokenExpiresIn?: number | string;
772
+ strategy?: "backend" | "config" | "hybrid";
773
+ };
774
+
775
+ fetchFn?: typeof fetch; // custom fetch for testing
776
+
777
+ onLogin?: (session: AuthSession<User>) => void;
778
+ onLogout?: () => void;
779
+ onRefreshError?: (error: unknown) => void;
780
+ }
781
+ ```
782
+
783
+ ---
784
+
565
785
  ## TypeScript Types
566
786
 
567
787
  ```ts
568
788
  interface AuthSession<User = unknown> {
569
789
  user: User | null;
570
- tokens: AuthTokens | null;
790
+ tokens: AuthTokens | null; // always null on client-side (HttpOnly)
571
791
  isAuthenticated: boolean;
572
792
  }
573
793
 
@@ -575,7 +795,7 @@ interface AuthTokens {
575
795
  accessToken: string;
576
796
  refreshToken: string;
577
797
  accessTokenExpiresAt: number; // Unix timestamp in ms
578
- refreshTokenExpiresAt?: number; // Unix timestamp in ms
798
+ refreshTokenExpiresAt?: number;
579
799
  }
580
800
 
581
801
  interface LoginResponse<User = unknown> {
@@ -586,12 +806,102 @@ interface LoginResponse<User = unknown> {
586
806
  accessTokenExpiresIn?: number | string;
587
807
  refreshTokenExpiresIn?: number | string;
588
808
  }
809
+ ```
810
+
811
+ All types are exported from `next-token-auth`.
812
+
813
+ ---
589
814
 
590
- type ExpiryInput = number | string;
591
- type ExpiryStrategy = "backend" | "config" | "hybrid";
815
+ ## Common Patterns
816
+
817
+ ### Custom user type
818
+
819
+ ```ts
820
+ interface MyUser {
821
+ id: string;
822
+ email: string;
823
+ role: "admin" | "user";
824
+ }
825
+
826
+ const { session } = useAuth<MyUser>();
827
+ console.log(session.user?.role);
828
+ ```
829
+
830
+ ### Logout with redirect
831
+
832
+ ```ts
833
+ const { logout } = useAuth();
834
+
835
+ async function handleLogout() {
836
+ await logout();
837
+ window.location.href = "/";
838
+ }
839
+ ```
840
+
841
+ ### Conditional rendering based on auth
842
+
843
+ ```ts
844
+ const { isAuthenticated } = useSession();
845
+
846
+ return (
847
+ <nav>
848
+ {isAuthenticated ? (
849
+ <a href="/dashboard">Dashboard</a>
850
+ ) : (
851
+ <a href="/login">Sign in</a>
852
+ )}
853
+ </nav>
854
+ );
592
855
  ```
593
856
 
594
- All types are exported from the root `next-token-auth` import.
857
+ ### Server-side redirect in a server component
858
+
859
+ ```ts
860
+ import { redirect } from "next/navigation";
861
+ import { getServerSession } from "next-token-auth/server";
862
+
863
+ export default async function AdminPage() {
864
+ const session = await getServerSession(req, authConfig);
865
+
866
+ if (!session.isAuthenticated) {
867
+ redirect("/login");
868
+ }
869
+
870
+ if (session.user.role !== "admin") {
871
+ redirect("/dashboard");
872
+ }
873
+
874
+ return <h1>Admin Panel</h1>;
875
+ }
876
+ ```
877
+
878
+ ---
879
+
880
+ ## Troubleshooting
881
+
882
+ ### "Cannot find module 'next-token-auth/server'"
883
+
884
+ Run `npm run build` (or `pnpm build`) to generate the `dist/` folder. The package uses subpath exports (`/server`, `/react`) which require a build step.
885
+
886
+ ### Middleware always redirects to login
887
+
888
+ Check three things:
889
+
890
+ 1. The `matcher` in your `middleware.ts` includes the route you're testing
891
+ 2. The route is listed in `routes.protected` or not listed in `routes.public`
892
+ 3. You're actually logged in — check the Application tab in DevTools for the session cookie
893
+
894
+ ### "secret is undefined"
895
+
896
+ Make sure `AUTH_SECRET` is set in your `.env.local` file and you're importing `authConfig` (not `clientAuthConfig`) in your Route Handler and middleware.
897
+
898
+ ### Session is lost on page reload
899
+
900
+ The session should persist via the HttpOnly cookie. If it's not:
901
+
902
+ 1. Check that `cookieName` matches in both `authConfig` and `clientAuthConfig`
903
+ 2. Verify the cookie exists in DevTools → Application → Cookies
904
+ 3. Make sure `app/api/auth/[action]/route.ts` exists and exports `createAuthHandlers(authConfig)`
595
905
 
596
906
  ---
597
907
 
@@ -604,13 +914,14 @@ All types are exported from the root `next-token-auth` import.
604
914
 
605
915
  ---
606
916
 
607
- ## Security Notes
917
+ ## Security
608
918
 
609
- - Session cookies use `Secure` and `SameSite` flags by default
610
- - Server-side cookies are AES-GCM encrypted using your `secret`
919
+ - Tokens are stored in HttpOnly cookies JavaScript cannot read them
920
+ - Cookies are AES-GCM encrypted server-side using your `secret`
921
+ - The `secret` never leaves the server — it's only used in Route Handlers, middleware, and `getServerSession`
922
+ - `AuthProvider` receives `ClientAuthConfig` which does not contain `secret` or `baseUrl`
923
+ - Cookies use `Secure` and `SameSite` flags by default for CSRF protection
611
924
  - Use a random 32-character string for `secret` in production — never commit it
612
- - The `"memory"` storage mode keeps tokens out of cookies entirely, at the cost of losing the session on page refresh
613
- - Refresh tokens are never exposed to JavaScript when using server-side encrypted cookies
614
925
 
615
926
  ---
616
927