next-token-auth 1.0.15 → 1.0.17
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 +530 -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,48 @@ 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.
|
|
140
|
+
|
|
141
|
+
**Important:** The file path must be exactly `app/api/auth/[action]/route.ts` — the `[action]` part is a Next.js dynamic route segment (keep the square brackets as-is). Do not rename it.
|
|
144
142
|
|
|
145
143
|
```ts
|
|
146
144
|
// app/api/auth/[action]/route.ts
|
|
@@ -150,13 +148,17 @@ import { authConfig } from "@/lib/auth";
|
|
|
150
148
|
export const { GET, POST } = createAuthHandlers(authConfig);
|
|
151
149
|
```
|
|
152
150
|
|
|
153
|
-
This creates four endpoints
|
|
151
|
+
This creates four internal endpoints:
|
|
154
152
|
- `POST /api/auth/login` — authenticates and sets HttpOnly cookie
|
|
155
153
|
- `POST /api/auth/logout` — clears the session cookie
|
|
156
154
|
- `POST /api/auth/refresh` — refreshes the access token
|
|
157
155
|
- `GET /api/auth/session` — returns current user and auth status
|
|
158
156
|
|
|
159
|
-
|
|
157
|
+
Your `AuthProvider` calls these automatically. You never call them directly.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
### Step 4: Wrap your app
|
|
160
162
|
|
|
161
163
|
```tsx
|
|
162
164
|
// app/layout.tsx
|
|
@@ -167,36 +169,63 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|
|
167
169
|
return (
|
|
168
170
|
<html lang="en">
|
|
169
171
|
<body>
|
|
170
|
-
<AuthProvider config={clientAuthConfig}>
|
|
172
|
+
<AuthProvider config={clientAuthConfig}>
|
|
173
|
+
{children}
|
|
174
|
+
</AuthProvider>
|
|
171
175
|
</body>
|
|
172
176
|
</html>
|
|
173
177
|
);
|
|
174
178
|
}
|
|
175
179
|
```
|
|
176
180
|
|
|
177
|
-
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
### Step 5: Build your login page
|
|
178
184
|
|
|
179
185
|
```tsx
|
|
186
|
+
// app/login/page.tsx
|
|
180
187
|
"use client";
|
|
181
188
|
|
|
182
189
|
import { useAuth } from "next-token-auth/react";
|
|
190
|
+
import { useState } from "react";
|
|
183
191
|
|
|
184
192
|
export default function LoginPage() {
|
|
185
193
|
const { login, isLoading } = useAuth();
|
|
194
|
+
const [email, setEmail] = useState("");
|
|
195
|
+
const [password, setPassword] = useState("");
|
|
196
|
+
const [error, setError] = useState("");
|
|
186
197
|
|
|
187
|
-
async function handleSubmit(e: React.FormEvent
|
|
198
|
+
async function handleSubmit(e: React.FormEvent) {
|
|
188
199
|
e.preventDefault();
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
200
|
+
setError("");
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
await login({ email, password });
|
|
204
|
+
window.location.href = "/dashboard";
|
|
205
|
+
} catch (err) {
|
|
206
|
+
setError(err instanceof Error ? err.message : "Login failed");
|
|
207
|
+
}
|
|
192
208
|
}
|
|
193
209
|
|
|
194
210
|
return (
|
|
195
211
|
<form onSubmit={handleSubmit}>
|
|
196
|
-
<input
|
|
197
|
-
|
|
212
|
+
<input
|
|
213
|
+
type="email"
|
|
214
|
+
value={email}
|
|
215
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
216
|
+
placeholder="Email"
|
|
217
|
+
required
|
|
218
|
+
/>
|
|
219
|
+
<input
|
|
220
|
+
type="password"
|
|
221
|
+
value={password}
|
|
222
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
223
|
+
placeholder="Password"
|
|
224
|
+
required
|
|
225
|
+
/>
|
|
226
|
+
{error && <p style={{ color: "red" }}>{error}</p>}
|
|
198
227
|
<button type="submit" disabled={isLoading}>
|
|
199
|
-
{isLoading ? "Signing in
|
|
228
|
+
{isLoading ? "Signing in..." : "Sign in"}
|
|
200
229
|
</button>
|
|
201
230
|
</form>
|
|
202
231
|
);
|
|
@@ -205,101 +234,89 @@ export default function LoginPage() {
|
|
|
205
234
|
|
|
206
235
|
---
|
|
207
236
|
|
|
208
|
-
|
|
237
|
+
### Step 6: Protect a page
|
|
209
238
|
|
|
210
|
-
|
|
239
|
+
```tsx
|
|
240
|
+
// app/dashboard/page.tsx
|
|
241
|
+
"use client";
|
|
211
242
|
|
|
212
|
-
|
|
243
|
+
import { useRequireAuth, useSession } from "next-token-auth/react";
|
|
213
244
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
</AuthProvider>
|
|
218
|
-
```
|
|
245
|
+
export default function DashboardPage() {
|
|
246
|
+
// Redirects to /login if not authenticated
|
|
247
|
+
useRequireAuth();
|
|
219
248
|
|
|
220
|
-
|
|
249
|
+
const { user, isAuthenticated } = useSession();
|
|
221
250
|
|
|
222
|
-
|
|
251
|
+
if (!isAuthenticated) return null; // while redirecting
|
|
223
252
|
|
|
224
|
-
|
|
253
|
+
return (
|
|
254
|
+
<main>
|
|
255
|
+
<h1>Dashboard</h1>
|
|
256
|
+
<p>Welcome, {user?.name}</p>
|
|
257
|
+
</main>
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
```
|
|
225
261
|
|
|
226
|
-
|
|
227
|
-
interface ClientAuthConfig {
|
|
228
|
-
token?: {
|
|
229
|
-
cookieName?: string; // default: "next-token-auth.session"
|
|
230
|
-
};
|
|
262
|
+
---
|
|
231
263
|
|
|
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
|
-
};
|
|
264
|
+
### Step 7: Add middleware (optional but recommended)
|
|
236
265
|
|
|
237
|
-
|
|
266
|
+
Protect routes at the edge for better performance and security.
|
|
238
267
|
|
|
239
|
-
|
|
268
|
+
```ts
|
|
269
|
+
// middleware.ts (project root, next to app/)
|
|
270
|
+
import { authMiddleware } from "next-token-auth/server";
|
|
271
|
+
import { authConfig } from "@/lib/auth";
|
|
240
272
|
|
|
241
|
-
|
|
242
|
-
onLogin?: (session: AuthSession) => void;
|
|
243
|
-
onLogout?: () => void;
|
|
244
|
-
}
|
|
245
|
-
```
|
|
273
|
+
export const middleware = authMiddleware(authConfig);
|
|
246
274
|
|
|
247
|
-
|
|
275
|
+
export const config = {
|
|
276
|
+
// Run middleware on these routes
|
|
277
|
+
matcher: ["/login", "/register", "/dashboard*", "/profile*"],
|
|
278
|
+
};
|
|
279
|
+
```
|
|
248
280
|
|
|
249
|
-
|
|
281
|
+
**Next.js 16+ users:** Rename the file to `proxy.ts` and export `proxy` instead of `middleware`:
|
|
250
282
|
|
|
251
283
|
```ts
|
|
252
|
-
|
|
253
|
-
|
|
284
|
+
// proxy.ts
|
|
285
|
+
export const proxy = authMiddleware(authConfig);
|
|
286
|
+
```
|
|
254
287
|
|
|
255
|
-
|
|
256
|
-
login: string; // required
|
|
257
|
-
refresh: string; // required
|
|
258
|
-
register?: string;
|
|
259
|
-
logout?: string;
|
|
260
|
-
me?: string; // fetched to populate session.user
|
|
261
|
-
};
|
|
288
|
+
---
|
|
262
289
|
|
|
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
|
-
};
|
|
290
|
+
## How It Works
|
|
270
291
|
|
|
271
|
-
|
|
272
|
-
storage: "cookie" | "memory";
|
|
273
|
-
cookieName?: string;
|
|
274
|
-
secure?: boolean; // default: true
|
|
275
|
-
sameSite?: "strict" | "lax" | "none"; // default: "lax"
|
|
276
|
-
};
|
|
292
|
+
### The Flow
|
|
277
293
|
|
|
278
|
-
|
|
294
|
+
1. User submits login form → `useAuth().login()` is called
|
|
295
|
+
2. Client sends credentials to `POST /api/auth/login` (your Route Handler)
|
|
296
|
+
3. Route Handler calls your backend API, gets tokens back
|
|
297
|
+
4. Route Handler encrypts tokens with your `secret` and sets an HttpOnly cookie
|
|
298
|
+
5. Client receives `{ ok: true, user }` (no tokens — they're in the cookie)
|
|
299
|
+
6. `AuthProvider` updates state → `session.isAuthenticated = true`
|
|
300
|
+
7. On page reload, `AuthProvider` calls `GET /api/auth/session` to restore the session
|
|
301
|
+
8. Route Handler decrypts the cookie, validates expiry, fetches user profile, returns `{ user, isAuthenticated }`
|
|
279
302
|
|
|
280
|
-
|
|
281
|
-
refreshThreshold?: number;
|
|
303
|
+
### Why HttpOnly Cookies?
|
|
282
304
|
|
|
283
|
-
|
|
284
|
-
accessTokenExpiresIn?: number | string; // e.g. "2d", 3600
|
|
285
|
-
refreshTokenExpiresIn?: number | string; // e.g. "7d"
|
|
286
|
-
strategy?: "backend" | "config" | "hybrid"; // default: "hybrid"
|
|
287
|
-
};
|
|
305
|
+
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
306
|
|
|
289
|
-
|
|
307
|
+
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
308
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
onRefreshError?: (error: unknown) => void;
|
|
295
|
-
}
|
|
296
|
-
```
|
|
309
|
+
### Why Split the Config?
|
|
310
|
+
|
|
311
|
+
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
312
|
|
|
298
313
|
---
|
|
299
314
|
|
|
300
|
-
|
|
315
|
+
## API Reference
|
|
316
|
+
|
|
317
|
+
### `useAuth()`
|
|
301
318
|
|
|
302
|
-
The
|
|
319
|
+
The main hook for authentication operations.
|
|
303
320
|
|
|
304
321
|
```ts
|
|
305
322
|
const { session, login, logout, refresh, isLoading } = useAuth<User>();
|
|
@@ -307,27 +324,37 @@ const { session, login, logout, refresh, isLoading } = useAuth<User>();
|
|
|
307
324
|
|
|
308
325
|
| Property | Type | Description |
|
|
309
326
|
|-------------|-----------------------------------------|--------------------------------------------------|
|
|
310
|
-
| `session` | `AuthSession<User>` | Current
|
|
311
|
-
| `login` | `(input: LoginInput) => Promise<void>` |
|
|
312
|
-
| `logout` | `() => Promise<void>` |
|
|
313
|
-
| `refresh` | `() => Promise<void>` |
|
|
314
|
-
| `isLoading` | `boolean` | `true`
|
|
327
|
+
| `session` | `AuthSession<User>` | Current user and auth status |
|
|
328
|
+
| `login` | `(input: LoginInput) => Promise<void>` | Authenticate user, sets HttpOnly cookie |
|
|
329
|
+
| `logout` | `() => Promise<void>` | Clears session, calls backend logout if configured |
|
|
330
|
+
| `refresh` | `() => Promise<void>` | Manually refresh the access token |
|
|
331
|
+
| `isLoading` | `boolean` | `true` during initialization or login |
|
|
315
332
|
|
|
316
|
-
`LoginInput` is
|
|
333
|
+
`LoginInput` is flexible — pass any fields your backend expects:
|
|
334
|
+
|
|
335
|
+
```ts
|
|
336
|
+
await login({ email, password });
|
|
337
|
+
await login({ username, password, rememberMe: true });
|
|
338
|
+
```
|
|
317
339
|
|
|
318
340
|
---
|
|
319
341
|
|
|
320
|
-
### `useSession`
|
|
342
|
+
### `useSession()`
|
|
321
343
|
|
|
322
|
-
Read-only access to the current session. Use this in components that only
|
|
344
|
+
Read-only access to the current session. Use this in components that only display user data.
|
|
323
345
|
|
|
324
346
|
```ts
|
|
325
|
-
const { user,
|
|
347
|
+
const { user, isAuthenticated } = useSession<User>();
|
|
326
348
|
```
|
|
327
349
|
|
|
350
|
+
Returns:
|
|
351
|
+
- `user` — your user object (or `null` if not authenticated)
|
|
352
|
+
- `tokens` — always `null` on the client (tokens are HttpOnly)
|
|
353
|
+
- `isAuthenticated` — `true` if the user is logged in
|
|
354
|
+
|
|
328
355
|
---
|
|
329
356
|
|
|
330
|
-
### `useRequireAuth`
|
|
357
|
+
### `useRequireAuth(options?)`
|
|
331
358
|
|
|
332
359
|
Redirects unauthenticated users. Call it at the top of any protected client component.
|
|
333
360
|
|
|
@@ -335,45 +362,49 @@ Redirects unauthenticated users. Call it at the top of any protected client comp
|
|
|
335
362
|
useRequireAuth({ redirectTo: "/login" });
|
|
336
363
|
```
|
|
337
364
|
|
|
338
|
-
|
|
365
|
+
Options:
|
|
366
|
+
|
|
367
|
+
| Option | Type | Default | Description |
|
|
368
|
+
|---------------------|--------------|------------|--------------------------------------------------|
|
|
369
|
+
| `redirectTo` | `string` | `"/login"` | Where to send unauthenticated users |
|
|
370
|
+
| `onUnauthenticated` | `() => void` | — | Custom handler instead of redirect |
|
|
371
|
+
|
|
372
|
+
The hook waits for `isLoading` to finish before redirecting, so you won't see a flash.
|
|
373
|
+
|
|
374
|
+
Custom redirect example:
|
|
339
375
|
|
|
340
376
|
```ts
|
|
377
|
+
import { useRouter } from "next/navigation";
|
|
378
|
+
|
|
341
379
|
useRequireAuth({
|
|
342
380
|
onUnauthenticated: () => router.push("/login?from=/dashboard"),
|
|
343
381
|
});
|
|
344
382
|
```
|
|
345
383
|
|
|
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
384
|
---
|
|
354
385
|
|
|
355
386
|
### Making Authenticated API Requests
|
|
356
387
|
|
|
357
|
-
Since tokens are
|
|
388
|
+
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
389
|
|
|
359
|
-
|
|
390
|
+
**Client-side:**
|
|
360
391
|
|
|
361
392
|
```ts
|
|
362
393
|
"use client";
|
|
363
394
|
|
|
364
|
-
export default function
|
|
365
|
-
async function
|
|
395
|
+
export default function OrdersPage() {
|
|
396
|
+
async function loadOrders() {
|
|
366
397
|
// The session cookie is automatically sent with this request
|
|
367
398
|
const res = await fetch("/api/orders");
|
|
368
399
|
const data = await res.json();
|
|
369
400
|
console.log(data);
|
|
370
401
|
}
|
|
371
402
|
|
|
372
|
-
return <button onClick={
|
|
403
|
+
return <button onClick={loadOrders}>Load Orders</button>;
|
|
373
404
|
}
|
|
374
405
|
```
|
|
375
406
|
|
|
376
|
-
|
|
407
|
+
**Server-side (your API route):**
|
|
377
408
|
|
|
378
409
|
```ts
|
|
379
410
|
// app/api/orders/route.ts
|
|
@@ -381,7 +412,7 @@ import { getServerSession } from "next-token-auth/server";
|
|
|
381
412
|
import { authConfig } from "@/lib/auth";
|
|
382
413
|
import { cookies } from "next/headers";
|
|
383
414
|
|
|
384
|
-
export async function GET(
|
|
415
|
+
export async function GET() {
|
|
385
416
|
const cookieStore = await cookies();
|
|
386
417
|
const session = await getServerSession(
|
|
387
418
|
{ cookies: { get: (name) => cookieStore.get(name) } },
|
|
@@ -392,9 +423,11 @@ export async function GET(req: Request) {
|
|
|
392
423
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
393
424
|
}
|
|
394
425
|
|
|
395
|
-
//
|
|
426
|
+
// Now call your backend with the access token
|
|
396
427
|
const res = await fetch(`${authConfig.baseUrl}/orders`, {
|
|
397
|
-
headers: {
|
|
428
|
+
headers: {
|
|
429
|
+
Authorization: `Bearer ${session.tokens!.accessToken}`,
|
|
430
|
+
},
|
|
398
431
|
});
|
|
399
432
|
|
|
400
433
|
return Response.json(await res.json());
|
|
@@ -405,9 +438,44 @@ This keeps tokens secure — they never leave the server.
|
|
|
405
438
|
|
|
406
439
|
---
|
|
407
440
|
|
|
408
|
-
###
|
|
441
|
+
### `getServerSession(req, config)`
|
|
442
|
+
|
|
443
|
+
Reads and validates the session in server components and API routes.
|
|
444
|
+
|
|
445
|
+
```ts
|
|
446
|
+
// app/dashboard/page.tsx (server component)
|
|
447
|
+
import { redirect } from "next/navigation";
|
|
448
|
+
import { cookies } from "next/headers";
|
|
449
|
+
import { getServerSession } from "next-token-auth/server";
|
|
450
|
+
import { authConfig } from "@/lib/auth";
|
|
451
|
+
|
|
452
|
+
export default async function DashboardPage() {
|
|
453
|
+
const cookieStore = await cookies();
|
|
454
|
+
const session = await getServerSession(
|
|
455
|
+
{ cookies: { get: (name) => cookieStore.get(name) } },
|
|
456
|
+
authConfig
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
if (!session.isAuthenticated) {
|
|
460
|
+
redirect("/login");
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return <h1>Welcome, {session.user.name}</h1>;
|
|
464
|
+
}
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
What it does:
|
|
468
|
+
1. Reads the encrypted session cookie
|
|
469
|
+
2. Decrypts it using your `secret`
|
|
470
|
+
3. Validates token expiry
|
|
471
|
+
4. Optionally fetches the user profile from your backend
|
|
472
|
+
5. Returns `{ user, tokens, isAuthenticated }`
|
|
473
|
+
|
|
474
|
+
---
|
|
475
|
+
|
|
476
|
+
### `withAuth(config, handler, options?)`
|
|
409
477
|
|
|
410
|
-
|
|
478
|
+
Wraps App Router route handlers to require authentication.
|
|
411
479
|
|
|
412
480
|
```ts
|
|
413
481
|
// app/api/profile/route.ts
|
|
@@ -415,86 +483,109 @@ import { withAuth } from "next-token-auth/server";
|
|
|
415
483
|
import { authConfig } from "@/lib/auth";
|
|
416
484
|
|
|
417
485
|
export const GET = withAuth(authConfig, async (req, session) => {
|
|
486
|
+
// session.user is guaranteed to exist here
|
|
418
487
|
return Response.json({ user: session.user });
|
|
419
488
|
});
|
|
420
489
|
```
|
|
421
490
|
|
|
422
|
-
Unauthenticated requests are redirected to `/login` by default.
|
|
491
|
+
Unauthenticated requests are redirected to `/login` by default. Override with:
|
|
492
|
+
|
|
493
|
+
```ts
|
|
494
|
+
export const GET = withAuth(
|
|
495
|
+
authConfig,
|
|
496
|
+
async (req, session) => { /* ... */ },
|
|
497
|
+
{ redirectTo: "/sign-in" }
|
|
498
|
+
);
|
|
499
|
+
```
|
|
423
500
|
|
|
424
501
|
---
|
|
425
502
|
|
|
426
|
-
###
|
|
503
|
+
### `authMiddleware(config)`
|
|
427
504
|
|
|
428
|
-
|
|
505
|
+
Creates a Next.js middleware function for edge-level route protection.
|
|
429
506
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
507
|
+
```ts
|
|
508
|
+
// middleware.ts (project root, next to app/)
|
|
509
|
+
import { authMiddleware } from "next-token-auth/server";
|
|
510
|
+
import { authConfig } from "@/lib/auth";
|
|
433
511
|
|
|
434
|
-
|
|
512
|
+
export const middleware = authMiddleware(authConfig);
|
|
435
513
|
|
|
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
|
-
},
|
|
514
|
+
export const config = {
|
|
515
|
+
matcher: ["/login", "/register", "/dashboard*", "/profile*"],
|
|
447
516
|
};
|
|
448
517
|
```
|
|
449
518
|
|
|
519
|
+
**Next.js 16+ users:** Rename the file to `proxy.ts` and change the export:
|
|
520
|
+
|
|
450
521
|
```ts
|
|
451
|
-
//
|
|
452
|
-
|
|
453
|
-
|
|
522
|
+
// proxy.ts
|
|
523
|
+
export const proxy = authMiddleware(authConfig);
|
|
524
|
+
```
|
|
454
525
|
|
|
455
|
-
|
|
456
|
-
export const middleware = authMiddleware(authConfig);
|
|
526
|
+
---
|
|
457
527
|
|
|
458
|
-
|
|
459
|
-
export const proxy = authMiddleware(authConfig);
|
|
528
|
+
## Route Protection Explained
|
|
460
529
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
530
|
+
The middleware supports three route categories:
|
|
531
|
+
|
|
532
|
+
### 1. Public routes
|
|
533
|
+
|
|
534
|
+
Always accessible, no auth check. Example: homepage, about page.
|
|
535
|
+
|
|
536
|
+
```ts
|
|
537
|
+
routes: {
|
|
538
|
+
public: ["/", "/about", "/pricing"],
|
|
539
|
+
}
|
|
464
540
|
```
|
|
465
541
|
|
|
466
|
-
|
|
542
|
+
### 2. Protected routes
|
|
543
|
+
|
|
544
|
+
Require authentication. Unauthenticated users are redirected to `loginPath`.
|
|
467
545
|
|
|
468
546
|
```ts
|
|
469
|
-
// Using /auth/* convention
|
|
470
547
|
routes: {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
loginPath: "/auth/login",
|
|
474
|
-
redirectAuthenticatedTo: "/dashboard",
|
|
548
|
+
protected: ["/dashboard*", "/settings*"],
|
|
549
|
+
loginPath: "/login", // where to send unauthenticated users
|
|
475
550
|
}
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
Wildcard matching:
|
|
554
|
+
- `"/dashboard*"` matches `/dashboard`, `/dashboard/`, `/dashboard/settings`
|
|
555
|
+
- `"/api/admin*"` matches `/api/admin`, `/api/admin/users`
|
|
556
|
+
|
|
557
|
+
### 3. Guest-only routes
|
|
558
|
+
|
|
559
|
+
Only accessible when NOT authenticated. Authenticated users are redirected away.
|
|
560
|
+
|
|
561
|
+
Use this for login and register pages so logged-in users can't access them.
|
|
476
562
|
|
|
477
|
-
|
|
563
|
+
```ts
|
|
478
564
|
routes: {
|
|
479
|
-
guestOnly: ["/
|
|
480
|
-
|
|
481
|
-
loginPath: "/portal",
|
|
482
|
-
redirectAuthenticatedTo: "/admin",
|
|
565
|
+
guestOnly: ["/login", "/register"],
|
|
566
|
+
redirectAuthenticatedTo: "/dashboard", // where to send authenticated users
|
|
483
567
|
}
|
|
484
568
|
```
|
|
485
569
|
|
|
486
|
-
|
|
570
|
+
### Resolution order
|
|
571
|
+
|
|
572
|
+
When a request hits the middleware:
|
|
487
573
|
|
|
488
|
-
1. `guestOnly`
|
|
489
|
-
2. `public`
|
|
490
|
-
3. `protected`
|
|
574
|
+
1. Check if it's a `guestOnly` route → if authenticated, redirect to `redirectAuthenticatedTo`
|
|
575
|
+
2. Check if it's a `public` route → always allow through
|
|
576
|
+
3. Check if it's a `protected` route → if not authenticated, redirect to `loginPath`
|
|
491
577
|
|
|
492
|
-
|
|
578
|
+
### Matcher vs routes config
|
|
493
579
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
580
|
+
The `matcher` in your middleware file controls which routes Next.js runs the middleware on at all:
|
|
581
|
+
|
|
582
|
+
```ts
|
|
583
|
+
export const config = {
|
|
584
|
+
matcher: ["/login", "/dashboard*"],
|
|
585
|
+
};
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
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
589
|
|
|
499
590
|
---
|
|
500
591
|
|
|
@@ -502,9 +593,9 @@ Two things to keep in mind:
|
|
|
502
593
|
|
|
503
594
|
### How tokens are stored
|
|
504
595
|
|
|
505
|
-
Tokens are
|
|
596
|
+
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
597
|
|
|
507
|
-
|
|
598
|
+
JavaScript in the browser cannot read the cookie — only the server can decrypt it.
|
|
508
599
|
|
|
509
600
|
### Session restore on page load
|
|
510
601
|
|
|
@@ -512,67 +603,31 @@ When `AuthProvider` mounts, it calls `GET /api/auth/session`, which:
|
|
|
512
603
|
|
|
513
604
|
1. Reads the encrypted session cookie server-side
|
|
514
605
|
2. Decrypts it using your `secret`
|
|
515
|
-
3. Checks
|
|
516
|
-
4.
|
|
606
|
+
3. Checks if the refresh token is expired
|
|
607
|
+
4. Fetches the user profile from your backend (if `me` endpoint is configured)
|
|
517
608
|
5. Returns `{ user, isAuthenticated }` to the client
|
|
518
609
|
|
|
519
610
|
The client never sees the raw tokens — only the user object and auth status.
|
|
520
611
|
|
|
521
|
-
### Automatic refresh
|
|
612
|
+
### Automatic token refresh
|
|
522
613
|
|
|
523
|
-
When `autoRefresh: true`, the provider calls `POST /api/auth/refresh`
|
|
614
|
+
When `autoRefresh: true`, the provider periodically calls `POST /api/auth/refresh` (based on `refreshThreshold`, default 60 seconds before expiry).
|
|
524
615
|
|
|
616
|
+
The Route Handler:
|
|
525
617
|
1. Reads the encrypted cookie
|
|
526
618
|
2. Checks if the refresh token is still valid
|
|
527
619
|
3. Calls your backend's refresh endpoint with the refresh token
|
|
528
620
|
4. Encrypts the new tokens and updates the HttpOnly cookie
|
|
529
621
|
|
|
530
|
-
### Refresh flow
|
|
531
|
-
|
|
532
|
-
```
|
|
533
|
-
Client detects expiry → POST /api/auth/refresh → backend refresh endpoint → new encrypted cookie set
|
|
534
|
-
```
|
|
535
|
-
|
|
536
622
|
If the refresh token is expired, the session is cleared.
|
|
537
623
|
|
|
538
624
|
---
|
|
539
625
|
|
|
540
|
-
##
|
|
626
|
+
## Backend API Requirements
|
|
541
627
|
|
|
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";
|
|
628
|
+
Your backend needs to implement these endpoints:
|
|
550
629
|
|
|
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`
|
|
630
|
+
### `POST /auth/login` (or whatever you set in `endpoints.login`)
|
|
576
631
|
|
|
577
632
|
Request body: whatever fields you pass to `login()` (e.g. `{ email, password }`)
|
|
578
633
|
|
|
@@ -580,9 +635,13 @@ Response:
|
|
|
580
635
|
|
|
581
636
|
```json
|
|
582
637
|
{
|
|
583
|
-
"accessToken": "
|
|
584
|
-
"refreshToken": "
|
|
585
|
-
"user": {
|
|
638
|
+
"accessToken": "eyJhbGc...",
|
|
639
|
+
"refreshToken": "eyJhbGc...",
|
|
640
|
+
"user": {
|
|
641
|
+
"id": "123",
|
|
642
|
+
"email": "user@example.com",
|
|
643
|
+
"name": "Jane Doe"
|
|
644
|
+
},
|
|
586
645
|
|
|
587
646
|
// Optional — used by "backend" and "hybrid" expiry strategies
|
|
588
647
|
"accessTokenExpiresIn": "2d",
|
|
@@ -598,39 +657,53 @@ Response:
|
|
|
598
657
|
Request body:
|
|
599
658
|
|
|
600
659
|
```json
|
|
601
|
-
{
|
|
660
|
+
{
|
|
661
|
+
"refreshToken": "eyJhbGc..."
|
|
662
|
+
}
|
|
602
663
|
```
|
|
603
664
|
|
|
604
665
|
Response: same shape as the login response (new `accessToken` + `refreshToken`).
|
|
605
666
|
|
|
606
|
-
### `GET /auth/me`
|
|
667
|
+
### `GET /auth/me` (optional)
|
|
607
668
|
|
|
608
669
|
Returns the current user object. Called after login and on session restore if the `me` endpoint is configured.
|
|
609
670
|
|
|
610
|
-
|
|
671
|
+
Response:
|
|
672
|
+
|
|
673
|
+
```json
|
|
674
|
+
{
|
|
675
|
+
"id": "123",
|
|
676
|
+
"email": "user@example.com",
|
|
677
|
+
"name": "Jane Doe"
|
|
678
|
+
}
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
### `POST /auth/logout` (optional)
|
|
611
682
|
|
|
612
|
-
Called on logout. Failure is silently ignored —
|
|
683
|
+
Called on logout. Failure is silently ignored — the session cookie is always cleared locally regardless.
|
|
613
684
|
|
|
614
685
|
---
|
|
615
686
|
|
|
616
687
|
## Expiry Formats
|
|
617
688
|
|
|
618
|
-
The
|
|
689
|
+
The library accepts expiry values in multiple formats:
|
|
690
|
+
|
|
691
|
+
| Input | Seconds | Human-readable |
|
|
692
|
+
|---------|-----------|----------------|
|
|
693
|
+
| `900` | 900 | 15 minutes |
|
|
694
|
+
| `"15m"` | 900 | 15 minutes |
|
|
695
|
+
| `"2h"` | 7,200 | 2 hours |
|
|
696
|
+
| `"2d"` | 172,800 | 2 days |
|
|
697
|
+
| `"7d"` | 604,800 | 7 days |
|
|
698
|
+
| `"1w"` | 604,800 | 1 week |
|
|
619
699
|
|
|
620
|
-
|
|
621
|
-
|---------|-----------|
|
|
622
|
-
| `900` | 900 |
|
|
623
|
-
| `"15m"` | 900 |
|
|
624
|
-
| `"2h"` | 7 200 |
|
|
625
|
-
| `"2d"` | 172 800 |
|
|
626
|
-
| `"7d"` | 604 800 |
|
|
627
|
-
| `"1w"` | 604 800 |
|
|
700
|
+
Supported units: `s` (seconds), `m` (minutes), `h` (hours), `d` (days), `w` (weeks)
|
|
628
701
|
|
|
629
702
|
### Expiry strategies
|
|
630
703
|
|
|
631
|
-
| Strategy |
|
|
704
|
+
| Strategy | Behavior |
|
|
632
705
|
|-----------|------------------------------------------------------------------|
|
|
633
|
-
| `backend` | Use only the expiry values returned by
|
|
706
|
+
| `backend` | Use only the expiry values returned by your API |
|
|
634
707
|
| `config` | Use only the values set in `expiry` config |
|
|
635
708
|
| `hybrid` | API response first; fall back to config if not present (default) |
|
|
636
709
|
|
|
@@ -638,6 +711,79 @@ The `parseExpiry` utility accepts:
|
|
|
638
711
|
|
|
639
712
|
---
|
|
640
713
|
|
|
714
|
+
## Configuration Reference
|
|
715
|
+
|
|
716
|
+
### `ClientAuthConfig` (for `AuthProvider`)
|
|
717
|
+
|
|
718
|
+
```ts
|
|
719
|
+
interface ClientAuthConfig {
|
|
720
|
+
token?: {
|
|
721
|
+
cookieName?: string; // default: "next-token-auth.session"
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
routes?: {
|
|
725
|
+
loginPath?: string; // default: "/login"
|
|
726
|
+
redirectAuthenticatedTo?: string; // default: "/dashboard"
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
autoRefresh?: boolean; // default: false
|
|
730
|
+
refreshThreshold?: number; // seconds before expiry to refresh (default: 60)
|
|
731
|
+
|
|
732
|
+
onLogin?: (session: AuthSession) => void;
|
|
733
|
+
onLogout?: () => void;
|
|
734
|
+
}
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
### `AuthConfig` (for server-side functions)
|
|
738
|
+
|
|
739
|
+
```ts
|
|
740
|
+
interface AuthConfig<User = unknown> {
|
|
741
|
+
baseUrl: string; // Your backend API base URL
|
|
742
|
+
|
|
743
|
+
endpoints: {
|
|
744
|
+
login: string; // required
|
|
745
|
+
refresh: string; // required
|
|
746
|
+
register?: string;
|
|
747
|
+
logout?: string;
|
|
748
|
+
me?: string;
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
routes?: {
|
|
752
|
+
public: string[]; // always accessible
|
|
753
|
+
protected: string[]; // require auth
|
|
754
|
+
guestOnly?: string[]; // only when NOT authenticated
|
|
755
|
+
loginPath?: string; // default: "/login"
|
|
756
|
+
redirectAuthenticatedTo?: string; // default: "/dashboard"
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
token: {
|
|
760
|
+
storage: "cookie" | "memory";
|
|
761
|
+
cookieName?: string;
|
|
762
|
+
secure?: boolean; // default: true
|
|
763
|
+
sameSite?: "strict" | "lax" | "none"; // default: "lax"
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
secret: string; // AES-GCM encryption key
|
|
767
|
+
|
|
768
|
+
autoRefresh?: boolean;
|
|
769
|
+
refreshThreshold?: number;
|
|
770
|
+
|
|
771
|
+
expiry?: {
|
|
772
|
+
accessTokenExpiresIn?: number | string;
|
|
773
|
+
refreshTokenExpiresIn?: number | string;
|
|
774
|
+
strategy?: "backend" | "config" | "hybrid";
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
fetchFn?: typeof fetch; // custom fetch for testing
|
|
778
|
+
|
|
779
|
+
onLogin?: (session: AuthSession<User>) => void;
|
|
780
|
+
onLogout?: () => void;
|
|
781
|
+
onRefreshError?: (error: unknown) => void;
|
|
782
|
+
}
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
---
|
|
786
|
+
|
|
641
787
|
## TypeScript Types
|
|
642
788
|
|
|
643
789
|
```ts
|
|
@@ -651,7 +797,7 @@ interface AuthTokens {
|
|
|
651
797
|
accessToken: string;
|
|
652
798
|
refreshToken: string;
|
|
653
799
|
accessTokenExpiresAt: number; // Unix timestamp in ms
|
|
654
|
-
refreshTokenExpiresAt?: number;
|
|
800
|
+
refreshTokenExpiresAt?: number;
|
|
655
801
|
}
|
|
656
802
|
|
|
657
803
|
interface LoginResponse<User = unknown> {
|
|
@@ -662,34 +808,102 @@ interface LoginResponse<User = unknown> {
|
|
|
662
808
|
accessTokenExpiresIn?: number | string;
|
|
663
809
|
refreshTokenExpiresIn?: number | string;
|
|
664
810
|
}
|
|
811
|
+
```
|
|
665
812
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
813
|
+
All types are exported from `next-token-auth`.
|
|
814
|
+
|
|
815
|
+
---
|
|
816
|
+
|
|
817
|
+
## Common Patterns
|
|
818
|
+
|
|
819
|
+
### Custom user type
|
|
820
|
+
|
|
821
|
+
```ts
|
|
822
|
+
interface MyUser {
|
|
823
|
+
id: string;
|
|
824
|
+
email: string;
|
|
825
|
+
role: "admin" | "user";
|
|
676
826
|
}
|
|
677
827
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
828
|
+
const { session } = useAuth<MyUser>();
|
|
829
|
+
console.log(session.user?.role);
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
### Logout with redirect
|
|
833
|
+
|
|
834
|
+
```ts
|
|
835
|
+
const { logout } = useAuth();
|
|
836
|
+
|
|
837
|
+
async function handleLogout() {
|
|
838
|
+
await logout();
|
|
839
|
+
window.location.href = "/";
|
|
686
840
|
}
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
### Conditional rendering based on auth
|
|
844
|
+
|
|
845
|
+
```ts
|
|
846
|
+
const { isAuthenticated } = useSession();
|
|
847
|
+
|
|
848
|
+
return (
|
|
849
|
+
<nav>
|
|
850
|
+
{isAuthenticated ? (
|
|
851
|
+
<a href="/dashboard">Dashboard</a>
|
|
852
|
+
) : (
|
|
853
|
+
<a href="/login">Sign in</a>
|
|
854
|
+
)}
|
|
855
|
+
</nav>
|
|
856
|
+
);
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
### Server-side redirect in a server component
|
|
860
|
+
|
|
861
|
+
```ts
|
|
862
|
+
import { redirect } from "next/navigation";
|
|
863
|
+
import { getServerSession } from "next-token-auth/server";
|
|
864
|
+
|
|
865
|
+
export default async function AdminPage() {
|
|
866
|
+
const session = await getServerSession(req, authConfig);
|
|
867
|
+
|
|
868
|
+
if (!session.isAuthenticated) {
|
|
869
|
+
redirect("/login");
|
|
870
|
+
}
|
|
687
871
|
|
|
688
|
-
|
|
689
|
-
|
|
872
|
+
if (session.user.role !== "admin") {
|
|
873
|
+
redirect("/dashboard");
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
return <h1>Admin Panel</h1>;
|
|
877
|
+
}
|
|
690
878
|
```
|
|
691
879
|
|
|
692
|
-
|
|
880
|
+
---
|
|
881
|
+
|
|
882
|
+
## Troubleshooting
|
|
883
|
+
|
|
884
|
+
### "Cannot find module 'next-token-auth/server'"
|
|
885
|
+
|
|
886
|
+
Run `npm run build` (or `pnpm build`) to generate the `dist/` folder. The package uses subpath exports (`/server`, `/react`) which require a build step.
|
|
887
|
+
|
|
888
|
+
### Middleware always redirects to login
|
|
889
|
+
|
|
890
|
+
Check three things:
|
|
891
|
+
|
|
892
|
+
1. The `matcher` in your `middleware.ts` includes the route you're testing
|
|
893
|
+
2. The route is listed in `routes.protected` or not listed in `routes.public`
|
|
894
|
+
3. You're actually logged in — check the Application tab in DevTools for the session cookie
|
|
895
|
+
|
|
896
|
+
### "secret is undefined"
|
|
897
|
+
|
|
898
|
+
Make sure `AUTH_SECRET` is set in your `.env.local` file and you're importing `authConfig` (not `clientAuthConfig`) in your Route Handler and middleware.
|
|
899
|
+
|
|
900
|
+
### Session is lost on page reload
|
|
901
|
+
|
|
902
|
+
The session should persist via the HttpOnly cookie. If it's not:
|
|
903
|
+
|
|
904
|
+
1. Check that `cookieName` matches in both `authConfig` and `clientAuthConfig`
|
|
905
|
+
2. Verify the cookie exists in DevTools → Application → Cookies
|
|
906
|
+
3. Make sure `app/api/auth/[action]/route.ts` exists and exports `createAuthHandlers(authConfig)`
|
|
693
907
|
|
|
694
908
|
---
|
|
695
909
|
|
|
@@ -702,15 +916,14 @@ All types are exported from the root `next-token-auth` import.
|
|
|
702
916
|
|
|
703
917
|
---
|
|
704
918
|
|
|
705
|
-
## Security
|
|
919
|
+
## Security
|
|
706
920
|
|
|
707
|
-
-
|
|
708
|
-
-
|
|
921
|
+
- Tokens are stored in HttpOnly cookies — JavaScript cannot read them
|
|
922
|
+
- Cookies are AES-GCM encrypted server-side using your `secret`
|
|
709
923
|
- The `secret` never leaves the server — it's only used in Route Handlers, middleware, and `getServerSession`
|
|
710
924
|
- `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
925
|
- Cookies use `Secure` and `SameSite` flags by default for CSRF protection
|
|
713
|
-
-
|
|
926
|
+
- Use a random 32-character string for `secret` in production — never commit it
|
|
714
927
|
|
|
715
928
|
---
|
|
716
929
|
|