next-token-auth 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 ADDED
@@ -0,0 +1,520 @@
1
+ # next-auth-kit
2
+
3
+ A production-grade authentication library for Next.js. Handles access tokens, refresh tokens, session management, and route protection — so you don't have to wire it all up yourself.
4
+
5
+ Works with both the App Router and Pages Router. Fully typed with TypeScript.
6
+
7
+ ---
8
+
9
+ ## The Problem
10
+
11
+ Authentication in Next.js involves a lot of moving parts:
12
+
13
+ - Storing tokens securely
14
+ - Refreshing access tokens before they expire
15
+ - Keeping client and server sessions in sync
16
+ - Protecting routes on both the client and server
17
+ - Wiring up login, logout, and user fetching from scratch
18
+
19
+ Most projects end up with hundreds of lines of boilerplate before a single feature is built.
20
+
21
+ ---
22
+
23
+ ## The Solution
24
+
25
+ `next-auth-kit` 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:
26
+
27
+ - Tokens are stored in cookies or memory
28
+ - Access tokens are automatically refreshed before they expire
29
+ - Sessions are restored on page load from stored tokens
30
+ - Routes can be protected client-side with a hook or server-side with middleware
31
+ - Every API request made through the built-in fetch wrapper gets a `Bearer` token injected automatically
32
+
33
+ ---
34
+
35
+ ## Features
36
+
37
+ - `AuthProvider` — React context provider that initializes and manages auth state
38
+ - `useAuth` — login, logout, refresh, and session in one hook
39
+ - `useSession` — read-only access to the current session
40
+ - `useRequireAuth` — redirects unauthenticated users, works in App Router and Pages Router
41
+ - Token storage in cookies or in-memory
42
+ - AES-GCM encrypted session cookies (server-side)
43
+ - Automatic access token refresh on a 30-second interval (when `autoRefresh` is enabled)
44
+ - 401 → refresh → retry built into the HTTP client
45
+ - `getServerSession` — read and validate the session in server components and API routes
46
+ - `withAuth` — higher-order function to protect App Router route handlers
47
+ - `authMiddleware` — Next.js middleware factory for edge-level route protection
48
+ - Flexible expiry parsing: `"15m"`, `"2h"`, `"2d"`, `"7d"`, `"1w"`, or plain seconds
49
+ - Three expiry strategies: `backend`, `config`, `hybrid`
50
+ - Fully typed with TypeScript generics for custom user shapes
51
+
52
+ ---
53
+
54
+ ## Installation
55
+
56
+ ```bash
57
+ npm install next-auth-kit
58
+ # or
59
+ yarn add next-auth-kit
60
+ # or
61
+ pnpm add next-auth-kit
62
+ # or
63
+ bun add next-auth-kit
64
+ ```
65
+
66
+ **Peer dependencies** (already installed in any Next.js project):
67
+
68
+ ```
69
+ next >= 13
70
+ react >= 18
71
+ react-dom >= 18
72
+ ```
73
+
74
+ ---
75
+
76
+ ## Quick Start
77
+
78
+ ### 1. Create your config
79
+
80
+ ```ts
81
+ // lib/auth.ts
82
+ import type { AuthConfig } from "next-auth-kit";
83
+
84
+ interface User {
85
+ id: string;
86
+ email: string;
87
+ name: string;
88
+ }
89
+
90
+ export const authConfig: AuthConfig<User> = {
91
+ baseUrl: process.env.NEXT_PUBLIC_API_URL!,
92
+
93
+ endpoints: {
94
+ login: "/auth/login",
95
+ refresh: "/auth/refresh",
96
+ logout: "/auth/logout",
97
+ me: "/auth/me",
98
+ },
99
+
100
+ token: {
101
+ storage: "cookie",
102
+ cookieName: "myapp.session",
103
+ secure: true,
104
+ sameSite: "lax",
105
+ },
106
+
107
+ secret: process.env.AUTH_SECRET!,
108
+
109
+ autoRefresh: true,
110
+
111
+ expiry: {
112
+ accessTokenExpiresIn: "2d",
113
+ refreshTokenExpiresIn: "7d",
114
+ strategy: "hybrid",
115
+ },
116
+ };
117
+ ```
118
+
119
+ ### 2. Wrap your app with `AuthProvider`
120
+
121
+ ```tsx
122
+ // app/layout.tsx
123
+ import { AuthProvider } from "next-auth-kit/react";
124
+ import { authConfig } from "@/lib/auth";
125
+
126
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
127
+ return (
128
+ <html lang="en">
129
+ <body>
130
+ <AuthProvider config={authConfig}>{children}</AuthProvider>
131
+ </body>
132
+ </html>
133
+ );
134
+ }
135
+ ```
136
+
137
+ ### 3. Use the hooks
138
+
139
+ ```tsx
140
+ "use client";
141
+
142
+ import { useAuth } from "next-auth-kit/react";
143
+
144
+ export default function LoginPage() {
145
+ const { login, isLoading } = useAuth();
146
+
147
+ async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
148
+ e.preventDefault();
149
+ const form = new FormData(e.currentTarget);
150
+ await login({ email: form.get("email"), password: form.get("password") });
151
+ }
152
+
153
+ return (
154
+ <form onSubmit={handleSubmit}>
155
+ <input name="email" type="email" required />
156
+ <input name="password" type="password" required />
157
+ <button type="submit" disabled={isLoading}>
158
+ {isLoading ? "Signing in…" : "Sign in"}
159
+ </button>
160
+ </form>
161
+ );
162
+ }
163
+ ```
164
+
165
+ ---
166
+
167
+ ## Usage
168
+
169
+ ### `AuthProvider`
170
+
171
+ Wrap your application once at the root. It initializes the `AuthClient`, restores any existing session from stored tokens on mount, and subscribes all child hooks to session changes.
172
+
173
+ ```tsx
174
+ <AuthProvider config={authConfig}>
175
+ {children}
176
+ </AuthProvider>
177
+ ```
178
+
179
+ When `autoRefresh: true` is set, the provider checks every 30 seconds whether the access token is near expiry and refreshes it silently in the background.
180
+
181
+ #### Full config reference
182
+
183
+ ```ts
184
+ interface AuthConfig<User = unknown> {
185
+ // Base URL of your backend API
186
+ baseUrl: string;
187
+
188
+ endpoints: {
189
+ login: string; // required
190
+ refresh: string; // required
191
+ register?: string;
192
+ logout?: string;
193
+ me?: string; // fetched after login/session restore to populate user
194
+ };
195
+
196
+ routes?: {
197
+ public: string[]; // always accessible, e.g. ["/", "/login"]
198
+ protected: string[]; // require auth, supports wildcard: "/dashboard/*"
199
+ };
200
+
201
+ token: {
202
+ storage: "cookie" | "memory";
203
+ cookieName?: string; // default: "next-auth-kit.session"
204
+ secure?: boolean; // default: true
205
+ sameSite?: "strict" | "lax" | "none"; // default: "lax"
206
+ };
207
+
208
+ // Used to AES-GCM encrypt session cookies server-side
209
+ secret: string;
210
+
211
+ // Automatically refresh the access token before it expires
212
+ autoRefresh?: boolean;
213
+
214
+ // Seconds before expiry to trigger a proactive refresh (default: 60)
215
+ refreshThreshold?: number;
216
+
217
+ expiry?: {
218
+ accessTokenExpiresIn?: number | string; // e.g. "2d", 3600
219
+ refreshTokenExpiresIn?: number | string; // e.g. "7d"
220
+ strategy?: "backend" | "config" | "hybrid"; // default: "hybrid"
221
+ };
222
+
223
+ // Provide a custom fetch implementation (e.g. for testing)
224
+ fetchFn?: typeof fetch;
225
+
226
+ // Lifecycle callbacks
227
+ onLogin?: (session: AuthSession<User>) => void;
228
+ onLogout?: () => void;
229
+ onRefreshError?: (error: unknown) => void;
230
+ }
231
+ ```
232
+
233
+ ---
234
+
235
+ ### `useAuth`
236
+
237
+ The primary hook. Gives you everything you need to build auth flows.
238
+
239
+ ```ts
240
+ const { session, login, logout, refresh, isLoading } = useAuth<User>();
241
+ ```
242
+
243
+ | Property | Type | Description |
244
+ |-------------|-----------------------------------------|--------------------------------------------------|
245
+ | `session` | `AuthSession<User>` | Current auth session |
246
+ | `login` | `(input: LoginInput) => Promise<void>` | POST to your login endpoint, stores tokens |
247
+ | `logout` | `() => Promise<void>` | Clears tokens, calls logout endpoint if set |
248
+ | `refresh` | `() => Promise<void>` | Manually trigger a token refresh |
249
+ | `isLoading` | `boolean` | `true` while initializing or during login |
250
+
251
+ `LoginInput` is an open object (`{ [key: string]: unknown }`), so you can pass any fields your backend expects.
252
+
253
+ ---
254
+
255
+ ### `useSession`
256
+
257
+ Read-only access to the current session. Use this in components that only need to display user data.
258
+
259
+ ```ts
260
+ const { user, tokens, isAuthenticated } = useSession<User>();
261
+ ```
262
+
263
+ ---
264
+
265
+ ### `useRequireAuth`
266
+
267
+ Redirects unauthenticated users. Call it at the top of any protected client component.
268
+
269
+ ```ts
270
+ useRequireAuth({ redirectTo: "/login" });
271
+ ```
272
+
273
+ You can also pass a custom handler instead of a redirect path:
274
+
275
+ ```ts
276
+ useRequireAuth({
277
+ onUnauthenticated: () => router.push("/login?from=/dashboard"),
278
+ });
279
+ ```
280
+
281
+ The hook waits for `isLoading` to be `false` before acting, so it won't flash a redirect during the initial session restore.
282
+
283
+ | Option | Type | Default |
284
+ |---------------------|--------------|------------|
285
+ | `redirectTo` | `string` | `"/login"` |
286
+ | `onUnauthenticated` | `() => void` | — |
287
+
288
+ ---
289
+
290
+ ### Protecting API Routes with `withAuth`
291
+
292
+ Wrap App Router route handlers to require authentication:
293
+
294
+ ```ts
295
+ // app/api/profile/route.ts
296
+ import { withAuth } from "next-auth-kit/server";
297
+ import { authConfig } from "@/lib/auth";
298
+
299
+ export const GET = withAuth(authConfig, async (req, session) => {
300
+ return Response.json({ user: session.user });
301
+ });
302
+ ```
303
+
304
+ Unauthenticated requests are redirected to `/login` by default. Pass `{ redirectTo: "/your-path" }` as the third argument to override.
305
+
306
+ ---
307
+
308
+ ### Middleware (Edge Route Protection)
309
+
310
+ Protect entire route groups at the edge using Next.js middleware:
311
+
312
+ ```ts
313
+ // middleware.ts (project root)
314
+ import { authMiddleware } from "next-auth-kit/server";
315
+ import { authConfig } from "@/lib/auth";
316
+
317
+ export const middleware = authMiddleware(authConfig);
318
+
319
+ export const config = {
320
+ matcher: ["/dashboard/:path*", "/settings/:path*"],
321
+ };
322
+ ```
323
+
324
+ The middleware reads the encrypted session cookie, checks whether the refresh token is still valid, and redirects to `/login` if not. Routes listed in `config.routes.public` are always allowed through.
325
+
326
+ ---
327
+
328
+ ## Session and Token Handling
329
+
330
+ ### How tokens are stored
331
+
332
+ | Storage mode | Where |
333
+ |--------------|-----------------------------------------------------------------------|
334
+ | `"cookie"` | Serialized as JSON in a browser cookie with `Secure` + `SameSite` |
335
+ | `"memory"` | Held in a JavaScript variable — cleared on page refresh |
336
+
337
+ Server-side (in `getServerSession` and `authMiddleware`), the cookie value is expected to be AES-GCM encrypted using your `secret`. The `TokenManager` provides `encryptTokens` / `decryptTokens` helpers for this.
338
+
339
+ ### Session restore on page load
340
+
341
+ When `AuthProvider` mounts, it calls `client.initialize()`, which:
342
+
343
+ 1. Reads tokens from the configured storage
344
+ 2. Checks whether the access token is expired (accounting for `refreshThreshold`)
345
+ 3. If the access token is near expiry but the refresh token is still valid, it silently refreshes
346
+ 4. If a `me` endpoint is configured, it fetches the user profile to populate `session.user`
347
+ 5. Updates React state — `isLoading` flips to `false` once complete
348
+
349
+ ### Automatic refresh
350
+
351
+ When `autoRefresh: true`, the provider runs a check every 30 seconds. If the access token is within `refreshThreshold` seconds of expiry (default: 60s) and the refresh token is still valid, it calls the refresh endpoint automatically.
352
+
353
+ The HTTP client also handles 401 responses: it attempts a token refresh and retries the original request once. Multiple concurrent 401s share a single refresh request (deduplicated via a shared promise).
354
+
355
+ ### Refresh flow
356
+
357
+ ```
358
+ Request → 401 → refresh endpoint → new tokens stored → original request retried
359
+ ```
360
+
361
+ If the refresh token is expired, the user is logged out and the session is cleared.
362
+
363
+ ---
364
+
365
+ ## Server-Side Session (`getServerSession`)
366
+
367
+ Use this in App Router server components and API routes to read the session without going through the client:
368
+
369
+ ```ts
370
+ // app/dashboard/page.tsx
371
+ import { redirect } from "next/navigation";
372
+ import { cookies } from "next/headers";
373
+ import { getServerSession } from "next-auth-kit/server";
374
+ import { authConfig } from "@/lib/auth";
375
+
376
+ export default async function DashboardPage() {
377
+ const cookieStore = await cookies();
378
+
379
+ const session = await getServerSession(
380
+ { cookies: { get: (name) => cookieStore.get(name) } },
381
+ authConfig
382
+ );
383
+
384
+ if (!session.isAuthenticated) {
385
+ redirect("/login");
386
+ }
387
+
388
+ return <h1>Welcome, {session.user.name}</h1>;
389
+ }
390
+ ```
391
+
392
+ `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.
393
+
394
+ ---
395
+
396
+ ## Backend Requirements
397
+
398
+ Your API needs to implement the following contract:
399
+
400
+ ### `POST /auth/login`
401
+
402
+ Request body: whatever fields you pass to `login()` (e.g. `{ email, password }`)
403
+
404
+ Response:
405
+
406
+ ```json
407
+ {
408
+ "accessToken": "eyJ...",
409
+ "refreshToken": "eyJ...",
410
+ "user": { "id": "1", "email": "user@example.com", "name": "Jane" },
411
+
412
+ // Optional — used by "backend" and "hybrid" expiry strategies
413
+ "accessTokenExpiresIn": "2d",
414
+ "refreshTokenExpiresIn": "7d",
415
+
416
+ // Legacy field, also accepted
417
+ "expiresIn": 172800
418
+ }
419
+ ```
420
+
421
+ ### `POST /auth/refresh`
422
+
423
+ Request body:
424
+
425
+ ```json
426
+ { "refreshToken": "eyJ..." }
427
+ ```
428
+
429
+ Response: same shape as the login response (new `accessToken` + `refreshToken`).
430
+
431
+ ### `GET /auth/me` _(optional)_
432
+
433
+ Returns the current user object. Called after login and on session restore if the `me` endpoint is configured.
434
+
435
+ ### `POST /auth/logout` _(optional)_
436
+
437
+ Called on logout. Failure is silently ignored — tokens are always cleared locally regardless.
438
+
439
+ ---
440
+
441
+ ## Expiry Formats
442
+
443
+ The `parseExpiry` utility accepts:
444
+
445
+ | Input | Seconds |
446
+ |---------|-----------|
447
+ | `900` | 900 |
448
+ | `"15m"` | 900 |
449
+ | `"2h"` | 7 200 |
450
+ | `"2d"` | 172 800 |
451
+ | `"7d"` | 604 800 |
452
+ | `"1w"` | 604 800 |
453
+
454
+ ### Expiry strategies
455
+
456
+ | Strategy | Behaviour |
457
+ |-----------|------------------------------------------------------------------|
458
+ | `backend` | Use only the expiry values returned by the API |
459
+ | `config` | Use only the values set in `expiry` config |
460
+ | `hybrid` | API response first; fall back to config if not present (default) |
461
+
462
+ `hybrid` is the safest choice — it works whether or not your backend returns expiry fields.
463
+
464
+ ---
465
+
466
+ ## TypeScript Types
467
+
468
+ ```ts
469
+ interface AuthSession<User = unknown> {
470
+ user: User | null;
471
+ tokens: AuthTokens | null;
472
+ isAuthenticated: boolean;
473
+ }
474
+
475
+ interface AuthTokens {
476
+ accessToken: string;
477
+ refreshToken: string;
478
+ accessTokenExpiresAt: number; // Unix timestamp in ms
479
+ refreshTokenExpiresAt?: number; // Unix timestamp in ms
480
+ }
481
+
482
+ interface LoginResponse<User = unknown> {
483
+ user: User;
484
+ accessToken: string;
485
+ refreshToken: string;
486
+ expiresIn?: number;
487
+ accessTokenExpiresIn?: number | string;
488
+ refreshTokenExpiresIn?: number | string;
489
+ }
490
+
491
+ type ExpiryInput = number | string;
492
+ type ExpiryStrategy = "backend" | "config" | "hybrid";
493
+ ```
494
+
495
+ All types are exported from the root `next-auth-kit` import.
496
+
497
+ ---
498
+
499
+ ## Who This Is For
500
+
501
+ - Developers building Next.js apps who want auth that just works
502
+ - Teams that need a consistent auth pattern across multiple projects
503
+ - Anyone tired of writing the same token refresh logic over and over
504
+ - SaaS and MVP builders who want to ship features, not auth plumbing
505
+
506
+ ---
507
+
508
+ ## Security Notes
509
+
510
+ - Session cookies use `Secure` and `SameSite` flags by default
511
+ - Server-side cookies are AES-GCM encrypted using your `secret`
512
+ - Use a random 32-character string for `secret` in production — never commit it
513
+ - The `"memory"` storage mode keeps tokens out of cookies entirely, at the cost of losing the session on page refresh
514
+ - Refresh tokens are never exposed to JavaScript when using server-side encrypted cookies
515
+
516
+ ---
517
+
518
+ ## License
519
+
520
+ MIT