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 +596 -285
- package/dist/{index-CejL5heu.d.mts → index-Csz5lfEv.d.mts} +30 -2
- package/dist/{index-CejL5heu.d.ts → index-Csz5lfEv.d.ts} +30 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +58 -31
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +59 -32
- package/dist/index.mjs.map +1 -1
- package/dist/react/index.d.mts +4 -4
- package/dist/react/index.d.ts +4 -4
- package/dist/react/index.js +53 -538
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +54 -539
- package/dist/react/index.mjs.map +1 -1
- package/dist/server/index.d.mts +45 -7
- package/dist/server/index.d.ts +45 -7
- package/dist/server/index.js +178 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +178 -1
- package/dist/server/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,53 +1,41 @@
|
|
|
1
1
|
# next-token-auth
|
|
2
2
|
|
|
3
|
-
A production-grade authentication library for Next.js
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
11
|
+
## Why This Exists
|
|
20
12
|
|
|
21
|
-
|
|
13
|
+
Authentication in Next.js is tedious. You need to:
|
|
22
14
|
|
|
23
|
-
|
|
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
|
-
|
|
22
|
+
Most projects spend days on auth boilerplate before shipping a single feature.
|
|
26
23
|
|
|
27
|
-
-
|
|
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
|
-
##
|
|
36
|
-
|
|
37
|
-
-
|
|
38
|
-
- `useAuth`
|
|
39
|
-
-
|
|
40
|
-
-
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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={
|
|
170
|
+
<AuthProvider config={clientAuthConfig}>
|
|
171
|
+
{children}
|
|
172
|
+
</AuthProvider>
|
|
138
173
|
</body>
|
|
139
174
|
</html>
|
|
140
175
|
);
|
|
141
176
|
}
|
|
142
177
|
```
|
|
143
178
|
|
|
144
|
-
|
|
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
|
|
196
|
+
async function handleSubmit(e: React.FormEvent) {
|
|
155
197
|
e.preventDefault();
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
163
|
-
|
|
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
|
|
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
|
-
|
|
235
|
+
### Step 6: Protect a page
|
|
175
236
|
|
|
176
|
-
|
|
237
|
+
```tsx
|
|
238
|
+
// app/dashboard/page.tsx
|
|
239
|
+
"use client";
|
|
177
240
|
|
|
178
|
-
|
|
241
|
+
import { useRequireAuth, useSession } from "next-token-auth/react";
|
|
179
242
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
### Step 7: Add middleware (optional but recommended)
|
|
187
263
|
|
|
188
|
-
|
|
264
|
+
Protect routes at the edge for better performance and security.
|
|
189
265
|
|
|
190
266
|
```ts
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
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
|
-
|
|
219
|
-
|
|
281
|
+
```ts
|
|
282
|
+
// proxy.ts
|
|
283
|
+
export const proxy = authMiddleware(authConfig);
|
|
284
|
+
```
|
|
220
285
|
|
|
221
|
-
|
|
222
|
-
autoRefresh?: boolean;
|
|
286
|
+
---
|
|
223
287
|
|
|
224
|
-
|
|
225
|
-
refreshThreshold?: number;
|
|
288
|
+
## How It Works
|
|
226
289
|
|
|
227
|
-
|
|
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
|
-
|
|
234
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
313
|
+
## API Reference
|
|
246
314
|
|
|
247
|
-
|
|
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
|
|
256
|
-
| `login` | `(input: LoginInput) => Promise<void>` |
|
|
257
|
-
| `logout` | `() => Promise<void>` | Clears
|
|
258
|
-
| `refresh` | `() => Promise<void>` | Manually
|
|
259
|
-
| `isLoading` | `boolean` | `true`
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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={
|
|
401
|
+
return <button onClick={loadOrders}>Load Orders</button>;
|
|
319
402
|
}
|
|
320
403
|
```
|
|
321
404
|
|
|
322
|
-
|
|
405
|
+
**Server-side (your API route):**
|
|
323
406
|
|
|
324
407
|
```ts
|
|
325
|
-
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
435
|
+
This keeps tokens secure — they never leave the server.
|
|
335
436
|
|
|
336
437
|
---
|
|
337
438
|
|
|
338
|
-
###
|
|
439
|
+
### `getServerSession(req, config)`
|
|
339
440
|
|
|
340
|
-
|
|
441
|
+
Reads and validates the session in server components and API routes.
|
|
341
442
|
|
|
342
443
|
```ts
|
|
343
|
-
// app/
|
|
344
|
-
import {
|
|
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
|
|
348
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
474
|
+
### `withAuth(config, handler, options?)`
|
|
357
475
|
|
|
358
|
-
|
|
476
|
+
Wraps App Router route handlers to require authentication.
|
|
359
477
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
|
|
489
|
+
Unauthenticated requests are redirected to `/login` by default. Override with:
|
|
365
490
|
|
|
366
491
|
```ts
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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: ["/
|
|
513
|
+
matcher: ["/login", "/register", "/dashboard*", "/profile*"],
|
|
389
514
|
};
|
|
390
515
|
```
|
|
391
516
|
|
|
392
|
-
|
|
517
|
+
**Next.js 16+ users:** Rename the file to `proxy.ts` and change the export:
|
|
393
518
|
|
|
394
519
|
```ts
|
|
395
|
-
//
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
530
|
+
### 1. Public routes
|
|
531
|
+
|
|
532
|
+
Always accessible, no auth check. Example: homepage, about page.
|
|
533
|
+
|
|
534
|
+
```ts
|
|
404
535
|
routes: {
|
|
405
|
-
|
|
406
|
-
protected: ["/admin*", "/workspace*"],
|
|
407
|
-
loginPath: "/portal",
|
|
408
|
-
redirectAuthenticatedTo: "/admin",
|
|
536
|
+
public: ["/", "/about", "/pricing"],
|
|
409
537
|
}
|
|
410
538
|
```
|
|
411
539
|
|
|
412
|
-
|
|
540
|
+
### 2. Protected routes
|
|
413
541
|
|
|
414
|
-
|
|
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
|
-
|
|
544
|
+
```ts
|
|
545
|
+
routes: {
|
|
546
|
+
protected: ["/dashboard*", "/settings*"],
|
|
547
|
+
loginPath: "/login", // where to send unauthenticated users
|
|
548
|
+
}
|
|
549
|
+
```
|
|
419
550
|
|
|
420
|
-
|
|
421
|
-
-
|
|
422
|
-
- `
|
|
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
|
-
|
|
557
|
+
Only accessible when NOT authenticated. Authenticated users are redirected away.
|
|
428
558
|
|
|
429
|
-
|
|
559
|
+
Use this for login and register pages so logged-in users can't access them.
|
|
430
560
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
561
|
+
```ts
|
|
562
|
+
routes: {
|
|
563
|
+
guestOnly: ["/login", "/register"],
|
|
564
|
+
redirectAuthenticatedTo: "/dashboard", // where to send authenticated users
|
|
565
|
+
}
|
|
566
|
+
```
|
|
435
567
|
|
|
436
|
-
|
|
568
|
+
### Resolution order
|
|
437
569
|
|
|
438
|
-
|
|
570
|
+
When a request hits the middleware:
|
|
439
571
|
|
|
440
|
-
|
|
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
|
-
|
|
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
|
-
|
|
578
|
+
The `matcher` in your middleware file controls which routes Next.js runs the middleware on at all:
|
|
449
579
|
|
|
450
|
-
|
|
580
|
+
```ts
|
|
581
|
+
export const config = {
|
|
582
|
+
matcher: ["/login", "/dashboard*"],
|
|
583
|
+
};
|
|
584
|
+
```
|
|
451
585
|
|
|
452
|
-
|
|
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
|
-
|
|
588
|
+
---
|
|
455
589
|
|
|
456
|
-
|
|
457
|
-
Request → 401 → refresh endpoint → new tokens stored → original request retried
|
|
458
|
-
```
|
|
590
|
+
## Session and Token Handling
|
|
459
591
|
|
|
460
|
-
|
|
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
|
-
|
|
596
|
+
JavaScript in the browser cannot read the cookie — only the server can decrypt it.
|
|
465
597
|
|
|
466
|
-
|
|
598
|
+
### Session restore on page load
|
|
467
599
|
|
|
468
|
-
|
|
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
|
-
|
|
476
|
-
|
|
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
|
-
|
|
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
|
-
|
|
484
|
-
redirect("/login");
|
|
485
|
-
}
|
|
610
|
+
### Automatic token refresh
|
|
486
611
|
|
|
487
|
-
|
|
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
|
-
|
|
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
|
|
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": "
|
|
508
|
-
"refreshToken": "
|
|
509
|
-
"user": {
|
|
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
|
-
{
|
|
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`
|
|
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
|
-
|
|
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 —
|
|
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
|
|
687
|
+
The library accepts expiry values in multiple formats:
|
|
543
688
|
|
|
544
|
-
| Input | Seconds |
|
|
545
|
-
|
|
546
|
-
| `900` | 900 |
|
|
547
|
-
| `"15m"` | 900 |
|
|
548
|
-
| `"2h"` | 7
|
|
549
|
-
| `"2d"` | 172
|
|
550
|
-
| `"7d"` | 604
|
|
551
|
-
| `"1w"` | 604
|
|
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 |
|
|
702
|
+
| Strategy | Behavior |
|
|
556
703
|
|-----------|------------------------------------------------------------------|
|
|
557
|
-
| `backend` | Use only the expiry values returned by
|
|
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;
|
|
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
|
-
|
|
591
|
-
|
|
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
|
-
|
|
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
|
|
917
|
+
## Security
|
|
608
918
|
|
|
609
|
-
-
|
|
610
|
-
-
|
|
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
|
|