@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 +169 -457
- package/dist/node/client/index.cjs +64 -20
- package/dist/node/client/index.js +65 -21
- package/dist/node/index.cjs +6 -2
- package/dist/node/index.js +222 -11
- package/dist/node/server/index.cjs +37 -3
- package/dist/node/server/index.js +247 -5
- package/dist/types/client/index.d.ts +1 -1
- package/dist/types/createAuth.d.ts +81 -0
- package/dist/types/index.d.ts +1 -71
- package/dist/types/server/index.d.ts +141 -1
- package/package.json +5 -5
- package/dist/node/chunk-JNVY7OLV.js +0 -232
package/README.md
CHANGED
|
@@ -1,20 +1,6 @@
|
|
|
1
1
|
# @imtbl/auth-nextjs
|
|
2
2
|
|
|
3
|
-
Next.js App Router authentication
|
|
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
|
-
##
|
|
11
|
+
## Setup
|
|
12
|
+
|
|
13
|
+
### 1. Shared Auth Config
|
|
26
14
|
|
|
27
|
-
|
|
15
|
+
Create a single config used by all auth components:
|
|
28
16
|
|
|
29
17
|
```typescript
|
|
30
|
-
// lib/auth.ts
|
|
31
|
-
import {
|
|
18
|
+
// lib/auth-config.ts
|
|
19
|
+
import type { ImmutableAuthConfig } from "@imtbl/auth-nextjs";
|
|
32
20
|
|
|
33
|
-
const
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
-
<
|
|
84
|
+
<ImmutableAuthProvider config={authConfig}>
|
|
85
|
+
{children}
|
|
86
|
+
</ImmutableAuthProvider>
|
|
101
87
|
</body>
|
|
102
88
|
</html>
|
|
103
89
|
);
|
|
104
90
|
}
|
|
105
91
|
```
|
|
106
92
|
|
|
107
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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={
|
|
132
|
+
return <button onClick={handleFetch}>Fetch</button>;
|
|
130
133
|
}
|
|
131
134
|
```
|
|
132
135
|
|
|
133
|
-
###
|
|
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
|
-
###
|
|
150
|
+
### Server Component - SSR Data Fetching (Recommended)
|
|
152
151
|
|
|
153
152
|
```typescript
|
|
154
|
-
//
|
|
155
|
-
import {
|
|
156
|
-
import { auth } from "
|
|
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
|
-
|
|
159
|
-
|
|
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
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
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
|
-
|
|
217
|
-
|
|
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
|
-
###
|
|
212
|
+
### Middleware - Route Protection
|
|
246
213
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
214
|
+
```typescript
|
|
215
|
+
// middleware.ts
|
|
216
|
+
import { createAuthMiddleware } from "@imtbl/auth-nextjs/server";
|
|
217
|
+
import { auth } from "@/lib/auth";
|
|
251
218
|
|
|
252
|
-
|
|
219
|
+
export default createAuthMiddleware(auth, { loginUrl: "/login" });
|
|
253
220
|
|
|
254
|
-
|
|
221
|
+
export const config = {
|
|
222
|
+
matcher: ["/dashboard/:path*", "/profile/:path*"],
|
|
223
|
+
};
|
|
224
|
+
```
|
|
255
225
|
|
|
256
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
263
|
+
### Login Flows
|
|
523
264
|
|
|
524
|
-
-
|
|
525
|
-
-
|
|
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
|
-
|
|
268
|
+
Both flows redirect to your callback page, which completes the auth and navigates to `redirectTo`.
|
|
529
269
|
|
|
530
|
-
###
|
|
270
|
+
### Sandbox Environment
|
|
531
271
|
|
|
532
|
-
|
|
272
|
+
For sandbox, set `passportDomain` explicitly:
|
|
533
273
|
|
|
534
274
|
```typescript
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
###
|
|
288
|
+
### SSR Data Fetching
|
|
575
289
|
|
|
576
|
-
|
|
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
|
-
|
|
579
|
-
const { getAccessToken } = useImmutableAuth();
|
|
296
|
+
### CallbackPage Props
|
|
580
297
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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 |
|