@smittdev/next-jwt-auth 0.1.0 → 1.0.0
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 +549 -0
- package/package.json +5 -1
package/README.md
ADDED
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
# @smittdev/next-jwt-auth
|
|
2
|
+
|
|
3
|
+
Zero-config JWT authentication scaffolder for Next.js App Router, specifically designed for integrating with **3rd-party backend APIs**.
|
|
4
|
+
|
|
5
|
+
Run one command. Get a complete, production-ready auth system in your project — fully typed, fully yours.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @smittdev/next-jwt-auth init
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## When to use this vs. Auth.js
|
|
14
|
+
|
|
15
|
+
[Auth.js (NextAuth)](https://authjs.dev/) is an incredible library and the gold standard for OAuth integrations (Google, GitHub, Apple, etc.). If your Next.js application *is* your backend and you need OAuth, you should use Auth.js.
|
|
16
|
+
|
|
17
|
+
However, **if you have a separate backend (Node, Go, Python, Java, etc.) that handles authentication and issues its own JWTs**, wiring it into Next.js can be tricky. This library exists to solve that specific problem.
|
|
18
|
+
|
|
19
|
+
It bridges the gap between your Next.js frontend and your external API server by:
|
|
20
|
+
- Managing the short-lived **access token** + long-lived **refresh token** lifecycle.
|
|
21
|
+
- Silently refreshing tokens before they expire using Next.js Middleware.
|
|
22
|
+
- Automatically synchronizing the user's session between Server Components and Client Components.
|
|
23
|
+
|
|
24
|
+
Instead of fighting an opinionated session format, this library scaffolds the plumbing and gets out of your way. You implement three adapter functions (`login`, `refreshToken`, `fetchUser`) that fetch from your API, and you own the resulting session.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Philosophy
|
|
29
|
+
|
|
30
|
+
This is not an npm package you add as a dependency. It's a **code scaffolder** — like shadcn/ui, it copies a set of battle-tested TypeScript files into your project. You own the code from day one.
|
|
31
|
+
|
|
32
|
+
- No black boxes. No magic. Every line is in your codebase.
|
|
33
|
+
- Bring your own API. The library calls your adapter functions — you decide how tokens are issued and validated.
|
|
34
|
+
- No environment variables required. No secret keys managed by this library.
|
|
35
|
+
- Dual-token strategy (access + refresh) baked in.
|
|
36
|
+
- Full App Router support: Server Components, Server Actions, middleware, client hooks.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
### 1. Scaffold
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npx @smittdev/next-jwt-auth init
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
You'll be asked:
|
|
49
|
+
- Where to place the library (default: `lib/auth/` or `src/lib/auth/`)
|
|
50
|
+
- Whether to generate `middleware.ts` (or `proxy.ts` for Next.js 16+)
|
|
51
|
+
- Whether to install `zod` (the only peer dependency)
|
|
52
|
+
|
|
53
|
+
### 2. Implement your adapter and configure
|
|
54
|
+
|
|
55
|
+
Open the generated `auth.ts` at your project root. Fill in the three required adapter functions and optionally tune the configuration:
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
// auth.ts
|
|
59
|
+
import { Auth } from "@/lib/auth";
|
|
60
|
+
|
|
61
|
+
export const auth = Auth({
|
|
62
|
+
// ── Adapter (required) ───────────────────────────────────────────────────
|
|
63
|
+
// Three functions are required. They call your backend API — you decide the
|
|
64
|
+
// shape of the request and response. The library only cares about the return
|
|
65
|
+
// types: TokenPair ({ accessToken, refreshToken }) and SessionUser ({ id, email, ... }).
|
|
66
|
+
|
|
67
|
+
adapter: {
|
|
68
|
+
// Called by loginAction() with whatever credentials you pass from the client.
|
|
69
|
+
async login(credentials) {
|
|
70
|
+
const res = await fetch("https://your-api.com/auth/login", {
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers: { "Content-Type": "application/json" },
|
|
73
|
+
body: JSON.stringify(credentials),
|
|
74
|
+
});
|
|
75
|
+
if (!res.ok) throw new Error("Invalid credentials");
|
|
76
|
+
return res.json(); // must return { accessToken, refreshToken }
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
// Called automatically by middleware and fetchSession when the access token
|
|
80
|
+
// is expired or within the refresh threshold. Never called on the client.
|
|
81
|
+
//
|
|
82
|
+
// ⚠️ Race condition warning: if the user has multiple tabs open, two tabs
|
|
83
|
+
// can call refreshToken() concurrently with the same refresh token. If your
|
|
84
|
+
// backend uses rotate-on-use (single-use) refresh tokens, one request will
|
|
85
|
+
// succeed and the other will receive a 401 — invalidating the session in
|
|
86
|
+
// that tab. To handle this gracefully your backend should either:
|
|
87
|
+
// a) Accept the same refresh token within a short reuse window (~5s), or
|
|
88
|
+
// b) Return the same new token pair for duplicate in-flight requests.
|
|
89
|
+
// If you use long-lived, multi-use refresh tokens this is not an issue.
|
|
90
|
+
async refreshToken(refreshToken) {
|
|
91
|
+
const res = await fetch("https://your-api.com/auth/refresh", {
|
|
92
|
+
method: "POST",
|
|
93
|
+
headers: { "Content-Type": "application/json" },
|
|
94
|
+
body: JSON.stringify({ refreshToken }),
|
|
95
|
+
});
|
|
96
|
+
if (!res.ok) throw new Error("Session expired");
|
|
97
|
+
return res.json(); // must return { accessToken, refreshToken }
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
// Called after login and during fetchSession to populate session.user.
|
|
101
|
+
// Return whatever user fields your app needs — extend SessionUser below
|
|
102
|
+
// via module augmentation to get full type safety.
|
|
103
|
+
async fetchUser(accessToken) {
|
|
104
|
+
const res = await fetch("https://your-api.com/me", {
|
|
105
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
106
|
+
});
|
|
107
|
+
if (!res.ok) throw new Error("Failed to fetch user");
|
|
108
|
+
return res.json(); // must return { id, email, ...any extra fields }
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
// Optional — called on logout to invalidate the refresh token server-side.
|
|
112
|
+
// If omitted, logout still clears cookies locally but skips the API call.
|
|
113
|
+
// Errors here are non-fatal — cookies are cleared regardless.
|
|
114
|
+
async logout({ refreshToken }) {
|
|
115
|
+
await fetch("https://your-api.com/auth/logout", {
|
|
116
|
+
method: "POST",
|
|
117
|
+
headers: { "Content-Type": "application/json" },
|
|
118
|
+
body: JSON.stringify({ refreshToken }),
|
|
119
|
+
});
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
// ── Cookies (optional) ───────────────────────────────────────────────────
|
|
124
|
+
// All token cookies are httpOnly and never accessible to JavaScript.
|
|
125
|
+
// The `name` value is used as a base — two cookies are created:
|
|
126
|
+
// <name>.access — short-lived access token
|
|
127
|
+
// <name>.refresh — long-lived refresh token
|
|
128
|
+
|
|
129
|
+
cookies: {
|
|
130
|
+
name: "auth-session", // default: "auth-session"
|
|
131
|
+
secure: true, // default: true in production, false in development
|
|
132
|
+
sameSite: "lax", // default: "lax" — use "strict" for stricter CSRF protection
|
|
133
|
+
path: "/", // default: "/"
|
|
134
|
+
// domain: "example.com", // optional — set for cross-subdomain sharing
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
// ── Token refresh (optional) ─────────────────────────────────────────────
|
|
138
|
+
// Controls when the middleware proactively refreshes an access token before
|
|
139
|
+
// it expires. If the token has less than `refreshThresholdSeconds` remaining,
|
|
140
|
+
// the middleware calls adapter.refreshToken() and writes new cookies.
|
|
141
|
+
|
|
142
|
+
refresh: {
|
|
143
|
+
refreshThresholdSeconds: 60, // default: 60 (refresh when < 60s remain on the access token)
|
|
144
|
+
// Increase to e.g. 3600 to refresh proactively when < 1 hour remains on the access token.
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
// ── Pages (optional) ─────────────────────────────────────────────────────
|
|
148
|
+
// Redirect targets used by requireSession(), loginAction(), and logoutAction().
|
|
149
|
+
|
|
150
|
+
pages: {
|
|
151
|
+
signIn: "/login", // default: "/login" — requireSession() + logoutAction() redirect here
|
|
152
|
+
home: "/", // default: "/" — loginAction() redirects here
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
// ── Debug (optional) ─────────────────────────────────────────────────────
|
|
156
|
+
// Logs token refresh decisions, session resolution, middleware activity,
|
|
157
|
+
// and action outcomes to the server console. Keep off in production.
|
|
158
|
+
|
|
159
|
+
debug: process.env.NODE_ENV === "development",
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// ── Extending the user type (optional) ──────────────────────────────────────
|
|
163
|
+
// Declare extra fields on SessionUser via module augmentation.
|
|
164
|
+
// These fields will be typed everywhere: server helpers, useSession(), middleware.
|
|
165
|
+
|
|
166
|
+
declare module "@/lib/auth" {
|
|
167
|
+
interface SessionUser {
|
|
168
|
+
name: string;
|
|
169
|
+
role: "admin" | "user";
|
|
170
|
+
avatarUrl?: string;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### 3. Wrap your layout
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
// app/layout.tsx
|
|
179
|
+
import { auth } from "@/auth";
|
|
180
|
+
import { AuthProvider } from "@/lib/auth/client";
|
|
181
|
+
|
|
182
|
+
export default function RootLayout({ children }) {
|
|
183
|
+
return (
|
|
184
|
+
<html>
|
|
185
|
+
<body>
|
|
186
|
+
<AuthProvider actions={auth.actions}>{children}</AuthProvider>
|
|
187
|
+
</body>
|
|
188
|
+
</html>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
That's it. Auth is ready.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## CLI Commands
|
|
198
|
+
|
|
199
|
+
### `init`
|
|
200
|
+
|
|
201
|
+
Scaffolds the auth library into your project. Detects your project setup (Next.js version, TypeScript, package manager, tsconfig alias) and runs interactively.
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
npx @smittdev/next-jwt-auth init
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### `update`
|
|
208
|
+
|
|
209
|
+
Updates the library files to the latest version without touching your `auth.ts` adapter implementation. Reports added, modified, and removed files.
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
npx @smittdev/next-jwt-auth update
|
|
213
|
+
|
|
214
|
+
# Preview what would change without writing any files
|
|
215
|
+
npx @smittdev/next-jwt-auth update --dry-run
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### `check`
|
|
219
|
+
|
|
220
|
+
Validates your project setup. Runs six checks and reports pass/warn/fail for each:
|
|
221
|
+
|
|
222
|
+
1. Library directory is installed
|
|
223
|
+
2. `auth.ts` exists
|
|
224
|
+
3. Adapter functions are implemented (not stubs)
|
|
225
|
+
4. `AuthProvider` is present in the root layout
|
|
226
|
+
5. `middleware.ts` / `proxy.ts` exists and is configured correctly
|
|
227
|
+
6. Import alias in `auth.ts` matches `tsconfig.json`
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
npx @smittdev/next-jwt-auth check
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Exits with code `1` if any check fails.
|
|
234
|
+
|
|
235
|
+
### `uninstall`
|
|
236
|
+
|
|
237
|
+
Removes the scaffolded auth files from your project. Interactively asks whether to delete the library directory, `auth.ts`, and `middleware.ts` / `proxy.ts` — so you can keep whatever you want.
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
npx @smittdev/next-jwt-auth uninstall
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
> `auth.ts` defaults to **no** when prompted — it contains your adapter implementation and is skipped unless you explicitly confirm.
|
|
244
|
+
|
|
245
|
+
### `--version` / `--help`
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
npx @smittdev/next-jwt-auth --version
|
|
249
|
+
npx @smittdev/next-jwt-auth --help
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Usage
|
|
255
|
+
|
|
256
|
+
### Server Components
|
|
257
|
+
|
|
258
|
+
```tsx
|
|
259
|
+
import { auth } from "@/auth";
|
|
260
|
+
|
|
261
|
+
// Get session (returns null if unauthenticated)
|
|
262
|
+
const session = await auth.getSession();
|
|
263
|
+
|
|
264
|
+
// Require session (redirects to /login if unauthenticated)
|
|
265
|
+
const session = await auth.requireSession();
|
|
266
|
+
|
|
267
|
+
// Require session and append ?callbackUrl= to the redirect
|
|
268
|
+
const session = await auth.requireSession({ includeCallbackUrl: true });
|
|
269
|
+
|
|
270
|
+
// Individual token/user helpers
|
|
271
|
+
const user = await auth.getUser();
|
|
272
|
+
const accessToken = await auth.getAccessToken();
|
|
273
|
+
const refreshToken = await auth.getRefreshToken();
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Client Components
|
|
277
|
+
|
|
278
|
+
```tsx
|
|
279
|
+
"use client";
|
|
280
|
+
import { useSession, useAuth } from "@/lib/auth/client";
|
|
281
|
+
|
|
282
|
+
function MyComponent() {
|
|
283
|
+
const session = useSession();
|
|
284
|
+
const { login, logout, fetchSession } = useAuth();
|
|
285
|
+
|
|
286
|
+
if (session.status === "loading") return <Spinner />;
|
|
287
|
+
if (session.status === "unauthenticated") return <LoginButton />;
|
|
288
|
+
|
|
289
|
+
// session.status === "authenticated"
|
|
290
|
+
return <p>Hello, {session.user.email}</p>;
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Login Form
|
|
295
|
+
|
|
296
|
+
```tsx
|
|
297
|
+
"use client";
|
|
298
|
+
import { useAuth } from "@/lib/auth/client";
|
|
299
|
+
import { useRouter } from "next/navigation";
|
|
300
|
+
|
|
301
|
+
export function LoginForm() {
|
|
302
|
+
const { login } = useAuth();
|
|
303
|
+
const router = useRouter();
|
|
304
|
+
|
|
305
|
+
async function handleSubmit(e) {
|
|
306
|
+
e.preventDefault();
|
|
307
|
+
const result = await login({ email, password });
|
|
308
|
+
if (result.success) router.push("/dashboard");
|
|
309
|
+
else setError(result.error);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return <form onSubmit={handleSubmit}>...</form>;
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Pass `redirect: false` to handle navigation yourself instead of letting the action redirect automatically:
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
await login(credentials, { redirect: false });
|
|
320
|
+
await login(credentials, { redirectTo: "/onboarding" });
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Middleware / Route Protection
|
|
324
|
+
|
|
325
|
+
The generated `middleware.ts` (or `proxy.ts` on Next.js 16+) runs on the edge before every request. Use it for **token refresh and coarse-grained routing** — it is not a replacement for per-page auth checks.
|
|
326
|
+
|
|
327
|
+
> **Important Limitation:** The Next.js middleware *only* runs when a page navigation happens or when users explicitly refresh the page. This library will silently refresh expired tokens dynamically *during those requests*. **However**, if you have long-lived client-side pages and make API requests with `axios` or `fetch`, the middleware will NOT run for those API requests. You must handle silent refreshes for client-side API calls inside an interceptor and then call `updateSessionToken(newToken)` to sync the new token into the cookies so the rest of the app can see it.
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
// middleware.ts
|
|
331
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
332
|
+
import { auth } from "@/auth";
|
|
333
|
+
|
|
334
|
+
const resolveAuth = auth.createMiddleware();
|
|
335
|
+
|
|
336
|
+
export default async function middleware(request: NextRequest) {
|
|
337
|
+
const session = await resolveAuth(request);
|
|
338
|
+
const { pathname } = request.nextUrl;
|
|
339
|
+
|
|
340
|
+
const isProtected = auth.matchesPath(pathname, ["/dashboard/:path*", "/settings"]);
|
|
341
|
+
const isAuthPage = auth.matchesPath(pathname, ["/login", "/register"]);
|
|
342
|
+
|
|
343
|
+
// Redirect unauthenticated users away from protected routes
|
|
344
|
+
if (isProtected && !session.isAuthenticated) {
|
|
345
|
+
return session.redirect(new URL("/login", request.url));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Redirect authenticated users away from auth pages
|
|
349
|
+
if (isAuthPage && session.isAuthenticated) {
|
|
350
|
+
return session.redirect(new URL("/dashboard", request.url));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return session.response(NextResponse.next());
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export const config = {
|
|
357
|
+
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
|
|
358
|
+
};
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Then guard individual pages with `requireSession()` inside the page itself:
|
|
362
|
+
|
|
363
|
+
```tsx
|
|
364
|
+
// app/dashboard/page.tsx
|
|
365
|
+
import { auth } from "@/auth";
|
|
366
|
+
|
|
367
|
+
export default async function DashboardPage() {
|
|
368
|
+
// This calls your adapter's fetchUser — confirms the session is real and fresh
|
|
369
|
+
const session = await auth.requireSession();
|
|
370
|
+
|
|
371
|
+
return <p>Welcome, {session.user.email}</p>;
|
|
372
|
+
}
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
**Path pattern syntax:**
|
|
376
|
+
|
|
377
|
+
| Pattern | Matches |
|
|
378
|
+
|---------|---------|
|
|
379
|
+
| `/dashboard` | Exact path only |
|
|
380
|
+
| `/dashboard/:path*` | `/dashboard` and all sub-routes |
|
|
381
|
+
| `/user/:id` | `/user/123`, `/user/abc`, etc. |
|
|
382
|
+
|
|
383
|
+
### SSR Hydration
|
|
384
|
+
|
|
385
|
+
Pass `initialSession` from the server to eliminate the loading flash on first render:
|
|
386
|
+
|
|
387
|
+
```tsx
|
|
388
|
+
// app/layout.tsx
|
|
389
|
+
import { auth } from "@/auth";
|
|
390
|
+
import { AuthProvider } from "@/lib/auth/client";
|
|
391
|
+
|
|
392
|
+
export default async function RootLayout({ children }) {
|
|
393
|
+
const session = await auth.getSession();
|
|
394
|
+
|
|
395
|
+
return (
|
|
396
|
+
<html>
|
|
397
|
+
<body>
|
|
398
|
+
<AuthProvider actions={auth.actions} initialSession={session}>
|
|
399
|
+
{children}
|
|
400
|
+
</AuthProvider>
|
|
401
|
+
</body>
|
|
402
|
+
</html>
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
- `initialSession={session}` — starts as `"authenticated"` with user data (no fetch on mount)
|
|
408
|
+
- `initialSession={null}` — starts as `"unauthenticated"` immediately (server confirmed no session)
|
|
409
|
+
- `initialSession` omitted — starts as `"loading"`, fetches on mount (default behavior)
|
|
410
|
+
|
|
411
|
+
> **Static rendering:** If you want your layout to be statically rendered at build time (e.g. for a marketing site or a public-facing shell), do **not** call `auth.getSession()` in the layout and pass it to `AuthProvider`. Reading cookies in the layout forces Next.js to opt the entire route into dynamic rendering. Instead, omit `initialSession` and let the client fetch the session on mount — your layout stays static and only the parts that need auth become dynamic.
|
|
412
|
+
|
|
413
|
+
### Session Expiry Handling
|
|
414
|
+
|
|
415
|
+
Use `onSessionExpired` to react when a background revalidation discovers the session has ended:
|
|
416
|
+
|
|
417
|
+
```tsx
|
|
418
|
+
<AuthProvider
|
|
419
|
+
actions={auth.actions}
|
|
420
|
+
onSessionExpired={() => {
|
|
421
|
+
toast.error("Your session expired. Please log in again.");
|
|
422
|
+
router.push("/login");
|
|
423
|
+
}}
|
|
424
|
+
>
|
|
425
|
+
{children}
|
|
426
|
+
</AuthProvider>
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
Disable the automatic refresh-on-focus behavior if needed:
|
|
430
|
+
|
|
431
|
+
```tsx
|
|
432
|
+
<AuthProvider actions={auth.actions} refreshOnFocus={false}>
|
|
433
|
+
{children}
|
|
434
|
+
</AuthProvider>
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### Data Fetching Utilities
|
|
438
|
+
|
|
439
|
+
Run server-side data fetches that automatically receive the current session:
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
import { auth } from "@/auth";
|
|
443
|
+
|
|
444
|
+
// Run callback if session exists, return null otherwise
|
|
445
|
+
const data = await auth.withSession(async (session) => {
|
|
446
|
+
return fetchPublicFeed(session.user.id);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
// Run callback or redirect to sign-in
|
|
450
|
+
const data = await auth.withRequiredSession(async (session) => {
|
|
451
|
+
return fetchProtectedData(session.accessToken);
|
|
452
|
+
});
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
## File Structure
|
|
458
|
+
|
|
459
|
+
After running `init`, your project will have:
|
|
460
|
+
|
|
461
|
+
```
|
|
462
|
+
auth.ts ← Your adapter + config (edit this)
|
|
463
|
+
middleware.ts ← Route protection (edit this; proxy.ts on Next.js 16+)
|
|
464
|
+
lib/auth/
|
|
465
|
+
.version ← Installed CLI version (do not edit — used by `update`)
|
|
466
|
+
index.ts ← Auth() factory + all public exports
|
|
467
|
+
types.ts ← All TypeScript types
|
|
468
|
+
config.ts ← Global config singleton (internal)
|
|
469
|
+
core/
|
|
470
|
+
jwt.ts ← JWT decode + expiry utilities
|
|
471
|
+
cookies.ts ← httpOnly cookie helpers
|
|
472
|
+
config.ts ← Config builder + defaults
|
|
473
|
+
server/
|
|
474
|
+
session.ts ← getSession(), requireSession(), etc.
|
|
475
|
+
actions.ts ← Server Actions (login, logout, fetchSession)
|
|
476
|
+
fetchers.ts ← withSession(), withRequiredSession()
|
|
477
|
+
middleware/
|
|
478
|
+
auth-middleware.ts ← Middleware resolver + matchesPath()
|
|
479
|
+
client/
|
|
480
|
+
provider.tsx ← <AuthProvider>, useSession(), useAuth()
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
---
|
|
484
|
+
|
|
485
|
+
## API Reference
|
|
486
|
+
|
|
487
|
+
### `Auth(config)` — `auth.ts`
|
|
488
|
+
|
|
489
|
+
| Option | Type | Default | Description |
|
|
490
|
+
|--------|------|---------|-------------|
|
|
491
|
+
| `adapter.login` | `(credentials) => Promise<TokenPair>` | required | Authenticate and return tokens |
|
|
492
|
+
| `adapter.refreshToken` | `(token) => Promise<TokenPair>` | required | Exchange refresh token for new pair |
|
|
493
|
+
| `adapter.fetchUser` | `(token) => Promise<SessionUser>` | required | Return user data for an access token |
|
|
494
|
+
| `adapter.logout` | `(tokens) => Promise<void>` | optional | Invalidate refresh token server-side |
|
|
495
|
+
| `cookies.name` | `string` | `"auth-session"` | Cookie base name |
|
|
496
|
+
| `cookies.secure` | `boolean` | `true` in prod | Secure cookie flag |
|
|
497
|
+
| `cookies.sameSite` | `string` | `"lax"` | SameSite cookie attribute |
|
|
498
|
+
| `refresh.refreshThresholdSeconds` | `number` | `60` | Seconds before expiry to proactively refresh. Refresh triggers when this many seconds remain on the access token |
|
|
499
|
+
| `pages.signIn` | `string` | `"/login"` | Sign-in page — used by `requireSession()` and post-logout redirect |
|
|
500
|
+
| `pages.home` | `string` | `"/"` | Post-login redirect |
|
|
501
|
+
| `debug` | `boolean` | `false` | Log debug info to console |
|
|
502
|
+
|
|
503
|
+
### Server Helpers
|
|
504
|
+
|
|
505
|
+
| Function | Returns | Description |
|
|
506
|
+
|----------|---------|-------------|
|
|
507
|
+
| `auth.getSession()` | `Session \| null` | Current session or null |
|
|
508
|
+
| `auth.requireSession(opts?)` | `Session` | Session or redirect to sign-in |
|
|
509
|
+
| `auth.getUser()` | `SessionUser \| null` | Current user or null |
|
|
510
|
+
| `auth.getAccessToken()` | `string \| null` | Current access token or null |
|
|
511
|
+
| `auth.getRefreshToken()` | `string \| null` | Current refresh token or null |
|
|
512
|
+
| `auth.withSession(cb, default?)` | `TResult \| null` | Run callback if authenticated |
|
|
513
|
+
| `auth.withRequiredSession(cb)` | `TResult` | Run callback or redirect |
|
|
514
|
+
|
|
515
|
+
### Middleware
|
|
516
|
+
|
|
517
|
+
| Function | Returns | Description |
|
|
518
|
+
|----------|---------|-------------|
|
|
519
|
+
| `auth.createMiddleware()` | `(req) => Promise<AuthMiddlewareResult>` | Creates middleware resolver with auto token refresh |
|
|
520
|
+
| `auth.matchesPath(pathname, patterns)` | `boolean` | Match pathname against wildcard patterns |
|
|
521
|
+
|
|
522
|
+
`AuthMiddlewareResult` has:
|
|
523
|
+
- `isAuthenticated: boolean` — valid, non-expired access token exists in cookie
|
|
524
|
+
- `accessToken: string \| null`
|
|
525
|
+
- `refreshToken: string \| null`
|
|
526
|
+
- `response(base: NextResponse): NextResponse` — applies refreshed cookies to response
|
|
527
|
+
- `redirect(url: URL): NextResponse` — redirects and clears token cookies
|
|
528
|
+
|
|
529
|
+
### Client Hooks
|
|
530
|
+
|
|
531
|
+
| Hook | Returns | Description |
|
|
532
|
+
|------|---------|-------------|
|
|
533
|
+
| `useSession()` | `ClientSession` | Reactive session state (`"loading"` / `"authenticated"` / `"unauthenticated"`) |
|
|
534
|
+
| `useAuth()` | `{ login, logout, fetchSession, updateSessionToken }` | Auth action handlers. `fetchSession` syncs client state — silently rotates tokens if expired before returning. `updateSessionToken` allows injecting a new accessToken from outside the library (e.g. via an axios interceptor) and syncing it into the cookies. |
|
|
535
|
+
|
|
536
|
+
### `<AuthProvider>` Props
|
|
537
|
+
|
|
538
|
+
| Prop | Type | Default | Description |
|
|
539
|
+
|------|------|---------|-------------|
|
|
540
|
+
| `actions` | `AuthActions` | required | Pass `auth.actions` from your `auth.ts` |
|
|
541
|
+
| `initialSession` | `Session \| null \| undefined` | `undefined` | Server session for SSR hydration |
|
|
542
|
+
| `onSessionExpired` | `() => void` | — | Called when background revalidation finds session gone |
|
|
543
|
+
| `refreshOnFocus` | `boolean` | `true` | Revalidate session when tab regains focus |
|
|
544
|
+
|
|
545
|
+
---
|
|
546
|
+
|
|
547
|
+
## License
|
|
548
|
+
|
|
549
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smittdev/next-jwt-auth",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Zero-config JWT authentication scaffolder for Next.js App Router",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"next.js",
|
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
],
|
|
13
13
|
"author": "ss",
|
|
14
14
|
"license": "MIT",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/smitt2767/next-jwt-auth.git"
|
|
18
|
+
},
|
|
15
19
|
"engines": {
|
|
16
20
|
"node": ">=18.0.0"
|
|
17
21
|
},
|