next-token-auth 1.0.15 → 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 +528 -317
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,55 +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
|
-
> **Breaking change in v1.1.0:** The secret is now server-side only. You
|
|
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.
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Why This Exists
|
|
12
12
|
|
|
13
|
-
Authentication in Next.js
|
|
13
|
+
Authentication in Next.js is tedious. You need to:
|
|
14
14
|
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
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
|
|
20
21
|
|
|
21
|
-
Most projects
|
|
22
|
+
Most projects spend days on auth boilerplate before shipping a single feature.
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
## The Solution
|
|
26
|
-
|
|
27
|
-
`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:
|
|
28
|
-
|
|
29
|
-
- Tokens are stored in cookies or memory
|
|
30
|
-
- Access tokens are automatically refreshed before they expire
|
|
31
|
-
- Sessions are restored on page load from stored tokens
|
|
32
|
-
- Routes can be protected client-side with a hook or server-side with middleware
|
|
33
|
-
- 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.
|
|
34
25
|
|
|
35
26
|
---
|
|
36
27
|
|
|
37
|
-
##
|
|
38
|
-
|
|
39
|
-
-
|
|
40
|
-
- `useAuth`
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
-
|
|
46
|
-
-
|
|
47
|
-
-
|
|
48
|
-
- `withAuth` — higher-order function to protect App Router route handlers
|
|
49
|
-
- `authMiddleware` — Next.js middleware factory for edge-level route protection with guest-only route support
|
|
50
|
-
- Flexible expiry parsing: `"15m"`, `"2h"`, `"2d"`, `"7d"`, `"1w"`, or plain seconds
|
|
51
|
-
- Three expiry strategies: `backend`, `config`, `hybrid`
|
|
52
|
-
- 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
|
|
53
39
|
|
|
54
40
|
---
|
|
55
41
|
|
|
@@ -57,30 +43,23 @@ Most projects end up with hundreds of lines of boilerplate before a single featu
|
|
|
57
43
|
|
|
58
44
|
```bash
|
|
59
45
|
npm install next-token-auth
|
|
60
|
-
# or
|
|
61
|
-
yarn add next-token-auth
|
|
62
|
-
# or
|
|
63
|
-
pnpm add next-token-auth
|
|
64
|
-
# or
|
|
65
|
-
bun add next-token-auth
|
|
66
46
|
```
|
|
67
47
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
react >= 18
|
|
73
|
-
react-dom >= 18
|
|
74
|
-
```
|
|
48
|
+
Peer dependencies (already in any Next.js project):
|
|
49
|
+
- `next >= 15.5.14`
|
|
50
|
+
- `react >= 18`
|
|
51
|
+
- `react-dom >= 18`
|
|
75
52
|
|
|
76
53
|
---
|
|
77
54
|
|
|
78
55
|
## Quick Start
|
|
79
56
|
|
|
80
|
-
### 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.
|
|
81
60
|
|
|
82
61
|
```ts
|
|
83
|
-
// lib/auth.ts
|
|
62
|
+
// lib/auth.ts
|
|
84
63
|
import type { AuthConfig } from "next-token-auth";
|
|
85
64
|
|
|
86
65
|
interface User {
|
|
@@ -90,23 +69,27 @@ interface User {
|
|
|
90
69
|
}
|
|
91
70
|
|
|
92
71
|
export const authConfig: AuthConfig<User> = {
|
|
93
|
-
|
|
72
|
+
// Your backend API base URL (no NEXT_PUBLIC_ prefix needed)
|
|
73
|
+
baseUrl: process.env.API_URL!,
|
|
94
74
|
|
|
75
|
+
// Your backend auth endpoints
|
|
95
76
|
endpoints: {
|
|
96
|
-
login: "/auth/login",
|
|
97
|
-
refresh: "/auth/refresh",
|
|
98
|
-
logout: "/auth/logout",
|
|
99
|
-
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)
|
|
100
81
|
},
|
|
101
82
|
|
|
83
|
+
// Route protection rules
|
|
102
84
|
routes: {
|
|
103
|
-
public: ["/", "/about"],
|
|
104
|
-
guestOnly: ["/login", "/register"],
|
|
105
|
-
protected: ["/dashboard*"],
|
|
106
|
-
loginPath: "/login",
|
|
107
|
-
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
|
|
108
90
|
},
|
|
109
91
|
|
|
92
|
+
// Token storage settings
|
|
110
93
|
token: {
|
|
111
94
|
storage: "cookie",
|
|
112
95
|
cookieName: "myapp.session",
|
|
@@ -114,33 +97,46 @@ export const authConfig: AuthConfig<User> = {
|
|
|
114
97
|
sameSite: "lax",
|
|
115
98
|
},
|
|
116
99
|
|
|
117
|
-
secret
|
|
100
|
+
// Encryption secret (32+ random characters)
|
|
101
|
+
secret: process.env.AUTH_SECRET!,
|
|
118
102
|
|
|
103
|
+
// Auto-refresh tokens before they expire
|
|
119
104
|
autoRefresh: true,
|
|
120
105
|
|
|
106
|
+
// Token expiry (matches your backend JWT settings)
|
|
121
107
|
expiry: {
|
|
122
|
-
accessTokenExpiresIn: "2d",
|
|
108
|
+
accessTokenExpiresIn: "2d", // can also be a number in seconds
|
|
123
109
|
refreshTokenExpiresIn: "7d",
|
|
124
|
-
strategy: "hybrid",
|
|
110
|
+
strategy: "hybrid", // backend first, fallback to config
|
|
125
111
|
},
|
|
126
112
|
};
|
|
127
113
|
```
|
|
128
114
|
|
|
129
|
-
|
|
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.
|
|
130
122
|
|
|
131
123
|
```ts
|
|
132
|
-
// lib/auth.client.ts
|
|
124
|
+
// lib/auth.client.ts
|
|
133
125
|
import type { ClientAuthConfig } from "next-token-auth";
|
|
134
126
|
|
|
135
127
|
export const clientAuthConfig: ClientAuthConfig = {
|
|
136
128
|
token: {
|
|
137
|
-
cookieName: "myapp.session",
|
|
129
|
+
cookieName: "myapp.session", // must match server config
|
|
138
130
|
},
|
|
139
131
|
autoRefresh: true,
|
|
140
132
|
};
|
|
141
133
|
```
|
|
142
134
|
|
|
143
|
-
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
### Step 3: Mount the Route Handlers
|
|
138
|
+
|
|
139
|
+
Create this file to handle login, logout, refresh, and session endpoints automatically.
|
|
144
140
|
|
|
145
141
|
```ts
|
|
146
142
|
// app/api/auth/[action]/route.ts
|
|
@@ -150,13 +146,17 @@ import { authConfig } from "@/lib/auth";
|
|
|
150
146
|
export const { GET, POST } = createAuthHandlers(authConfig);
|
|
151
147
|
```
|
|
152
148
|
|
|
153
|
-
This creates four endpoints
|
|
149
|
+
This creates four internal endpoints:
|
|
154
150
|
- `POST /api/auth/login` — authenticates and sets HttpOnly cookie
|
|
155
151
|
- `POST /api/auth/logout` — clears the session cookie
|
|
156
152
|
- `POST /api/auth/refresh` — refreshes the access token
|
|
157
153
|
- `GET /api/auth/session` — returns current user and auth status
|
|
158
154
|
|
|
159
|
-
|
|
155
|
+
Your `AuthProvider` calls these automatically. You never call them directly.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
### Step 4: Wrap your app
|
|
160
160
|
|
|
161
161
|
```tsx
|
|
162
162
|
// app/layout.tsx
|
|
@@ -167,36 +167,63 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|
|
167
167
|
return (
|
|
168
168
|
<html lang="en">
|
|
169
169
|
<body>
|
|
170
|
-
<AuthProvider config={clientAuthConfig}>
|
|
170
|
+
<AuthProvider config={clientAuthConfig}>
|
|
171
|
+
{children}
|
|
172
|
+
</AuthProvider>
|
|
171
173
|
</body>
|
|
172
174
|
</html>
|
|
173
175
|
);
|
|
174
176
|
}
|
|
175
177
|
```
|
|
176
178
|
|
|
177
|
-
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
### Step 5: Build your login page
|
|
178
182
|
|
|
179
183
|
```tsx
|
|
184
|
+
// app/login/page.tsx
|
|
180
185
|
"use client";
|
|
181
186
|
|
|
182
187
|
import { useAuth } from "next-token-auth/react";
|
|
188
|
+
import { useState } from "react";
|
|
183
189
|
|
|
184
190
|
export default function LoginPage() {
|
|
185
191
|
const { login, isLoading } = useAuth();
|
|
192
|
+
const [email, setEmail] = useState("");
|
|
193
|
+
const [password, setPassword] = useState("");
|
|
194
|
+
const [error, setError] = useState("");
|
|
186
195
|
|
|
187
|
-
async function handleSubmit(e: React.FormEvent
|
|
196
|
+
async function handleSubmit(e: React.FormEvent) {
|
|
188
197
|
e.preventDefault();
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
+
}
|
|
192
206
|
}
|
|
193
207
|
|
|
194
208
|
return (
|
|
195
209
|
<form onSubmit={handleSubmit}>
|
|
196
|
-
<input
|
|
197
|
-
|
|
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>}
|
|
198
225
|
<button type="submit" disabled={isLoading}>
|
|
199
|
-
{isLoading ? "Signing in
|
|
226
|
+
{isLoading ? "Signing in..." : "Sign in"}
|
|
200
227
|
</button>
|
|
201
228
|
</form>
|
|
202
229
|
);
|
|
@@ -205,101 +232,89 @@ export default function LoginPage() {
|
|
|
205
232
|
|
|
206
233
|
---
|
|
207
234
|
|
|
208
|
-
|
|
235
|
+
### Step 6: Protect a page
|
|
209
236
|
|
|
210
|
-
|
|
237
|
+
```tsx
|
|
238
|
+
// app/dashboard/page.tsx
|
|
239
|
+
"use client";
|
|
211
240
|
|
|
212
|
-
|
|
241
|
+
import { useRequireAuth, useSession } from "next-token-auth/react";
|
|
213
242
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
</AuthProvider>
|
|
218
|
-
```
|
|
243
|
+
export default function DashboardPage() {
|
|
244
|
+
// Redirects to /login if not authenticated
|
|
245
|
+
useRequireAuth();
|
|
219
246
|
|
|
220
|
-
|
|
247
|
+
const { user, isAuthenticated } = useSession();
|
|
221
248
|
|
|
222
|
-
|
|
249
|
+
if (!isAuthenticated) return null; // while redirecting
|
|
223
250
|
|
|
224
|
-
|
|
251
|
+
return (
|
|
252
|
+
<main>
|
|
253
|
+
<h1>Dashboard</h1>
|
|
254
|
+
<p>Welcome, {user?.name}</p>
|
|
255
|
+
</main>
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
```
|
|
225
259
|
|
|
226
|
-
|
|
227
|
-
interface ClientAuthConfig {
|
|
228
|
-
token?: {
|
|
229
|
-
cookieName?: string; // default: "next-token-auth.session"
|
|
230
|
-
};
|
|
260
|
+
---
|
|
231
261
|
|
|
232
|
-
|
|
233
|
-
loginPath?: string; // where to redirect unauthenticated users (default: "/login")
|
|
234
|
-
redirectAuthenticatedTo?: string; // where to redirect authenticated users on guestOnly routes (default: "/dashboard")
|
|
235
|
-
};
|
|
262
|
+
### Step 7: Add middleware (optional but recommended)
|
|
236
263
|
|
|
237
|
-
|
|
264
|
+
Protect routes at the edge for better performance and security.
|
|
238
265
|
|
|
239
|
-
|
|
266
|
+
```ts
|
|
267
|
+
// middleware.ts (project root, next to app/)
|
|
268
|
+
import { authMiddleware } from "next-token-auth/server";
|
|
269
|
+
import { authConfig } from "@/lib/auth";
|
|
240
270
|
|
|
241
|
-
|
|
242
|
-
onLogin?: (session: AuthSession) => void;
|
|
243
|
-
onLogout?: () => void;
|
|
244
|
-
}
|
|
245
|
-
```
|
|
271
|
+
export const middleware = authMiddleware(authConfig);
|
|
246
272
|
|
|
247
|
-
|
|
273
|
+
export const config = {
|
|
274
|
+
// Run middleware on these routes
|
|
275
|
+
matcher: ["/login", "/register", "/dashboard*", "/profile*"],
|
|
276
|
+
};
|
|
277
|
+
```
|
|
248
278
|
|
|
249
|
-
|
|
279
|
+
**Next.js 16+ users:** Rename the file to `proxy.ts` and export `proxy` instead of `middleware`:
|
|
250
280
|
|
|
251
281
|
```ts
|
|
252
|
-
|
|
253
|
-
|
|
282
|
+
// proxy.ts
|
|
283
|
+
export const proxy = authMiddleware(authConfig);
|
|
284
|
+
```
|
|
254
285
|
|
|
255
|
-
|
|
256
|
-
login: string; // required
|
|
257
|
-
refresh: string; // required
|
|
258
|
-
register?: string;
|
|
259
|
-
logout?: string;
|
|
260
|
-
me?: string; // fetched to populate session.user
|
|
261
|
-
};
|
|
286
|
+
---
|
|
262
287
|
|
|
263
|
-
|
|
264
|
-
public: string[]; // always accessible
|
|
265
|
-
protected: string[]; // require auth, supports wildcard: "/dashboard*"
|
|
266
|
-
guestOnly?: string[]; // only accessible when NOT authenticated
|
|
267
|
-
loginPath?: string; // where to redirect unauthenticated users (default: "/login")
|
|
268
|
-
redirectAuthenticatedTo?: string; // where to redirect authenticated users on guestOnly routes (default: "/dashboard")
|
|
269
|
-
};
|
|
288
|
+
## How It Works
|
|
270
289
|
|
|
271
|
-
|
|
272
|
-
storage: "cookie" | "memory";
|
|
273
|
-
cookieName?: string;
|
|
274
|
-
secure?: boolean; // default: true
|
|
275
|
-
sameSite?: "strict" | "lax" | "none"; // default: "lax"
|
|
276
|
-
};
|
|
290
|
+
### The Flow
|
|
277
291
|
|
|
278
|
-
|
|
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 }`
|
|
279
300
|
|
|
280
|
-
|
|
281
|
-
refreshThreshold?: number;
|
|
301
|
+
### Why HttpOnly Cookies?
|
|
282
302
|
|
|
283
|
-
|
|
284
|
-
accessTokenExpiresIn?: number | string; // e.g. "2d", 3600
|
|
285
|
-
refreshTokenExpiresIn?: number | string; // e.g. "7d"
|
|
286
|
-
strategy?: "backend" | "config" | "hybrid"; // default: "hybrid"
|
|
287
|
-
};
|
|
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.
|
|
288
304
|
|
|
289
|
-
|
|
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).
|
|
290
306
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
onRefreshError?: (error: unknown) => void;
|
|
295
|
-
}
|
|
296
|
-
```
|
|
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.
|
|
297
310
|
|
|
298
311
|
---
|
|
299
312
|
|
|
300
|
-
|
|
313
|
+
## API Reference
|
|
314
|
+
|
|
315
|
+
### `useAuth()`
|
|
301
316
|
|
|
302
|
-
The
|
|
317
|
+
The main hook for authentication operations.
|
|
303
318
|
|
|
304
319
|
```ts
|
|
305
320
|
const { session, login, logout, refresh, isLoading } = useAuth<User>();
|
|
@@ -307,27 +322,37 @@ const { session, login, logout, refresh, isLoading } = useAuth<User>();
|
|
|
307
322
|
|
|
308
323
|
| Property | Type | Description |
|
|
309
324
|
|-------------|-----------------------------------------|--------------------------------------------------|
|
|
310
|
-
| `session` | `AuthSession<User>` | Current
|
|
311
|
-
| `login` | `(input: LoginInput) => Promise<void>` |
|
|
312
|
-
| `logout` | `() => Promise<void>` |
|
|
313
|
-
| `refresh` | `() => Promise<void>` |
|
|
314
|
-
| `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:
|
|
315
332
|
|
|
316
|
-
|
|
333
|
+
```ts
|
|
334
|
+
await login({ email, password });
|
|
335
|
+
await login({ username, password, rememberMe: true });
|
|
336
|
+
```
|
|
317
337
|
|
|
318
338
|
---
|
|
319
339
|
|
|
320
|
-
### `useSession`
|
|
340
|
+
### `useSession()`
|
|
321
341
|
|
|
322
|
-
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.
|
|
323
343
|
|
|
324
344
|
```ts
|
|
325
|
-
const { user,
|
|
345
|
+
const { user, isAuthenticated } = useSession<User>();
|
|
326
346
|
```
|
|
327
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
|
+
|
|
328
353
|
---
|
|
329
354
|
|
|
330
|
-
### `useRequireAuth`
|
|
355
|
+
### `useRequireAuth(options?)`
|
|
331
356
|
|
|
332
357
|
Redirects unauthenticated users. Call it at the top of any protected client component.
|
|
333
358
|
|
|
@@ -335,45 +360,49 @@ Redirects unauthenticated users. Call it at the top of any protected client comp
|
|
|
335
360
|
useRequireAuth({ redirectTo: "/login" });
|
|
336
361
|
```
|
|
337
362
|
|
|
338
|
-
|
|
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:
|
|
339
373
|
|
|
340
374
|
```ts
|
|
375
|
+
import { useRouter } from "next/navigation";
|
|
376
|
+
|
|
341
377
|
useRequireAuth({
|
|
342
378
|
onUnauthenticated: () => router.push("/login?from=/dashboard"),
|
|
343
379
|
});
|
|
344
380
|
```
|
|
345
381
|
|
|
346
|
-
The hook waits for `isLoading` to be `false` before acting, so it won't flash a redirect during the initial session restore.
|
|
347
|
-
|
|
348
|
-
| Option | Type | Default |
|
|
349
|
-
|---------------------|--------------|------------|
|
|
350
|
-
| `redirectTo` | `string` | `"/login"` |
|
|
351
|
-
| `onUnauthenticated` | `() => void` | — |
|
|
352
|
-
|
|
353
382
|
---
|
|
354
383
|
|
|
355
384
|
### Making Authenticated API Requests
|
|
356
385
|
|
|
357
|
-
Since tokens are
|
|
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.
|
|
358
387
|
|
|
359
|
-
|
|
388
|
+
**Client-side:**
|
|
360
389
|
|
|
361
390
|
```ts
|
|
362
391
|
"use client";
|
|
363
392
|
|
|
364
|
-
export default function
|
|
365
|
-
async function
|
|
393
|
+
export default function OrdersPage() {
|
|
394
|
+
async function loadOrders() {
|
|
366
395
|
// The session cookie is automatically sent with this request
|
|
367
396
|
const res = await fetch("/api/orders");
|
|
368
397
|
const data = await res.json();
|
|
369
398
|
console.log(data);
|
|
370
399
|
}
|
|
371
400
|
|
|
372
|
-
return <button onClick={
|
|
401
|
+
return <button onClick={loadOrders}>Load Orders</button>;
|
|
373
402
|
}
|
|
374
403
|
```
|
|
375
404
|
|
|
376
|
-
|
|
405
|
+
**Server-side (your API route):**
|
|
377
406
|
|
|
378
407
|
```ts
|
|
379
408
|
// app/api/orders/route.ts
|
|
@@ -381,7 +410,7 @@ import { getServerSession } from "next-token-auth/server";
|
|
|
381
410
|
import { authConfig } from "@/lib/auth";
|
|
382
411
|
import { cookies } from "next/headers";
|
|
383
412
|
|
|
384
|
-
export async function GET(
|
|
413
|
+
export async function GET() {
|
|
385
414
|
const cookieStore = await cookies();
|
|
386
415
|
const session = await getServerSession(
|
|
387
416
|
{ cookies: { get: (name) => cookieStore.get(name) } },
|
|
@@ -392,9 +421,11 @@ export async function GET(req: Request) {
|
|
|
392
421
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
393
422
|
}
|
|
394
423
|
|
|
395
|
-
//
|
|
424
|
+
// Now call your backend with the access token
|
|
396
425
|
const res = await fetch(`${authConfig.baseUrl}/orders`, {
|
|
397
|
-
headers: {
|
|
426
|
+
headers: {
|
|
427
|
+
Authorization: `Bearer ${session.tokens!.accessToken}`,
|
|
428
|
+
},
|
|
398
429
|
});
|
|
399
430
|
|
|
400
431
|
return Response.json(await res.json());
|
|
@@ -405,9 +436,44 @@ This keeps tokens secure — they never leave the server.
|
|
|
405
436
|
|
|
406
437
|
---
|
|
407
438
|
|
|
408
|
-
###
|
|
439
|
+
### `getServerSession(req, config)`
|
|
440
|
+
|
|
441
|
+
Reads and validates the session in server components and API routes.
|
|
442
|
+
|
|
443
|
+
```ts
|
|
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";
|
|
448
|
+
import { authConfig } from "@/lib/auth";
|
|
449
|
+
|
|
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
|
+
}
|
|
463
|
+
```
|
|
464
|
+
|
|
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 }`
|
|
471
|
+
|
|
472
|
+
---
|
|
473
|
+
|
|
474
|
+
### `withAuth(config, handler, options?)`
|
|
409
475
|
|
|
410
|
-
|
|
476
|
+
Wraps App Router route handlers to require authentication.
|
|
411
477
|
|
|
412
478
|
```ts
|
|
413
479
|
// app/api/profile/route.ts
|
|
@@ -415,86 +481,109 @@ import { withAuth } from "next-token-auth/server";
|
|
|
415
481
|
import { authConfig } from "@/lib/auth";
|
|
416
482
|
|
|
417
483
|
export const GET = withAuth(authConfig, async (req, session) => {
|
|
484
|
+
// session.user is guaranteed to exist here
|
|
418
485
|
return Response.json({ user: session.user });
|
|
419
486
|
});
|
|
420
487
|
```
|
|
421
488
|
|
|
422
|
-
Unauthenticated requests are redirected to `/login` by default.
|
|
489
|
+
Unauthenticated requests are redirected to `/login` by default. Override with:
|
|
490
|
+
|
|
491
|
+
```ts
|
|
492
|
+
export const GET = withAuth(
|
|
493
|
+
authConfig,
|
|
494
|
+
async (req, session) => { /* ... */ },
|
|
495
|
+
{ redirectTo: "/sign-in" }
|
|
496
|
+
);
|
|
497
|
+
```
|
|
423
498
|
|
|
424
499
|
---
|
|
425
500
|
|
|
426
|
-
###
|
|
501
|
+
### `authMiddleware(config)`
|
|
427
502
|
|
|
428
|
-
|
|
503
|
+
Creates a Next.js middleware function for edge-level route protection.
|
|
429
504
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
505
|
+
```ts
|
|
506
|
+
// middleware.ts (project root, next to app/)
|
|
507
|
+
import { authMiddleware } from "next-token-auth/server";
|
|
508
|
+
import { authConfig } from "@/lib/auth";
|
|
433
509
|
|
|
434
|
-
|
|
510
|
+
export const middleware = authMiddleware(authConfig);
|
|
435
511
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
export const authConfig: AuthConfig = {
|
|
439
|
-
// ...
|
|
440
|
-
routes: {
|
|
441
|
-
public: ["/", "/about"],
|
|
442
|
-
guestOnly: ["/sign-in", "/sign-up"], // any names you want
|
|
443
|
-
protected: ["/app*", "/account*"],
|
|
444
|
-
loginPath: "/sign-in", // where unauthenticated users are sent
|
|
445
|
-
redirectAuthenticatedTo: "/app/home", // where authenticated users are sent from guestOnly routes
|
|
446
|
-
},
|
|
512
|
+
export const config = {
|
|
513
|
+
matcher: ["/login", "/register", "/dashboard*", "/profile*"],
|
|
447
514
|
};
|
|
448
515
|
```
|
|
449
516
|
|
|
517
|
+
**Next.js 16+ users:** Rename the file to `proxy.ts` and change the export:
|
|
518
|
+
|
|
450
519
|
```ts
|
|
451
|
-
//
|
|
452
|
-
|
|
453
|
-
|
|
520
|
+
// proxy.ts
|
|
521
|
+
export const proxy = authMiddleware(authConfig);
|
|
522
|
+
```
|
|
454
523
|
|
|
455
|
-
|
|
456
|
-
export const middleware = authMiddleware(authConfig);
|
|
524
|
+
---
|
|
457
525
|
|
|
458
|
-
|
|
459
|
-
export const proxy = authMiddleware(authConfig);
|
|
526
|
+
## Route Protection Explained
|
|
460
527
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
528
|
+
The middleware supports three route categories:
|
|
529
|
+
|
|
530
|
+
### 1. Public routes
|
|
531
|
+
|
|
532
|
+
Always accessible, no auth check. Example: homepage, about page.
|
|
533
|
+
|
|
534
|
+
```ts
|
|
535
|
+
routes: {
|
|
536
|
+
public: ["/", "/about", "/pricing"],
|
|
537
|
+
}
|
|
464
538
|
```
|
|
465
539
|
|
|
466
|
-
|
|
540
|
+
### 2. Protected routes
|
|
541
|
+
|
|
542
|
+
Require authentication. Unauthenticated users are redirected to `loginPath`.
|
|
467
543
|
|
|
468
544
|
```ts
|
|
469
|
-
// Using /auth/* convention
|
|
470
545
|
routes: {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
loginPath: "/auth/login",
|
|
474
|
-
redirectAuthenticatedTo: "/dashboard",
|
|
546
|
+
protected: ["/dashboard*", "/settings*"],
|
|
547
|
+
loginPath: "/login", // where to send unauthenticated users
|
|
475
548
|
}
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
Wildcard matching:
|
|
552
|
+
- `"/dashboard*"` matches `/dashboard`, `/dashboard/`, `/dashboard/settings`
|
|
553
|
+
- `"/api/admin*"` matches `/api/admin`, `/api/admin/users`
|
|
554
|
+
|
|
555
|
+
### 3. Guest-only routes
|
|
556
|
+
|
|
557
|
+
Only accessible when NOT authenticated. Authenticated users are redirected away.
|
|
558
|
+
|
|
559
|
+
Use this for login and register pages so logged-in users can't access them.
|
|
476
560
|
|
|
477
|
-
|
|
561
|
+
```ts
|
|
478
562
|
routes: {
|
|
479
|
-
guestOnly: ["/
|
|
480
|
-
|
|
481
|
-
loginPath: "/portal",
|
|
482
|
-
redirectAuthenticatedTo: "/admin",
|
|
563
|
+
guestOnly: ["/login", "/register"],
|
|
564
|
+
redirectAuthenticatedTo: "/dashboard", // where to send authenticated users
|
|
483
565
|
}
|
|
484
566
|
```
|
|
485
567
|
|
|
486
|
-
|
|
568
|
+
### Resolution order
|
|
569
|
+
|
|
570
|
+
When a request hits the middleware:
|
|
571
|
+
|
|
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`
|
|
575
|
+
|
|
576
|
+
### Matcher vs routes config
|
|
487
577
|
|
|
488
|
-
|
|
489
|
-
2. `public` — always allow through
|
|
490
|
-
3. `protected` — require valid session, redirect to `loginPath` if missing
|
|
578
|
+
The `matcher` in your middleware file controls which routes Next.js runs the middleware on at all:
|
|
491
579
|
|
|
492
|
-
|
|
580
|
+
```ts
|
|
581
|
+
export const config = {
|
|
582
|
+
matcher: ["/login", "/dashboard*"],
|
|
583
|
+
};
|
|
584
|
+
```
|
|
493
585
|
|
|
494
|
-
|
|
495
|
-
- 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
|
|
496
|
-
- `loginPath` defaults to `"/login"` if not set
|
|
497
|
-
- `redirectAuthenticatedTo` defaults to `"/dashboard"` if not set
|
|
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.
|
|
498
587
|
|
|
499
588
|
---
|
|
500
589
|
|
|
@@ -502,9 +591,9 @@ Two things to keep in mind:
|
|
|
502
591
|
|
|
503
592
|
### How tokens are stored
|
|
504
593
|
|
|
505
|
-
Tokens are
|
|
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`.
|
|
506
595
|
|
|
507
|
-
|
|
596
|
+
JavaScript in the browser cannot read the cookie — only the server can decrypt it.
|
|
508
597
|
|
|
509
598
|
### Session restore on page load
|
|
510
599
|
|
|
@@ -512,67 +601,31 @@ When `AuthProvider` mounts, it calls `GET /api/auth/session`, which:
|
|
|
512
601
|
|
|
513
602
|
1. Reads the encrypted session cookie server-side
|
|
514
603
|
2. Decrypts it using your `secret`
|
|
515
|
-
3. Checks
|
|
516
|
-
4.
|
|
604
|
+
3. Checks if the refresh token is expired
|
|
605
|
+
4. Fetches the user profile from your backend (if `me` endpoint is configured)
|
|
517
606
|
5. Returns `{ user, isAuthenticated }` to the client
|
|
518
607
|
|
|
519
608
|
The client never sees the raw tokens — only the user object and auth status.
|
|
520
609
|
|
|
521
|
-
### Automatic refresh
|
|
610
|
+
### Automatic token refresh
|
|
522
611
|
|
|
523
|
-
When `autoRefresh: true`, the provider calls `POST /api/auth/refresh`
|
|
612
|
+
When `autoRefresh: true`, the provider periodically calls `POST /api/auth/refresh` (based on `refreshThreshold`, default 60 seconds before expiry).
|
|
524
613
|
|
|
614
|
+
The Route Handler:
|
|
525
615
|
1. Reads the encrypted cookie
|
|
526
616
|
2. Checks if the refresh token is still valid
|
|
527
617
|
3. Calls your backend's refresh endpoint with the refresh token
|
|
528
618
|
4. Encrypts the new tokens and updates the HttpOnly cookie
|
|
529
619
|
|
|
530
|
-
### Refresh flow
|
|
531
|
-
|
|
532
|
-
```
|
|
533
|
-
Client detects expiry → POST /api/auth/refresh → backend refresh endpoint → new encrypted cookie set
|
|
534
|
-
```
|
|
535
|
-
|
|
536
620
|
If the refresh token is expired, the session is cleared.
|
|
537
621
|
|
|
538
622
|
---
|
|
539
623
|
|
|
540
|
-
##
|
|
624
|
+
## Backend API Requirements
|
|
541
625
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
```ts
|
|
545
|
-
// app/dashboard/page.tsx
|
|
546
|
-
import { redirect } from "next/navigation";
|
|
547
|
-
import { cookies } from "next/headers";
|
|
548
|
-
import { getServerSession } from "next-token-auth/server";
|
|
549
|
-
import { authConfig } from "@/lib/auth";
|
|
626
|
+
Your backend needs to implement these endpoints:
|
|
550
627
|
|
|
551
|
-
|
|
552
|
-
const cookieStore = await cookies();
|
|
553
|
-
|
|
554
|
-
const session = await getServerSession(
|
|
555
|
-
{ cookies: { get: (name) => cookieStore.get(name) } },
|
|
556
|
-
authConfig
|
|
557
|
-
);
|
|
558
|
-
|
|
559
|
-
if (!session.isAuthenticated) {
|
|
560
|
-
redirect("/login");
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
return <h1>Welcome, {session.user.name}</h1>;
|
|
564
|
-
}
|
|
565
|
-
```
|
|
566
|
-
|
|
567
|
-
`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.
|
|
568
|
-
|
|
569
|
-
---
|
|
570
|
-
|
|
571
|
-
## Backend Requirements
|
|
572
|
-
|
|
573
|
-
Your API needs to implement the following contract:
|
|
574
|
-
|
|
575
|
-
### `POST /auth/login`
|
|
628
|
+
### `POST /auth/login` (or whatever you set in `endpoints.login`)
|
|
576
629
|
|
|
577
630
|
Request body: whatever fields you pass to `login()` (e.g. `{ email, password }`)
|
|
578
631
|
|
|
@@ -580,9 +633,13 @@ Response:
|
|
|
580
633
|
|
|
581
634
|
```json
|
|
582
635
|
{
|
|
583
|
-
"accessToken": "
|
|
584
|
-
"refreshToken": "
|
|
585
|
-
"user": {
|
|
636
|
+
"accessToken": "eyJhbGc...",
|
|
637
|
+
"refreshToken": "eyJhbGc...",
|
|
638
|
+
"user": {
|
|
639
|
+
"id": "123",
|
|
640
|
+
"email": "user@example.com",
|
|
641
|
+
"name": "Jane Doe"
|
|
642
|
+
},
|
|
586
643
|
|
|
587
644
|
// Optional — used by "backend" and "hybrid" expiry strategies
|
|
588
645
|
"accessTokenExpiresIn": "2d",
|
|
@@ -598,39 +655,53 @@ Response:
|
|
|
598
655
|
Request body:
|
|
599
656
|
|
|
600
657
|
```json
|
|
601
|
-
{
|
|
658
|
+
{
|
|
659
|
+
"refreshToken": "eyJhbGc..."
|
|
660
|
+
}
|
|
602
661
|
```
|
|
603
662
|
|
|
604
663
|
Response: same shape as the login response (new `accessToken` + `refreshToken`).
|
|
605
664
|
|
|
606
|
-
### `GET /auth/me`
|
|
665
|
+
### `GET /auth/me` (optional)
|
|
607
666
|
|
|
608
667
|
Returns the current user object. Called after login and on session restore if the `me` endpoint is configured.
|
|
609
668
|
|
|
610
|
-
|
|
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)
|
|
611
680
|
|
|
612
|
-
Called on logout. Failure is silently ignored —
|
|
681
|
+
Called on logout. Failure is silently ignored — the session cookie is always cleared locally regardless.
|
|
613
682
|
|
|
614
683
|
---
|
|
615
684
|
|
|
616
685
|
## Expiry Formats
|
|
617
686
|
|
|
618
|
-
The
|
|
687
|
+
The library accepts expiry values in multiple formats:
|
|
688
|
+
|
|
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 |
|
|
619
697
|
|
|
620
|
-
|
|
621
|
-
|---------|-----------|
|
|
622
|
-
| `900` | 900 |
|
|
623
|
-
| `"15m"` | 900 |
|
|
624
|
-
| `"2h"` | 7 200 |
|
|
625
|
-
| `"2d"` | 172 800 |
|
|
626
|
-
| `"7d"` | 604 800 |
|
|
627
|
-
| `"1w"` | 604 800 |
|
|
698
|
+
Supported units: `s` (seconds), `m` (minutes), `h` (hours), `d` (days), `w` (weeks)
|
|
628
699
|
|
|
629
700
|
### Expiry strategies
|
|
630
701
|
|
|
631
|
-
| Strategy |
|
|
702
|
+
| Strategy | Behavior |
|
|
632
703
|
|-----------|------------------------------------------------------------------|
|
|
633
|
-
| `backend` | Use only the expiry values returned by
|
|
704
|
+
| `backend` | Use only the expiry values returned by your API |
|
|
634
705
|
| `config` | Use only the values set in `expiry` config |
|
|
635
706
|
| `hybrid` | API response first; fall back to config if not present (default) |
|
|
636
707
|
|
|
@@ -638,6 +709,79 @@ The `parseExpiry` utility accepts:
|
|
|
638
709
|
|
|
639
710
|
---
|
|
640
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
|
+
|
|
641
785
|
## TypeScript Types
|
|
642
786
|
|
|
643
787
|
```ts
|
|
@@ -651,7 +795,7 @@ interface AuthTokens {
|
|
|
651
795
|
accessToken: string;
|
|
652
796
|
refreshToken: string;
|
|
653
797
|
accessTokenExpiresAt: number; // Unix timestamp in ms
|
|
654
|
-
refreshTokenExpiresAt?: number;
|
|
798
|
+
refreshTokenExpiresAt?: number;
|
|
655
799
|
}
|
|
656
800
|
|
|
657
801
|
interface LoginResponse<User = unknown> {
|
|
@@ -662,34 +806,102 @@ interface LoginResponse<User = unknown> {
|
|
|
662
806
|
accessTokenExpiresIn?: number | string;
|
|
663
807
|
refreshTokenExpiresIn?: number | string;
|
|
664
808
|
}
|
|
809
|
+
```
|
|
665
810
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
811
|
+
All types are exported from `next-token-auth`.
|
|
812
|
+
|
|
813
|
+
---
|
|
814
|
+
|
|
815
|
+
## Common Patterns
|
|
816
|
+
|
|
817
|
+
### Custom user type
|
|
818
|
+
|
|
819
|
+
```ts
|
|
820
|
+
interface MyUser {
|
|
821
|
+
id: string;
|
|
822
|
+
email: string;
|
|
823
|
+
role: "admin" | "user";
|
|
676
824
|
}
|
|
677
825
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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 = "/";
|
|
686
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
|
+
);
|
|
855
|
+
```
|
|
856
|
+
|
|
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
|
+
}
|
|
687
869
|
|
|
688
|
-
|
|
689
|
-
|
|
870
|
+
if (session.user.role !== "admin") {
|
|
871
|
+
redirect("/dashboard");
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
return <h1>Admin Panel</h1>;
|
|
875
|
+
}
|
|
690
876
|
```
|
|
691
877
|
|
|
692
|
-
|
|
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)`
|
|
693
905
|
|
|
694
906
|
---
|
|
695
907
|
|
|
@@ -702,15 +914,14 @@ All types are exported from the root `next-token-auth` import.
|
|
|
702
914
|
|
|
703
915
|
---
|
|
704
916
|
|
|
705
|
-
## Security
|
|
917
|
+
## Security
|
|
706
918
|
|
|
707
|
-
-
|
|
708
|
-
-
|
|
919
|
+
- Tokens are stored in HttpOnly cookies — JavaScript cannot read them
|
|
920
|
+
- Cookies are AES-GCM encrypted server-side using your `secret`
|
|
709
921
|
- The `secret` never leaves the server — it's only used in Route Handlers, middleware, and `getServerSession`
|
|
710
922
|
- `AuthProvider` receives `ClientAuthConfig` which does not contain `secret` or `baseUrl`
|
|
711
|
-
- Use a random 32-character string for `secret` in production — never commit it
|
|
712
923
|
- Cookies use `Secure` and `SameSite` flags by default for CSRF protection
|
|
713
|
-
-
|
|
924
|
+
- Use a random 32-character string for `secret` in production — never commit it
|
|
714
925
|
|
|
715
926
|
---
|
|
716
927
|
|