@imtbl/auth-nextjs 2.12.5-alpha.0 → 2.12.5-alpha.10

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 CHANGED
@@ -1,20 +1,6 @@
1
1
  # @imtbl/auth-nextjs
2
2
 
3
- Next.js App Router authentication integration for Immutable SDK using Auth.js v5.
4
-
5
- This package bridges `@imtbl/auth` popup-based authentication with Auth.js session management, providing:
6
-
7
- - Server-side session storage in encrypted JWT cookies
8
- - Client-side token refresh with automatic session sync
9
- - SSR data fetching with automatic fallback when tokens are expired
10
- - React hooks for easy client-side authentication
11
- - Middleware support for protecting routes
12
-
13
- ## Requirements
14
-
15
- - Next.js 14+ with App Router
16
- - Auth.js v5 (next-auth@5.x)
17
- - React 18+
3
+ Next.js App Router authentication for Immutable SDK using Auth.js v5.
18
4
 
19
5
  ## Installation
20
6
 
@@ -22,72 +8,70 @@ This package bridges `@imtbl/auth` popup-based authentication with Auth.js sessi
22
8
  pnpm add @imtbl/auth-nextjs next-auth@beta
23
9
  ```
24
10
 
25
- ## Quick Start
11
+ ## Setup
12
+
13
+ ### 1. Shared Auth Config
26
14
 
27
- ### 1. Create Auth Configuration
15
+ Create a single config used by all auth components:
28
16
 
29
17
  ```typescript
30
- // lib/auth.ts
31
- import { createImmutableAuth } from "@imtbl/auth-nextjs";
18
+ // lib/auth-config.ts
19
+ import type { ImmutableAuthConfig } from "@imtbl/auth-nextjs";
32
20
 
33
- const config = {
21
+ export const authConfig: ImmutableAuthConfig = {
34
22
  clientId: process.env.NEXT_PUBLIC_IMMUTABLE_CLIENT_ID!,
23
+ // OAuth callback URL - where Immutable redirects after login
35
24
  redirectUri: `${process.env.NEXT_PUBLIC_BASE_URL}/callback`,
25
+ // Optional: for popup-based login (defaults to redirectUri if not set)
26
+ // popupRedirectUri: `${process.env.NEXT_PUBLIC_BASE_URL}/callback`,
36
27
  };
28
+ ```
29
+
30
+ ### 2. Server Auth (createImmutableAuth)
37
31
 
38
- export const { handlers, auth, signIn, signOut } = createImmutableAuth(config);
32
+ ```typescript
33
+ // lib/auth.ts
34
+ import { createImmutableAuth } from "@imtbl/auth-nextjs";
35
+ import { authConfig } from "./auth-config";
36
+
37
+ export const { handlers, auth } = createImmutableAuth(authConfig);
39
38
  ```
40
39
 
41
- ### 2. Set Up Auth API Route
40
+ ### 3. API Route
42
41
 
43
42
  ```typescript
44
43
  // app/api/auth/[...nextauth]/route.ts
45
44
  import { handlers } from "@/lib/auth";
46
-
47
45
  export const { GET, POST } = handlers;
48
46
  ```
49
47
 
50
- ### 3. Create Callback Page
48
+ ### 4. Callback Page
49
+
50
+ The callback page handles the OAuth redirect. The `redirectUri` in config must match this page's URL.
51
51
 
52
52
  ```typescript
53
53
  // app/callback/page.tsx
54
54
  "use client";
55
-
56
55
  import { CallbackPage } from "@imtbl/auth-nextjs/client";
57
-
58
- const config = {
59
- clientId: process.env.NEXT_PUBLIC_IMMUTABLE_CLIENT_ID!,
60
- redirectUri: `${process.env.NEXT_PUBLIC_BASE_URL}/callback`,
61
- };
56
+ import { authConfig } from "@/lib/auth-config";
62
57
 
63
58
  export default function Callback() {
64
- return <CallbackPage config={config} redirectTo="/dashboard" />;
59
+ return (
60
+ <CallbackPage
61
+ config={authConfig}
62
+ // Where to navigate AFTER auth completes (not the OAuth redirect)
63
+ redirectTo="/dashboard"
64
+ />
65
+ );
65
66
  }
66
67
  ```
67
68
 
68
- See [CallbackPage Props](#callbackpage-props) for all available options.
69
-
70
- ### 4. Add Provider to Layout
69
+ ### 5. Provider
71
70
 
72
71
  ```typescript
73
- // app/providers.tsx
74
- "use client";
75
-
76
- import { ImmutableAuthProvider } from "@imtbl/auth-nextjs/client";
77
-
78
- const config = {
79
- clientId: process.env.NEXT_PUBLIC_IMMUTABLE_CLIENT_ID!,
80
- redirectUri: `${process.env.NEXT_PUBLIC_BASE_URL}/callback`,
81
- };
82
-
83
- export function Providers({ children }: { children: React.ReactNode }) {
84
- return (
85
- <ImmutableAuthProvider config={config}>{children}</ImmutableAuthProvider>
86
- );
87
- }
88
-
89
72
  // app/layout.tsx
90
- import { Providers } from "./providers";
73
+ import { ImmutableAuthProvider } from "@imtbl/auth-nextjs/client";
74
+ import { authConfig } from "@/lib/auth-config";
91
75
 
92
76
  export default function RootLayout({
93
77
  children,
@@ -97,40 +81,59 @@ export default function RootLayout({
97
81
  return (
98
82
  <html>
99
83
  <body>
100
- <Providers>{children}</Providers>
84
+ <ImmutableAuthProvider config={authConfig}>
85
+ {children}
86
+ </ImmutableAuthProvider>
101
87
  </body>
102
88
  </html>
103
89
  );
104
90
  }
105
91
  ```
106
92
 
107
- ### 5. Use in Components
93
+ ## Usage Examples
94
+
95
+ ### Client Component - Login/Logout
108
96
 
109
97
  ```typescript
110
- // app/components/LoginButton.tsx
111
98
  "use client";
112
-
113
99
  import { useImmutableAuth } from "@imtbl/auth-nextjs/client";
114
100
 
115
101
  export function LoginButton() {
116
- const { user, isLoading, signIn, signOut } = useImmutableAuth();
102
+ const { user, isLoading, isLoggingIn, signIn, signOut } = useImmutableAuth();
117
103
 
118
104
  if (isLoading) return <div>Loading...</div>;
105
+ if (user) return <button onClick={signOut}>Logout ({user.email})</button>;
106
+
107
+ return (
108
+ <button onClick={() => signIn()} disabled={isLoggingIn}>
109
+ {isLoggingIn ? "Logging in..." : "Login"}
110
+ </button>
111
+ );
112
+ }
113
+ ```
119
114
 
120
- if (user) {
121
- return (
122
- <div>
123
- <span>Welcome, {user.email}</span>
124
- <button onClick={signOut}>Logout</button>
125
- </div>
126
- );
115
+ ### Client Component - API Calls
116
+
117
+ ```typescript
118
+ "use client";
119
+ import { useImmutableAuth } from "@imtbl/auth-nextjs/client";
120
+
121
+ export function FetchData() {
122
+ const { getAccessToken } = useImmutableAuth();
123
+
124
+ async function handleFetch() {
125
+ const token = await getAccessToken(); // Auto-refreshes if expired
126
+ const res = await fetch("/api/data", {
127
+ headers: { Authorization: `Bearer ${token}` },
128
+ });
129
+ return res.json();
127
130
  }
128
131
 
129
- return <button onClick={() => signIn()}>Login with Immutable</button>;
132
+ return <button onClick={handleFetch}>Fetch</button>;
130
133
  }
131
134
  ```
132
135
 
133
- ### 6. Access Session in Server Components
136
+ ### Server Component - Basic
134
137
 
135
138
  ```typescript
136
139
  // app/profile/page.tsx
@@ -139,137 +142,104 @@ import { redirect } from "next/navigation";
139
142
 
140
143
  export default async function ProfilePage() {
141
144
  const session = await auth();
142
-
143
- if (!session) {
144
- redirect("/login");
145
- }
146
-
145
+ if (!session) redirect("/login");
147
146
  return <h1>Welcome, {session.user.email}</h1>;
148
147
  }
149
148
  ```
150
149
 
151
- ### 7. Protect Routes with Middleware (Optional)
150
+ ### Server Component - SSR Data Fetching (Recommended)
152
151
 
153
152
  ```typescript
154
- // middleware.ts
155
- import { createAuthMiddleware } from "@imtbl/auth-nextjs/server";
156
- import { auth } from "@/lib/auth";
153
+ // lib/auth-server.ts
154
+ import { createProtectedFetchers } from "@imtbl/auth-nextjs/server";
155
+ import { auth } from "./auth";
156
+ import { redirect } from "next/navigation";
157
157
 
158
- export default createAuthMiddleware(auth, {
159
- loginUrl: "/login",
158
+ // Define auth error handling once
159
+ export const { getData } = createProtectedFetchers(auth, (error) => {
160
+ redirect(`/login?error=${encodeURIComponent(error)}`);
160
161
  });
161
-
162
- export const config = {
163
- matcher: ["/dashboard/:path*", "/profile/:path*"],
164
- };
165
162
  ```
166
163
 
167
- ## SSR Data Fetching
168
-
169
- This package provides utilities for fetching authenticated data during SSR with automatic client-side fallback when tokens are expired.
170
-
171
- ### How It Works
172
-
173
- | Token State | Server Behavior | Client Behavior |
174
- | --------------- | -------------------------------------------- | ------------------------------------- |
175
- | **Valid** | Fetches data → `{ ssr: true, data: {...} }` | Uses server data immediately |
176
- | **Expired** | Skips fetch → `{ ssr: false, data: null }` | Refreshes token, fetches client-side |
177
- | **Auth Error** | Returns `{ authError: "..." }` | Redirect to login |
178
-
179
- ### Server Component: `getAuthenticatedData`
180
-
181
164
  ```typescript
182
- // app/dashboard/page.tsx (Server Component)
183
- import { getAuthenticatedData } from "@imtbl/auth-nextjs/server";
184
- import { auth } from "@/lib/auth";
185
- import { redirect } from "next/navigation";
186
- import Dashboard from "./Dashboard";
165
+ // lib/fetchers.ts - Shared fetcher for server & client
166
+ export interface DashboardData {
167
+ stats: { views: number };
168
+ }
187
169
 
188
- // Define your data fetcher
189
- async function fetchDashboardData(token: string) {
170
+ export async function fetchDashboard(token: string): Promise<DashboardData> {
190
171
  const res = await fetch("https://api.example.com/dashboard", {
191
172
  headers: { Authorization: `Bearer ${token}` },
192
- cache: "no-store",
193
173
  });
194
- if (!res.ok) throw new Error("Failed to fetch dashboard data");
195
174
  return res.json();
196
175
  }
176
+ ```
197
177
 
198
- export default async function DashboardPage() {
199
- // Fetch data on server if token is valid, skip if expired
200
- const props = await getAuthenticatedData(auth, fetchDashboardData);
201
-
202
- // Only redirect on auth errors (e.g., refresh token invalid)
203
- if (props.authError) redirect(`/login?error=${props.authError}`);
178
+ ```typescript
179
+ // app/dashboard/page.tsx (Server Component)
180
+ import { getData } from "@/lib/auth-server";
181
+ import { fetchDashboard } from "@/lib/fetchers";
182
+ import Dashboard from "./Dashboard";
204
183
 
205
- // Pass everything to client component - it handles both SSR and CSR cases
184
+ export default async function DashboardPage() {
185
+ const props = await getData(fetchDashboard); // Auth errors redirect automatically
206
186
  return <Dashboard {...props} />;
207
187
  }
208
188
  ```
209
189
 
210
- ### Client Component: `useHydratedData`
211
-
212
190
  ```typescript
213
191
  // app/dashboard/Dashboard.tsx (Client Component)
214
192
  "use client";
193
+ import {
194
+ useHydratedData,
195
+ type ProtectedAuthPropsWithData,
196
+ } from "@imtbl/auth-nextjs/client";
197
+ import { fetchDashboard, type DashboardData } from "@/lib/fetchers";
198
+
199
+ export default function Dashboard(
200
+ props: ProtectedAuthPropsWithData<DashboardData>
201
+ ) {
202
+ // ssr=true: uses server data immediately
203
+ // ssr=false: refreshes token client-side, then fetches
204
+ const { data, isLoading, error } = useHydratedData(props, fetchDashboard);
215
205
 
216
- import { useHydratedData } from "@imtbl/auth-nextjs/client";
217
- import type { AuthPropsWithData } from "@imtbl/auth-nextjs/server";
218
-
219
- // Same fetcher as server (or a client-optimized version)
220
- async function fetchDashboardData(token: string) {
221
- const res = await fetch("/api/dashboard", {
222
- headers: { Authorization: `Bearer ${token}` },
223
- });
224
- return res.json();
225
- }
226
-
227
- interface DashboardData {
228
- items: Array<{ id: string; name: string }>;
229
- }
230
-
231
- export default function Dashboard(props: AuthPropsWithData<DashboardData>) {
232
- // When ssr=true: uses server-fetched data immediately (no loading state!)
233
- // When ssr=false: refreshes token client-side and fetches data
234
- const { data, isLoading, error, refetch } = useHydratedData(
235
- props,
236
- fetchDashboardData
237
- );
238
-
239
- if (isLoading) return <DashboardSkeleton />;
240
- if (error) return <ErrorDisplay error={error} onRetry={refetch} />;
241
- return <DashboardContent data={data!} />;
206
+ if (isLoading) return <div>Loading...</div>;
207
+ if (error) return <div>Error: {error}</div>;
208
+ return <div>Views: {data!.stats.views}</div>;
242
209
  }
243
210
  ```
244
211
 
245
- ### Benefits
212
+ ### Middleware - Route Protection
246
213
 
247
- - **Optimal UX**: When tokens are valid, data is pre-fetched on the server - no loading spinner on initial render
248
- - **Graceful Degradation**: When tokens expire, the page still loads and data is fetched client-side after token refresh
249
- - **No Race Conditions**: Token refresh only happens on the client, avoiding refresh token rotation conflicts
250
- - **Simple API**: The `useHydratedData` hook handles all the complexity automatically
214
+ ```typescript
215
+ // middleware.ts
216
+ import { createAuthMiddleware } from "@imtbl/auth-nextjs/server";
217
+ import { auth } from "@/lib/auth";
251
218
 
252
- ## Configuration Options
219
+ export default createAuthMiddleware(auth, { loginUrl: "/login" });
253
220
 
254
- The `ImmutableAuthConfig` object accepts the following properties:
221
+ export const config = {
222
+ matcher: ["/dashboard/:path*", "/profile/:path*"],
223
+ };
224
+ ```
255
225
 
256
- | Property | Type | Required | Default | Description |
257
- | ---------------------- | -------- | -------- | ------------------------------------------------ | -------------------------------------------------------------- |
258
- | `clientId` | `string` | Yes | - | Immutable OAuth client ID |
259
- | `redirectUri` | `string` | Yes | - | OAuth callback redirect URI (for redirect flow) |
260
- | `popupRedirectUri` | `string` | No | `redirectUri` | OAuth callback redirect URI for popup flow |
261
- | `logoutRedirectUri` | `string` | No | - | Where to redirect after logout |
262
- | `audience` | `string` | No | `"platform_api"` | OAuth audience |
263
- | `scope` | `string` | No | `"openid profile email offline_access transact"` | OAuth scopes |
264
- | `authenticationDomain` | `string` | No | `"https://auth.immutable.com"` | Authentication domain |
265
- | `passportDomain` | `string` | No | `"https://passport.immutable.com"` | Passport domain for transaction confirmations (see note below) |
226
+ ### Server Action - Protected
266
227
 
267
- > **Important:** The `passportDomain` must match your target environment for transaction signing to work correctly:
268
- >
269
- > - **Production:** `https://passport.immutable.com` (default)
270
- > - **Sandbox:** `https://passport.sandbox.immutable.com`
271
- >
272
- > If you're using the sandbox environment, you must explicitly set `passportDomain` to the sandbox URL.
228
+ ```typescript
229
+ // app/actions.ts
230
+ "use server";
231
+ import { withAuth } from "@imtbl/auth-nextjs/server";
232
+ import { auth } from "@/lib/auth";
233
+
234
+ export const updateProfile = withAuth(
235
+ auth,
236
+ async (session, formData: FormData) => {
237
+ // session.user, session.accessToken available
238
+ const name = formData.get("name");
239
+ // ... update logic
240
+ }
241
+ );
242
+ ```
273
243
 
274
244
  ## Environment Variables
275
245
 
@@ -277,317 +247,59 @@ The `ImmutableAuthConfig` object accepts the following properties:
277
247
  # .env.local
278
248
  NEXT_PUBLIC_IMMUTABLE_CLIENT_ID=your-client-id
279
249
  NEXT_PUBLIC_BASE_URL=http://localhost:3000
280
-
281
- # Required by Auth.js for cookie encryption
282
- AUTH_SECRET=generate-with-openssl-rand-base64-32
283
- ```
284
-
285
- Generate a secret:
286
-
287
- ```bash
288
- openssl rand -base64 32
289
- ```
290
-
291
- ## Sandbox vs Production Configuration
292
-
293
- When developing or testing, you'll typically use the **Sandbox** environment. Make sure to configure `passportDomain` correctly:
294
-
295
- ```typescript
296
- // lib/auth.ts
297
-
298
- // For SANDBOX environment
299
- const sandboxConfig = {
300
- clientId: process.env.NEXT_PUBLIC_IMMUTABLE_CLIENT_ID!,
301
- redirectUri: `${process.env.NEXT_PUBLIC_BASE_URL}/callback`,
302
- passportDomain: "https://passport.sandbox.immutable.com", // Required for sandbox!
303
- };
304
-
305
- // For PRODUCTION environment
306
- const productionConfig = {
307
- clientId: process.env.NEXT_PUBLIC_IMMUTABLE_CLIENT_ID!,
308
- redirectUri: `${process.env.NEXT_PUBLIC_BASE_URL}/callback`,
309
- // passportDomain defaults to 'https://passport.immutable.com'
310
- };
311
-
312
- // Use environment variable to switch between configs
313
- const config =
314
- process.env.NEXT_PUBLIC_IMMUTABLE_ENV === "production"
315
- ? productionConfig
316
- : sandboxConfig;
317
-
318
- export const { handlers, auth, signIn, signOut } = createImmutableAuth(config);
319
- ```
320
-
321
- > **Note:** The `passportDomain` is used for transaction confirmation popups. If not set correctly for your environment, transaction signing will not work as expected.
322
-
323
- ## API Reference
324
-
325
- ### Main Exports (`@imtbl/auth-nextjs`)
326
-
327
- | Export | Description |
328
- | --------------------------------------- | ------------------------------------------------------------------- |
329
- | `createImmutableAuth(config, options?)` | Creates Auth.js instance with `{ handlers, auth, signIn, signOut }` |
330
- | `createAuthConfig(config)` | Creates Auth.js config (for advanced use) |
331
- | `isTokenExpired(expires, buffer?)` | Utility to check if a token is expired |
332
-
333
- **Types:**
334
-
335
- | Type | Description |
336
- | ------------------------ | ----------------------------------------- |
337
- | `ImmutableAuthConfig` | Configuration options |
338
- | `ImmutableAuthOverrides` | Auth.js options override type |
339
- | `ImmutableAuthResult` | Return type of createImmutableAuth |
340
- | `ImmutableUser` | User profile type |
341
- | `ImmutableTokenData` | Token data passed to credentials provider |
342
- | `ZkEvmInfo` | zkEVM wallet information type |
343
-
344
- ### Client Exports (`@imtbl/auth-nextjs/client`)
345
-
346
- | Export | Description |
347
- | ----------------------- | ------------------------------------------------------ |
348
- | `ImmutableAuthProvider` | React context provider (wraps Auth.js SessionProvider) |
349
- | `useImmutableAuth()` | Hook for authentication state and methods (see below) |
350
- | `useAccessToken()` | Hook returning `getAccessToken` function |
351
- | `useHydratedData()` | Hook for hydrating server data with client fallback |
352
- | `CallbackPage` | Pre-built callback page component for OAuth redirects |
353
-
354
- #### CallbackPage Props
355
-
356
- | Prop | Type | Default | Description |
357
- | ------------------ | ----------------------------------------------------- | ------- | ------------------------------------------------------------------ |
358
- | `config` | `ImmutableAuthConfig` | - | Required. Immutable auth configuration |
359
- | `redirectTo` | `string \| ((user: ImmutableUser) => string \| void)` | `"/"` | Where to redirect after successful auth (supports dynamic routing) |
360
- | `loadingComponent` | `React.ReactElement \| null` | `null` | Custom loading UI while processing authentication |
361
- | `errorComponent` | `(error: string) => React.ReactElement \| null` | - | Custom error UI component |
362
- | `onSuccess` | `(user: ImmutableUser) => void \| Promise<void>` | - | Callback fired after successful authentication |
363
- | `onError` | `(error: string) => void` | - | Callback fired when authentication fails |
364
-
365
- **Example with all props:**
366
-
367
- ```tsx
368
- // app/callback/page.tsx
369
- "use client";
370
-
371
- import { CallbackPage } from "@imtbl/auth-nextjs/client";
372
- import { Spinner } from "@/components/ui/spinner";
373
-
374
- const config = {
375
- clientId: process.env.NEXT_PUBLIC_IMMUTABLE_CLIENT_ID!,
376
- redirectUri: `${process.env.NEXT_PUBLIC_BASE_URL}/callback`,
377
- };
378
-
379
- export default function Callback() {
380
- return (
381
- <CallbackPage
382
- config={config}
383
- // Dynamic redirect based on user
384
- redirectTo={(user) => {
385
- if (user.email?.endsWith("@admin.com")) return "/admin";
386
- return "/dashboard";
387
- }}
388
- // Custom loading UI
389
- loadingComponent={
390
- <div className="flex items-center justify-center min-h-screen">
391
- <Spinner />
392
- <span>Completing authentication...</span>
393
- </div>
394
- }
395
- // Custom error UI
396
- errorComponent={(error) => (
397
- <div className="text-center p-8">
398
- <h2 className="text-red-500">Authentication Error</h2>
399
- <p>{error}</p>
400
- <a href="/">Return Home</a>
401
- </div>
402
- )}
403
- // Success callback for analytics
404
- onSuccess={async (user) => {
405
- await analytics.track("login_success", { userId: user.sub });
406
- }}
407
- // Error callback for logging
408
- onError={(error) => {
409
- console.error("Auth failed:", error);
410
- Sentry.captureMessage(error);
411
- }}
412
- />
413
- );
414
- }
250
+ AUTH_SECRET=generate-with-openssl-rand-base64-32 # openssl rand -base64 32
415
251
  ```
416
252
 
417
- **`useImmutableAuth()` Return Value:**
418
-
419
- | Property | Type | Description |
420
- | ----------------- | ----------------------- | ------------------------------------------------ |
421
- | `user` | `ImmutableUser \| null` | Current user profile (null if not authenticated) |
422
- | `session` | `Session \| null` | Full Auth.js session with tokens |
423
- | `isLoading` | `boolean` | Whether authentication state is loading |
424
- | `isLoggingIn` | `boolean` | Whether a login is in progress |
425
- | `isAuthenticated` | `boolean` | Whether user is authenticated |
426
- | `signIn` | `(options?) => Promise` | Sign in with Immutable (opens popup) |
427
- | `signOut` | `() => Promise<void>` | Sign out from both Auth.js and Immutable |
428
- | `getAccessToken` | `() => Promise<string>` | Get a valid access token (refreshes if needed) |
429
- | `auth` | `Auth \| null` | The underlying Auth instance (for advanced use) |
430
-
431
- **`useHydratedData()` Return Value:**
432
-
433
- | Property | Type | Description |
434
- | ----------- | ------------------------- | ------------------------------------------ |
435
- | `data` | `T \| null` | The fetched data (server or client) |
436
- | `isLoading` | `boolean` | Whether data is being fetched client-side |
437
- | `error` | `Error \| null` | Error if fetch failed |
438
- | `refetch` | `() => Promise<void>` | Function to manually refetch data |
439
-
440
- ### Server Exports (`@imtbl/auth-nextjs/server`)
441
-
442
- | Export | Description |
443
- | ----------------------------------- | -------------------------------------------------- |
444
- | `createImmutableAuth` | Re-exported for convenience |
445
- | `getAuthProps(auth)` | Get auth props without data fetching |
446
- | `getAuthenticatedData(auth, fetch)` | Fetch data on server with automatic SSR/CSR switch |
447
- | `getValidSession(auth)` | Get session with detailed status |
448
- | `withServerAuth(auth, render, opts)`| Helper for conditional rendering based on auth |
449
- | `createAuthMiddleware(auth, opts?)` | Create middleware for protecting routes |
450
- | `withAuth(auth, handler)` | HOC for protecting Server Actions/Route Handlers |
451
-
452
- **`createAuthMiddleware` Options:**
453
-
454
- | Option | Type | Default | Description |
455
- | ---------------- | ---------------------- | ---------- | -------------------------------------- |
456
- | `loginUrl` | `string` | `"/login"` | URL to redirect when not authenticated |
457
- | `protectedPaths` | `(string \| RegExp)[]` | - | Paths that require authentication |
458
- | `publicPaths` | `(string \| RegExp)[]` | - | Paths to exclude from protection |
459
-
460
- **Types:**
461
-
462
- | Type | Description |
463
- | ------------------- | ---------------------------------------------- |
464
- | `AuthProps` | Basic auth props (session, ssr, authError) |
465
- | `AuthPropsWithData` | Auth props with pre-fetched data |
466
- | `ValidSessionResult`| Detailed session status result |
467
-
468
- ## How It Works
469
-
470
- 1. **Login**: User clicks login → `@imtbl/auth` opens popup → tokens returned
471
- 2. **Session Creation**: Tokens passed to Auth.js credentials provider → stored in encrypted JWT cookie
472
- 3. **SSR Data Fetching**: Server checks token validity → fetches data if valid, skips if expired
473
- 4. **Client Hydration**: `useHydratedData` uses server data if available, or refreshes token and fetches if SSR was skipped
474
- 5. **Token Refresh**: Only happens on client via `@imtbl/auth` → new tokens synced to NextAuth session
475
- 6. **Auto-sync**: When client refreshes tokens, they're automatically synced to the server session
476
-
477
- ## Token Refresh Architecture
478
-
479
- This package uses a **client-only token refresh** strategy to avoid race conditions with refresh token rotation:
253
+ ## Tips & Caveats
480
254
 
481
- ```
482
- ┌─────────────────────────────────────────────────────────────────┐
483
- │ Token Flow │
484
- ├─────────────────────────────────────────────────────────────────┤
485
- │ │
486
- │ [Server Request] │
487
- │ │ │
488
- │ ▼ │
489
- │ ┌─────────────┐ Valid? ┌──────────────────┐ │
490
- │ │ Check Token │──────Yes─────▶│ Fetch Data (SSR) │ │
491
- │ │ Expiry │ └──────────────────┘ │
492
- │ └─────────────┘ │
493
- │ │ │
494
- │ No (Expired) │
495
- │ │ │
496
- │ ▼ │
497
- │ ┌─────────────────┐ │
498
- │ │ Mark as Expired │ (Don't refresh on server!) │
499
- │ │ Skip SSR Fetch │ │
500
- │ └─────────────────┘ │
501
- │ │ │
502
- │ ▼ │
503
- │ [Client Hydration] │
504
- │ │ │
505
- │ ▼ │
506
- │ ┌─────────────────┐ ┌───────────────────┐ │
507
- │ │ useHydratedData │──ssr:false──▶│ getAccessToken() │ │
508
- │ └─────────────────┘ │ (triggers refresh) │ │
509
- │ └───────────────────┘ │
510
- │ │ │
511
- │ ▼ │
512
- │ ┌───────────────────┐ │
513
- │ │ Sync new tokens │ │
514
- │ │ to NextAuth │ │
515
- │ └───────────────────┘ │
516
- │ │
517
- └─────────────────────────────────────────────────────────────────┘
518
- ```
255
+ ### Redirect URIs Explained
519
256
 
520
- ### Why Client-Only Refresh?
257
+ | Config Property | Purpose |
258
+ | -------------------------------- | ------------------------------------------------------------------------------------------------------- |
259
+ | `redirectUri` | OAuth callback URL - where Immutable redirects after authentication (must match your callback page URL) |
260
+ | `popupRedirectUri` | Same as `redirectUri` but for popup login flow. Defaults to `redirectUri` if not set |
261
+ | `redirectTo` (CallbackPage prop) | Where to navigate the user AFTER authentication completes (e.g., `/dashboard`) |
521
262
 
522
- Immutable uses **refresh token rotation** - each refresh invalidates the previous refresh token. If both server and client attempt to refresh simultaneously, one will fail with an "invalid_grant" error. By keeping refresh client-only:
263
+ ### Login Flows
523
264
 
524
- - No race conditions between server and client
525
- - Refresh tokens are never exposed to server logs
526
- - Simpler architecture with predictable behavior
265
+ - **Popup (default)**: `signIn()` opens a popup window. Uses `popupRedirectUri` (or `redirectUri`)
266
+ - **Redirect**: `signIn({ useCachedSession: false, useRedirectFlow: true })` does a full page redirect
527
267
 
528
- ## Handling Token Expiration
268
+ Both flows redirect to your callback page, which completes the auth and navigates to `redirectTo`.
529
269
 
530
- ### With SSR Data Fetching (Recommended)
270
+ ### Sandbox Environment
531
271
 
532
- Use `getAuthenticatedData` + `useHydratedData` for automatic handling:
272
+ For sandbox, set `passportDomain` explicitly:
533
273
 
534
274
  ```typescript
535
- // Server: Fetches if valid, skips if expired
536
- const props = await getAuthenticatedData(auth, fetchData);
537
-
538
- // Client: Uses server data or fetches after refresh
539
- const { data, isLoading } = useHydratedData(props, fetchData);
275
+ export const authConfig: ImmutableAuthConfig = {
276
+ clientId: "...",
277
+ redirectUri: "...",
278
+ passportDomain: "https://passport.sandbox.immutable.com", // Required for sandbox
279
+ };
540
280
  ```
541
281
 
542
- ### Manual Handling
543
-
544
- For components that don't use SSR data fetching:
545
-
546
- ```typescript
547
- "use client";
548
-
549
- import { useImmutableAuth } from "@imtbl/auth-nextjs/client";
550
-
551
- export function ProtectedContent() {
552
- const { session, user, signIn, isLoading, getAccessToken } = useImmutableAuth();
553
-
554
- if (isLoading) return <div>Loading...</div>;
555
-
556
- // Handle expired tokens or errors
557
- if (session?.error) {
558
- return (
559
- <div>
560
- <p>Your session has expired. Please log in again.</p>
561
- <button onClick={() => signIn()}>Log In</button>
562
- </div>
563
- );
564
- }
565
-
566
- if (!user) {
567
- return <button onClick={() => signIn()}>Log In</button>;
568
- }
282
+ ### Token Refresh
569
283
 
570
- return <div>Welcome, {user.email}</div>;
571
- }
572
- ```
284
+ - Tokens are refreshed **client-side only** to avoid race conditions with refresh token rotation
285
+ - `getAccessToken()` automatically refreshes expired tokens
286
+ - `useHydratedData` handles SSR/CSR switching automatically - if server token is expired, it fetches client-side after refresh
573
287
 
574
- ### Using getAccessToken
288
+ ### SSR Data Fetching
575
289
 
576
- The `getAccessToken()` function automatically refreshes expired tokens:
290
+ | Token State | Server | Client |
291
+ | ----------- | -------------------------- | ------------------------ |
292
+ | Valid | Fetches data (`ssr: true`) | Uses server data |
293
+ | Expired | Skips fetch (`ssr: false`) | Refreshes token, fetches |
294
+ | Auth Error | Redirects via handler | - |
577
295
 
578
- ```typescript
579
- const { getAccessToken } = useImmutableAuth();
296
+ ### CallbackPage Props
580
297
 
581
- async function fetchData() {
582
- try {
583
- const token = await getAccessToken(); // Refreshes if needed
584
- const response = await fetch("/api/data", {
585
- headers: { Authorization: `Bearer ${token}` },
586
- });
587
- return response.json();
588
- } catch (error) {
589
- // Token refresh failed - redirect to login or show error
590
- console.error("Failed to get access token:", error);
591
- }
592
- }
593
- ```
298
+ | Prop | Description |
299
+ | ------------------ | ----------------------------------------------------------- |
300
+ | `config` | Required. Auth configuration |
301
+ | `redirectTo` | Where to redirect after auth (string or `(user) => string`) |
302
+ | `loadingComponent` | Custom loading UI |
303
+ | `errorComponent` | Custom error UI `(error) => ReactElement` |
304
+ | `onSuccess` | Callback after successful auth |
305
+ | `onError` | Callback when auth fails |