@imtbl/auth-next-server 2.12.7-alpha.1 → 2.12.7-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 +80 -62
- package/dist/node/index.cjs +101 -73
- package/dist/node/index.js +101 -73
- package/package.json +1 -1
- package/src/config.ts +123 -91
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@ Server-side utilities for Immutable authentication with Auth.js v5 (NextAuth) in
|
|
|
7
7
|
This package provides server-side authentication utilities for Next.js applications using the App Router. It integrates with Auth.js v5 to handle OAuth authentication with Immutable's identity provider.
|
|
8
8
|
|
|
9
9
|
**Key features:**
|
|
10
|
+
|
|
10
11
|
- Auth.js v5 configuration for Immutable authentication
|
|
11
12
|
- Route protection via middleware
|
|
12
13
|
- Server utilities for authenticated data fetching
|
|
@@ -40,10 +41,12 @@ Create a file to configure Immutable authentication:
|
|
|
40
41
|
import NextAuth from "next-auth";
|
|
41
42
|
import { createAuthConfig } from "@imtbl/auth-next-server";
|
|
42
43
|
|
|
43
|
-
export const { handlers, auth, signIn, signOut } = NextAuth(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
44
|
+
export const { handlers, auth, signIn, signOut } = NextAuth(
|
|
45
|
+
createAuthConfig({
|
|
46
|
+
clientId: process.env.NEXT_PUBLIC_IMMUTABLE_CLIENT_ID!,
|
|
47
|
+
redirectUri: `${process.env.NEXT_PUBLIC_BASE_URL}/callback`,
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
47
50
|
```
|
|
48
51
|
|
|
49
52
|
### 2. Set Up API Route
|
|
@@ -76,27 +79,29 @@ Creates an Auth.js v5 configuration object for Immutable authentication. You pas
|
|
|
76
79
|
import NextAuth from "next-auth";
|
|
77
80
|
import { createAuthConfig } from "@imtbl/auth-next-server";
|
|
78
81
|
|
|
79
|
-
const { handlers, auth, signIn, signOut } = NextAuth(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
82
|
+
const { handlers, auth, signIn, signOut } = NextAuth(
|
|
83
|
+
createAuthConfig({
|
|
84
|
+
// Required
|
|
85
|
+
clientId: "your-client-id",
|
|
86
|
+
redirectUri: "https://your-app.com/callback",
|
|
87
|
+
|
|
88
|
+
// Optional
|
|
89
|
+
audience: "platform_api", // Default: "platform_api"
|
|
90
|
+
scope: "openid profile email offline_access transact", // Default scope
|
|
91
|
+
authenticationDomain: "https://auth.immutable.com", // Default domain
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
89
94
|
```
|
|
90
95
|
|
|
91
96
|
#### Configuration Options
|
|
92
97
|
|
|
93
|
-
| Option
|
|
94
|
-
|
|
95
|
-
| `clientId`
|
|
96
|
-
| `redirectUri`
|
|
97
|
-
| `audience`
|
|
98
|
-
| `scope`
|
|
99
|
-
| `authenticationDomain` | `string` | No
|
|
98
|
+
| Option | Type | Required | Description |
|
|
99
|
+
| ---------------------- | -------- | -------- | ------------------------------------------------------------------------ |
|
|
100
|
+
| `clientId` | `string` | Yes | Your Immutable application client ID |
|
|
101
|
+
| `redirectUri` | `string` | Yes | OAuth redirect URI configured in Immutable Hub |
|
|
102
|
+
| `audience` | `string` | No | OAuth audience (default: `"platform_api"`) |
|
|
103
|
+
| `scope` | `string` | No | OAuth scopes (default: `"openid profile email offline_access transact"`) |
|
|
104
|
+
| `authenticationDomain` | `string` | No | Auth domain (default: `"https://auth.immutable.com"`) |
|
|
100
105
|
|
|
101
106
|
#### Extending the Configuration
|
|
102
107
|
|
|
@@ -117,19 +122,20 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
|
|
|
117
122
|
secret: process.env.AUTH_SECRET,
|
|
118
123
|
trustHost: true,
|
|
119
124
|
basePath: "/api/auth/custom",
|
|
120
|
-
|
|
125
|
+
|
|
121
126
|
// Extend callbacks (be sure to call the base callbacks first)
|
|
122
127
|
callbacks: {
|
|
123
128
|
...baseConfig.callbacks,
|
|
124
129
|
async jwt(params) {
|
|
125
130
|
// Call base jwt callback first
|
|
126
|
-
const token = await baseConfig.callbacks?.jwt?.(params) ?? params.token;
|
|
131
|
+
const token = (await baseConfig.callbacks?.jwt?.(params)) ?? params.token;
|
|
127
132
|
// Add your custom logic
|
|
128
133
|
return token;
|
|
129
134
|
},
|
|
130
135
|
async session(params) {
|
|
131
136
|
// Call base session callback first
|
|
132
|
-
const session =
|
|
137
|
+
const session =
|
|
138
|
+
(await baseConfig.callbacks?.session?.(params)) ?? params.session;
|
|
133
139
|
// Add your custom logic
|
|
134
140
|
return session;
|
|
135
141
|
},
|
|
@@ -141,18 +147,19 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
|
|
|
141
147
|
|
|
142
148
|
This package provides several utilities for handling authentication in Server Components. Choose the right one based on your needs:
|
|
143
149
|
|
|
144
|
-
| Utility
|
|
145
|
-
|
|
146
|
-
| `getAuthProps`
|
|
147
|
-
| `getAuthenticatedData`
|
|
148
|
-
| `createProtectedFetchers` | Multiple pages with same error handling
|
|
149
|
-
| `getValidSession`
|
|
150
|
+
| Utility | Use Case | Data Fetching | Error Handling |
|
|
151
|
+
| ------------------------- | ------------------------------------------------- | ------------- | ----------------- |
|
|
152
|
+
| `getAuthProps` | Pass auth state to client, fetch data client-side | No | Manual |
|
|
153
|
+
| `getAuthenticatedData` | SSR data fetching with client fallback | Yes | Manual |
|
|
154
|
+
| `createProtectedFetchers` | Multiple pages with same error handling | Optional | Centralized |
|
|
155
|
+
| `getValidSession` | Custom logic for each auth state | No | Manual (detailed) |
|
|
150
156
|
|
|
151
157
|
### `getAuthProps(auth)`
|
|
152
158
|
|
|
153
159
|
**Use case:** You want to pass authentication state to a Client Component but handle data fetching entirely on the client side. This is the simplest approach when your page doesn't need SSR data fetching.
|
|
154
160
|
|
|
155
161
|
**When to use:**
|
|
162
|
+
|
|
156
163
|
- Pages where data is fetched client-side (e.g., infinite scroll, real-time updates)
|
|
157
164
|
- Pages that show a loading skeleton while fetching
|
|
158
165
|
- When you want full control over loading states in the client
|
|
@@ -167,11 +174,11 @@ import { DashboardClient } from "./DashboardClient";
|
|
|
167
174
|
|
|
168
175
|
export default async function DashboardPage() {
|
|
169
176
|
const authProps = await getAuthProps(auth);
|
|
170
|
-
|
|
177
|
+
|
|
171
178
|
if (authProps.authError) {
|
|
172
179
|
redirect("/login");
|
|
173
180
|
}
|
|
174
|
-
|
|
181
|
+
|
|
175
182
|
// DashboardClient will fetch its own data using useImmutableSession().getUser()
|
|
176
183
|
return <DashboardClient {...authProps} />;
|
|
177
184
|
}
|
|
@@ -182,11 +189,13 @@ export default async function DashboardPage() {
|
|
|
182
189
|
**Use case:** You want to fetch data server-side for faster initial page loads (SSR), but gracefully fall back to client-side fetching when the token is expired.
|
|
183
190
|
|
|
184
191
|
**When to use:**
|
|
192
|
+
|
|
185
193
|
- Pages that benefit from SSR (SEO, faster first paint)
|
|
186
194
|
- Profile pages, settings pages, or any page showing user-specific data
|
|
187
195
|
- When you want the best of both worlds: SSR when possible, CSR as fallback
|
|
188
196
|
|
|
189
197
|
**How it works:**
|
|
198
|
+
|
|
190
199
|
1. If token is valid → fetches data server-side, returns `ssr: true`
|
|
191
200
|
2. If token is expired → skips fetch, returns `ssr: false`, client refreshes and fetches
|
|
192
201
|
3. Pair with `useHydratedData` hook on the client for seamless handling
|
|
@@ -208,11 +217,11 @@ async function fetchUserProfile(accessToken: string) {
|
|
|
208
217
|
|
|
209
218
|
export default async function ProfilePage() {
|
|
210
219
|
const result = await getAuthenticatedData(auth, fetchUserProfile);
|
|
211
|
-
|
|
220
|
+
|
|
212
221
|
if (result.authError) {
|
|
213
222
|
redirect("/login");
|
|
214
223
|
}
|
|
215
|
-
|
|
224
|
+
|
|
216
225
|
// ProfileClient uses useHydratedData() to handle both SSR data and client fallback
|
|
217
226
|
return <ProfileClient {...result} />;
|
|
218
227
|
}
|
|
@@ -223,11 +232,13 @@ export default async function ProfilePage() {
|
|
|
223
232
|
**Use case:** You have multiple protected pages and want to define auth error handling once, rather than repeating `if (authError) redirect(...)` on every page.
|
|
224
233
|
|
|
225
234
|
**When to use:**
|
|
235
|
+
|
|
226
236
|
- Apps with many protected pages sharing the same error handling logic
|
|
227
237
|
- When you want DRY (Don't Repeat Yourself) error handling
|
|
228
238
|
- Teams that want consistent auth error behavior across the app
|
|
229
239
|
|
|
230
240
|
**How it works:**
|
|
241
|
+
|
|
231
242
|
- Define error handling once in a shared file
|
|
232
243
|
- Use the returned `getAuthProps` and `getData` functions in your pages
|
|
233
244
|
- Auth errors automatically trigger your handler (no manual checking needed)
|
|
@@ -245,7 +256,7 @@ export const { getAuthProps, getData } = createProtectedFetchers(
|
|
|
245
256
|
(error) => {
|
|
246
257
|
// This runs automatically when there's an auth error (e.g., RefreshTokenError)
|
|
247
258
|
redirect(`/login?error=${error}`);
|
|
248
|
-
}
|
|
259
|
+
},
|
|
249
260
|
);
|
|
250
261
|
```
|
|
251
262
|
|
|
@@ -259,7 +270,7 @@ export default async function DashboardPage() {
|
|
|
259
270
|
const result = await getData(async (token) => {
|
|
260
271
|
return fetchDashboardData(token);
|
|
261
272
|
});
|
|
262
|
-
|
|
273
|
+
|
|
263
274
|
return <DashboardClient {...result} />;
|
|
264
275
|
}
|
|
265
276
|
```
|
|
@@ -281,6 +292,7 @@ export default async function SettingsPage() {
|
|
|
281
292
|
**Use case:** You need fine-grained control over different authentication states and want to handle each case with custom logic.
|
|
282
293
|
|
|
283
294
|
**When to use:**
|
|
295
|
+
|
|
284
296
|
- Complex pages that render completely different UI based on auth state
|
|
285
297
|
- When you need to distinguish between "token expired" vs "not authenticated"
|
|
286
298
|
- Analytics or logging that needs to track specific auth states
|
|
@@ -294,22 +306,22 @@ import { getValidSession } from "@imtbl/auth-next-server";
|
|
|
294
306
|
|
|
295
307
|
export default async function AccountPage() {
|
|
296
308
|
const result = await getValidSession(auth);
|
|
297
|
-
|
|
309
|
+
|
|
298
310
|
switch (result.status) {
|
|
299
311
|
case "authenticated":
|
|
300
312
|
// Full access - render the complete account page with SSR data
|
|
301
313
|
const userData = await fetchUserData(result.session.accessToken);
|
|
302
314
|
return <FullAccountPage user={userData} />;
|
|
303
|
-
|
|
315
|
+
|
|
304
316
|
case "token_expired":
|
|
305
317
|
// Token expired but user has session - show skeleton, let client refresh
|
|
306
318
|
// This avoids a flash of "please login" for users who are actually logged in
|
|
307
319
|
return <AccountPageSkeleton session={result.session} />;
|
|
308
|
-
|
|
320
|
+
|
|
309
321
|
case "unauthenticated":
|
|
310
322
|
// No session at all - show login prompt or redirect
|
|
311
323
|
return <LoginPrompt message="Sign in to view your account" />;
|
|
312
|
-
|
|
324
|
+
|
|
313
325
|
case "error":
|
|
314
326
|
// Auth system error (e.g., refresh token revoked) - needs re-login
|
|
315
327
|
return <AuthErrorPage error={result.error} />;
|
|
@@ -324,11 +336,13 @@ export default async function AccountPage() {
|
|
|
324
336
|
**Use case:** Protect entire sections of your app at the routing level, before pages even render. This is the most efficient way to block unauthenticated access.
|
|
325
337
|
|
|
326
338
|
**When to use:**
|
|
339
|
+
|
|
327
340
|
- You have groups of pages that all require authentication (e.g., `/dashboard/*`, `/settings/*`)
|
|
328
341
|
- You want to redirect unauthenticated users before any page code runs
|
|
329
342
|
- You need consistent protection across many routes without adding checks to each page
|
|
330
343
|
|
|
331
344
|
**When NOT to use:**
|
|
345
|
+
|
|
332
346
|
- Pages that show different content for authenticated vs unauthenticated users (use page-level checks instead)
|
|
333
347
|
- Public pages with optional authenticated features
|
|
334
348
|
|
|
@@ -352,17 +366,18 @@ export const config = {
|
|
|
352
366
|
|
|
353
367
|
#### Middleware Options
|
|
354
368
|
|
|
355
|
-
| Option
|
|
356
|
-
|
|
357
|
-
| `loginUrl`
|
|
358
|
-
| `protectedPaths` | `(string \| RegExp)[]` | Paths that require authentication
|
|
359
|
-
| `publicPaths`
|
|
369
|
+
| Option | Type | Description |
|
|
370
|
+
| ---------------- | ---------------------- | ----------------------------------------------------------- |
|
|
371
|
+
| `loginUrl` | `string` | URL to redirect unauthenticated users (default: `"/login"`) |
|
|
372
|
+
| `protectedPaths` | `(string \| RegExp)[]` | Paths that require authentication |
|
|
373
|
+
| `publicPaths` | `(string \| RegExp)[]` | Paths that skip authentication (takes precedence) |
|
|
360
374
|
|
|
361
375
|
### `withAuth(auth, handler)`
|
|
362
376
|
|
|
363
377
|
**Use case:** Protect individual API Route Handlers or Server Actions. Ensures the handler only runs for authenticated users.
|
|
364
378
|
|
|
365
379
|
**When to use:**
|
|
380
|
+
|
|
366
381
|
- API routes that should only be accessible to authenticated users
|
|
367
382
|
- Server Actions (form submissions, mutations) that require authentication
|
|
368
383
|
- When you need the session/user info inside the handler
|
|
@@ -394,11 +409,11 @@ import { auth } from "@/lib/auth";
|
|
|
394
409
|
import { withAuth } from "@imtbl/auth-next-server";
|
|
395
410
|
|
|
396
411
|
export const transferAsset = withAuth(
|
|
397
|
-
auth,
|
|
412
|
+
auth,
|
|
398
413
|
async (session, formData: FormData) => {
|
|
399
414
|
const assetId = formData.get("assetId") as string;
|
|
400
415
|
const toAddress = formData.get("toAddress") as string;
|
|
401
|
-
|
|
416
|
+
|
|
402
417
|
// Use session.user.sub to identify the sender
|
|
403
418
|
// Use session.accessToken to call Immutable APIs
|
|
404
419
|
const result = await executeTransfer({
|
|
@@ -407,9 +422,9 @@ export const transferAsset = withAuth(
|
|
|
407
422
|
assetId,
|
|
408
423
|
accessToken: session.accessToken,
|
|
409
424
|
});
|
|
410
|
-
|
|
425
|
+
|
|
411
426
|
return result;
|
|
412
|
-
}
|
|
427
|
+
},
|
|
413
428
|
);
|
|
414
429
|
```
|
|
415
430
|
|
|
@@ -420,22 +435,24 @@ The package augments the Auth.js `Session` type with Immutable-specific fields:
|
|
|
420
435
|
```typescript
|
|
421
436
|
interface Session {
|
|
422
437
|
user: {
|
|
423
|
-
sub: string;
|
|
438
|
+
sub: string; // Immutable user ID
|
|
424
439
|
email?: string;
|
|
425
440
|
nickname?: string;
|
|
426
441
|
};
|
|
427
442
|
accessToken: string;
|
|
428
443
|
refreshToken?: string;
|
|
429
|
-
idToken?: string;
|
|
444
|
+
idToken?: string; // Only present transiently after sign-in or token refresh (not stored in cookie)
|
|
430
445
|
accessTokenExpires: number;
|
|
431
446
|
zkEvm?: {
|
|
432
447
|
ethAddress: string;
|
|
433
448
|
userAdminAddress: string;
|
|
434
449
|
};
|
|
435
|
-
error?: string;
|
|
450
|
+
error?: string; // "TokenExpired" or "RefreshTokenError"
|
|
436
451
|
}
|
|
437
452
|
```
|
|
438
453
|
|
|
454
|
+
> **Note:** The `idToken` is **not** stored in the session cookie. It is stripped by a custom `jwt.encode` to keep cookie size under CDN header limits. The `idToken` is only present in the session response transiently after sign-in or token refresh. On the client, `@imtbl/auth-next-client` automatically persists it in `localStorage` so that wallet operations (via `getUser()`) can always access it. All data extracted from the idToken (`email`, `nickname`, `zkEvm`) remains in the cookie as separate fields.
|
|
455
|
+
|
|
439
456
|
## Token Refresh
|
|
440
457
|
|
|
441
458
|
### Automatic Refresh on Token Expiry
|
|
@@ -453,10 +470,10 @@ import { useImmutableSession } from "@imtbl/auth-next-client";
|
|
|
453
470
|
|
|
454
471
|
function MyComponent() {
|
|
455
472
|
const { getUser } = useImmutableSession();
|
|
456
|
-
|
|
473
|
+
|
|
457
474
|
const handleRegistration = async () => {
|
|
458
475
|
// After zkEVM registration completes...
|
|
459
|
-
|
|
476
|
+
|
|
460
477
|
// Force refresh to get updated zkEvm claims from IDP
|
|
461
478
|
const freshUser = await getUser(true);
|
|
462
479
|
console.log("Updated zkEvm:", freshUser?.zkEvm);
|
|
@@ -465,6 +482,7 @@ function MyComponent() {
|
|
|
465
482
|
```
|
|
466
483
|
|
|
467
484
|
When `forceRefresh` is triggered:
|
|
485
|
+
|
|
468
486
|
1. Client calls `update({ forceRefresh: true })` via NextAuth
|
|
469
487
|
2. The `jwt` callback detects `trigger === 'update'` with `forceRefresh: true`
|
|
470
488
|
3. Server performs a token refresh using the refresh token
|
|
@@ -476,10 +494,10 @@ When `forceRefresh` is triggered:
|
|
|
476
494
|
The package also exports utilities for manual token handling:
|
|
477
495
|
|
|
478
496
|
```typescript
|
|
479
|
-
import {
|
|
480
|
-
isTokenExpired,
|
|
481
|
-
refreshAccessToken,
|
|
482
|
-
extractZkEvmFromIdToken // Extract zkEvm claims from ID token
|
|
497
|
+
import {
|
|
498
|
+
isTokenExpired, // Check if access token is expired
|
|
499
|
+
refreshAccessToken, // Manually refresh tokens
|
|
500
|
+
extractZkEvmFromIdToken, // Extract zkEvm claims from ID token
|
|
483
501
|
} from "@imtbl/auth-next-server";
|
|
484
502
|
```
|
|
485
503
|
|
|
@@ -487,10 +505,10 @@ import {
|
|
|
487
505
|
|
|
488
506
|
The session may contain an `error` field indicating authentication issues:
|
|
489
507
|
|
|
490
|
-
| Error
|
|
491
|
-
|
|
492
|
-
| `"TokenExpired"`
|
|
493
|
-
| `"RefreshTokenError"` | Refresh token invalid/expired
|
|
508
|
+
| Error | Description | Recommended Action |
|
|
509
|
+
| --------------------- | ------------------------------------------------ | ------------------------------------------------ |
|
|
510
|
+
| `"TokenExpired"` | Access token expired, refresh token may be valid | Let client refresh via `@imtbl/auth-next-client` |
|
|
511
|
+
| `"RefreshTokenError"` | Refresh token invalid/expired | Redirect to login |
|
|
494
512
|
|
|
495
513
|
## TypeScript
|
|
496
514
|
|
package/dist/node/index.cjs
CHANGED
|
@@ -60,6 +60,7 @@ var import_next_auth = __toESM(require("next-auth"), 1);
|
|
|
60
60
|
|
|
61
61
|
// src/config.ts
|
|
62
62
|
var import_credentials = __toESM(require("next-auth/providers/credentials"), 1);
|
|
63
|
+
var import_jwt = require("next-auth/jwt");
|
|
63
64
|
|
|
64
65
|
// src/constants.ts
|
|
65
66
|
var DEFAULT_AUTH_DOMAIN = "https://auth.immutable.com";
|
|
@@ -148,6 +149,7 @@ async function refreshAccessToken(refreshToken, clientId, authDomain = DEFAULT_A
|
|
|
148
149
|
|
|
149
150
|
// src/config.ts
|
|
150
151
|
var Credentials = import_credentials.default.default || import_credentials.default;
|
|
152
|
+
var defaultJwtEncode = import_jwt.encode.default || import_jwt.encode;
|
|
151
153
|
async function validateTokens(accessToken, authDomain) {
|
|
152
154
|
try {
|
|
153
155
|
const response = await fetch(`${authDomain}/userinfo`, {
|
|
@@ -169,6 +171,22 @@ async function validateTokens(accessToken, authDomain) {
|
|
|
169
171
|
function createAuthConfig(config) {
|
|
170
172
|
const authDomain = config.authenticationDomain || DEFAULT_AUTH_DOMAIN;
|
|
171
173
|
return {
|
|
174
|
+
// Custom jwt.encode: strip idToken from the cookie to reduce size and avoid
|
|
175
|
+
// CloudFront 413 "Request Entity Too Large" errors. The idToken (~1-2 KB) is
|
|
176
|
+
// still available in session responses (after sign-in or token refresh) because
|
|
177
|
+
// the session callback runs BEFORE encode. All data extracted FROM idToken
|
|
178
|
+
// (email, nickname, zkEvm) remains in the cookie as separate fields.
|
|
179
|
+
// On the client, idToken is persisted in localStorage by @imtbl/auth-next-client.
|
|
180
|
+
jwt: {
|
|
181
|
+
async encode(params) {
|
|
182
|
+
const { token, ...rest } = params;
|
|
183
|
+
if (token) {
|
|
184
|
+
const { idToken, ...cookieToken } = token;
|
|
185
|
+
return defaultJwtEncode({ ...rest, token: cookieToken });
|
|
186
|
+
}
|
|
187
|
+
return defaultJwtEncode(params);
|
|
188
|
+
}
|
|
189
|
+
},
|
|
172
190
|
providers: [
|
|
173
191
|
Credentials({
|
|
174
192
|
id: IMMUTABLE_PROVIDER_ID,
|
|
@@ -227,22 +245,63 @@ function createAuthConfig(config) {
|
|
|
227
245
|
trigger,
|
|
228
246
|
session: sessionUpdate
|
|
229
247
|
}) {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
248
|
+
try {
|
|
249
|
+
if (user) {
|
|
250
|
+
return {
|
|
251
|
+
...token,
|
|
252
|
+
sub: user.sub,
|
|
253
|
+
email: user.email,
|
|
254
|
+
nickname: user.nickname,
|
|
255
|
+
accessToken: user.accessToken,
|
|
256
|
+
refreshToken: user.refreshToken,
|
|
257
|
+
idToken: user.idToken,
|
|
258
|
+
accessTokenExpires: user.accessTokenExpires,
|
|
259
|
+
zkEvm: user.zkEvm
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
if (trigger === "update" && sessionUpdate) {
|
|
263
|
+
const update = sessionUpdate;
|
|
264
|
+
if (update.forceRefresh && token.refreshToken) {
|
|
265
|
+
try {
|
|
266
|
+
const refreshed = await refreshAccessToken(
|
|
267
|
+
token.refreshToken,
|
|
268
|
+
config.clientId,
|
|
269
|
+
authDomain
|
|
270
|
+
);
|
|
271
|
+
const zkEvm = extractZkEvmFromIdToken(refreshed.idToken);
|
|
272
|
+
return {
|
|
273
|
+
...token,
|
|
274
|
+
accessToken: refreshed.accessToken,
|
|
275
|
+
refreshToken: refreshed.refreshToken,
|
|
276
|
+
idToken: refreshed.idToken,
|
|
277
|
+
accessTokenExpires: refreshed.accessTokenExpires,
|
|
278
|
+
zkEvm: zkEvm ?? token.zkEvm,
|
|
279
|
+
// Keep existing zkEvm if not in new token
|
|
280
|
+
error: void 0
|
|
281
|
+
};
|
|
282
|
+
} catch (error) {
|
|
283
|
+
console.error("[auth-next-server] Force refresh failed:", error);
|
|
284
|
+
return {
|
|
285
|
+
...token,
|
|
286
|
+
error: "RefreshTokenError"
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
...token,
|
|
292
|
+
...update.accessToken ? { accessToken: update.accessToken } : {},
|
|
293
|
+
...update.refreshToken ? { refreshToken: update.refreshToken } : {},
|
|
294
|
+
...update.idToken ? { idToken: update.idToken } : {},
|
|
295
|
+
...update.accessTokenExpires ? { accessTokenExpires: update.accessTokenExpires } : {},
|
|
296
|
+
...update.zkEvm ? { zkEvm: update.zkEvm } : {},
|
|
297
|
+
// Clear any stale error when valid tokens are synced from client-side
|
|
298
|
+
error: void 0
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
if (!isTokenExpired(token.accessTokenExpires)) {
|
|
302
|
+
return token;
|
|
303
|
+
}
|
|
304
|
+
if (token.refreshToken) {
|
|
246
305
|
try {
|
|
247
306
|
const refreshed = await refreshAccessToken(
|
|
248
307
|
token.refreshToken,
|
|
@@ -259,9 +318,10 @@ function createAuthConfig(config) {
|
|
|
259
318
|
zkEvm: zkEvm ?? token.zkEvm,
|
|
260
319
|
// Keep existing zkEvm if not in new token
|
|
261
320
|
error: void 0
|
|
321
|
+
// Clear any previous error
|
|
262
322
|
};
|
|
263
323
|
} catch (error) {
|
|
264
|
-
console.error("[auth-next-server]
|
|
324
|
+
console.error("[auth-next-server] Token refresh failed:", error);
|
|
265
325
|
return {
|
|
266
326
|
...token,
|
|
267
327
|
error: "RefreshTokenError"
|
|
@@ -270,67 +330,35 @@ function createAuthConfig(config) {
|
|
|
270
330
|
}
|
|
271
331
|
return {
|
|
272
332
|
...token,
|
|
273
|
-
|
|
274
|
-
...update.refreshToken ? { refreshToken: update.refreshToken } : {},
|
|
275
|
-
...update.idToken ? { idToken: update.idToken } : {},
|
|
276
|
-
...update.accessTokenExpires ? { accessTokenExpires: update.accessTokenExpires } : {},
|
|
277
|
-
...update.zkEvm ? { zkEvm: update.zkEvm } : {},
|
|
278
|
-
// Clear any stale error when valid tokens are synced from client-side
|
|
279
|
-
error: void 0
|
|
333
|
+
error: "TokenExpired"
|
|
280
334
|
};
|
|
335
|
+
} catch (error) {
|
|
336
|
+
console.error("[auth-next-server] JWT callback error:", error);
|
|
337
|
+
throw error;
|
|
281
338
|
}
|
|
282
|
-
if (!isTokenExpired(token.accessTokenExpires)) {
|
|
283
|
-
return token;
|
|
284
|
-
}
|
|
285
|
-
if (token.refreshToken) {
|
|
286
|
-
try {
|
|
287
|
-
const refreshed = await refreshAccessToken(
|
|
288
|
-
token.refreshToken,
|
|
289
|
-
config.clientId,
|
|
290
|
-
authDomain
|
|
291
|
-
);
|
|
292
|
-
const zkEvm = extractZkEvmFromIdToken(refreshed.idToken);
|
|
293
|
-
return {
|
|
294
|
-
...token,
|
|
295
|
-
accessToken: refreshed.accessToken,
|
|
296
|
-
refreshToken: refreshed.refreshToken,
|
|
297
|
-
idToken: refreshed.idToken,
|
|
298
|
-
accessTokenExpires: refreshed.accessTokenExpires,
|
|
299
|
-
zkEvm: zkEvm ?? token.zkEvm,
|
|
300
|
-
// Keep existing zkEvm if not in new token
|
|
301
|
-
error: void 0
|
|
302
|
-
// Clear any previous error
|
|
303
|
-
};
|
|
304
|
-
} catch (error) {
|
|
305
|
-
console.error("[auth-next-server] Token refresh failed:", error);
|
|
306
|
-
return {
|
|
307
|
-
...token,
|
|
308
|
-
error: "RefreshTokenError"
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
return {
|
|
313
|
-
...token,
|
|
314
|
-
error: "TokenExpired"
|
|
315
|
-
};
|
|
316
339
|
},
|
|
317
340
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
318
341
|
async session({ session, token }) {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
342
|
+
try {
|
|
343
|
+
return {
|
|
344
|
+
...session,
|
|
345
|
+
user: {
|
|
346
|
+
...session.user,
|
|
347
|
+
sub: token.sub,
|
|
348
|
+
email: token.email,
|
|
349
|
+
nickname: token.nickname
|
|
350
|
+
},
|
|
351
|
+
accessToken: token.accessToken,
|
|
352
|
+
refreshToken: token.refreshToken,
|
|
353
|
+
idToken: token.idToken,
|
|
354
|
+
accessTokenExpires: token.accessTokenExpires,
|
|
355
|
+
zkEvm: token.zkEvm,
|
|
356
|
+
...token.error && { error: token.error }
|
|
357
|
+
};
|
|
358
|
+
} catch (error) {
|
|
359
|
+
console.error("[auth-next-server] Session callback error:", error);
|
|
360
|
+
throw error;
|
|
361
|
+
}
|
|
334
362
|
}
|
|
335
363
|
},
|
|
336
364
|
session: {
|
package/dist/node/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { default as default2 } from "next-auth";
|
|
|
13
13
|
|
|
14
14
|
// src/config.ts
|
|
15
15
|
import CredentialsImport from "next-auth/providers/credentials";
|
|
16
|
+
import { encode as encodeImport } from "next-auth/jwt";
|
|
16
17
|
|
|
17
18
|
// src/constants.ts
|
|
18
19
|
var DEFAULT_AUTH_DOMAIN = "https://auth.immutable.com";
|
|
@@ -101,6 +102,7 @@ async function refreshAccessToken(refreshToken, clientId, authDomain = DEFAULT_A
|
|
|
101
102
|
|
|
102
103
|
// src/config.ts
|
|
103
104
|
var Credentials = CredentialsImport.default || CredentialsImport;
|
|
105
|
+
var defaultJwtEncode = encodeImport.default || encodeImport;
|
|
104
106
|
async function validateTokens(accessToken, authDomain) {
|
|
105
107
|
try {
|
|
106
108
|
const response = await fetch(`${authDomain}/userinfo`, {
|
|
@@ -122,6 +124,22 @@ async function validateTokens(accessToken, authDomain) {
|
|
|
122
124
|
function createAuthConfig(config) {
|
|
123
125
|
const authDomain = config.authenticationDomain || DEFAULT_AUTH_DOMAIN;
|
|
124
126
|
return {
|
|
127
|
+
// Custom jwt.encode: strip idToken from the cookie to reduce size and avoid
|
|
128
|
+
// CloudFront 413 "Request Entity Too Large" errors. The idToken (~1-2 KB) is
|
|
129
|
+
// still available in session responses (after sign-in or token refresh) because
|
|
130
|
+
// the session callback runs BEFORE encode. All data extracted FROM idToken
|
|
131
|
+
// (email, nickname, zkEvm) remains in the cookie as separate fields.
|
|
132
|
+
// On the client, idToken is persisted in localStorage by @imtbl/auth-next-client.
|
|
133
|
+
jwt: {
|
|
134
|
+
async encode(params) {
|
|
135
|
+
const { token, ...rest } = params;
|
|
136
|
+
if (token) {
|
|
137
|
+
const { idToken, ...cookieToken } = token;
|
|
138
|
+
return defaultJwtEncode({ ...rest, token: cookieToken });
|
|
139
|
+
}
|
|
140
|
+
return defaultJwtEncode(params);
|
|
141
|
+
}
|
|
142
|
+
},
|
|
125
143
|
providers: [
|
|
126
144
|
Credentials({
|
|
127
145
|
id: IMMUTABLE_PROVIDER_ID,
|
|
@@ -180,22 +198,63 @@ function createAuthConfig(config) {
|
|
|
180
198
|
trigger,
|
|
181
199
|
session: sessionUpdate
|
|
182
200
|
}) {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
201
|
+
try {
|
|
202
|
+
if (user) {
|
|
203
|
+
return {
|
|
204
|
+
...token,
|
|
205
|
+
sub: user.sub,
|
|
206
|
+
email: user.email,
|
|
207
|
+
nickname: user.nickname,
|
|
208
|
+
accessToken: user.accessToken,
|
|
209
|
+
refreshToken: user.refreshToken,
|
|
210
|
+
idToken: user.idToken,
|
|
211
|
+
accessTokenExpires: user.accessTokenExpires,
|
|
212
|
+
zkEvm: user.zkEvm
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
if (trigger === "update" && sessionUpdate) {
|
|
216
|
+
const update = sessionUpdate;
|
|
217
|
+
if (update.forceRefresh && token.refreshToken) {
|
|
218
|
+
try {
|
|
219
|
+
const refreshed = await refreshAccessToken(
|
|
220
|
+
token.refreshToken,
|
|
221
|
+
config.clientId,
|
|
222
|
+
authDomain
|
|
223
|
+
);
|
|
224
|
+
const zkEvm = extractZkEvmFromIdToken(refreshed.idToken);
|
|
225
|
+
return {
|
|
226
|
+
...token,
|
|
227
|
+
accessToken: refreshed.accessToken,
|
|
228
|
+
refreshToken: refreshed.refreshToken,
|
|
229
|
+
idToken: refreshed.idToken,
|
|
230
|
+
accessTokenExpires: refreshed.accessTokenExpires,
|
|
231
|
+
zkEvm: zkEvm ?? token.zkEvm,
|
|
232
|
+
// Keep existing zkEvm if not in new token
|
|
233
|
+
error: void 0
|
|
234
|
+
};
|
|
235
|
+
} catch (error) {
|
|
236
|
+
console.error("[auth-next-server] Force refresh failed:", error);
|
|
237
|
+
return {
|
|
238
|
+
...token,
|
|
239
|
+
error: "RefreshTokenError"
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
...token,
|
|
245
|
+
...update.accessToken ? { accessToken: update.accessToken } : {},
|
|
246
|
+
...update.refreshToken ? { refreshToken: update.refreshToken } : {},
|
|
247
|
+
...update.idToken ? { idToken: update.idToken } : {},
|
|
248
|
+
...update.accessTokenExpires ? { accessTokenExpires: update.accessTokenExpires } : {},
|
|
249
|
+
...update.zkEvm ? { zkEvm: update.zkEvm } : {},
|
|
250
|
+
// Clear any stale error when valid tokens are synced from client-side
|
|
251
|
+
error: void 0
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
if (!isTokenExpired(token.accessTokenExpires)) {
|
|
255
|
+
return token;
|
|
256
|
+
}
|
|
257
|
+
if (token.refreshToken) {
|
|
199
258
|
try {
|
|
200
259
|
const refreshed = await refreshAccessToken(
|
|
201
260
|
token.refreshToken,
|
|
@@ -212,9 +271,10 @@ function createAuthConfig(config) {
|
|
|
212
271
|
zkEvm: zkEvm ?? token.zkEvm,
|
|
213
272
|
// Keep existing zkEvm if not in new token
|
|
214
273
|
error: void 0
|
|
274
|
+
// Clear any previous error
|
|
215
275
|
};
|
|
216
276
|
} catch (error) {
|
|
217
|
-
console.error("[auth-next-server]
|
|
277
|
+
console.error("[auth-next-server] Token refresh failed:", error);
|
|
218
278
|
return {
|
|
219
279
|
...token,
|
|
220
280
|
error: "RefreshTokenError"
|
|
@@ -223,67 +283,35 @@ function createAuthConfig(config) {
|
|
|
223
283
|
}
|
|
224
284
|
return {
|
|
225
285
|
...token,
|
|
226
|
-
|
|
227
|
-
...update.refreshToken ? { refreshToken: update.refreshToken } : {},
|
|
228
|
-
...update.idToken ? { idToken: update.idToken } : {},
|
|
229
|
-
...update.accessTokenExpires ? { accessTokenExpires: update.accessTokenExpires } : {},
|
|
230
|
-
...update.zkEvm ? { zkEvm: update.zkEvm } : {},
|
|
231
|
-
// Clear any stale error when valid tokens are synced from client-side
|
|
232
|
-
error: void 0
|
|
286
|
+
error: "TokenExpired"
|
|
233
287
|
};
|
|
288
|
+
} catch (error) {
|
|
289
|
+
console.error("[auth-next-server] JWT callback error:", error);
|
|
290
|
+
throw error;
|
|
234
291
|
}
|
|
235
|
-
if (!isTokenExpired(token.accessTokenExpires)) {
|
|
236
|
-
return token;
|
|
237
|
-
}
|
|
238
|
-
if (token.refreshToken) {
|
|
239
|
-
try {
|
|
240
|
-
const refreshed = await refreshAccessToken(
|
|
241
|
-
token.refreshToken,
|
|
242
|
-
config.clientId,
|
|
243
|
-
authDomain
|
|
244
|
-
);
|
|
245
|
-
const zkEvm = extractZkEvmFromIdToken(refreshed.idToken);
|
|
246
|
-
return {
|
|
247
|
-
...token,
|
|
248
|
-
accessToken: refreshed.accessToken,
|
|
249
|
-
refreshToken: refreshed.refreshToken,
|
|
250
|
-
idToken: refreshed.idToken,
|
|
251
|
-
accessTokenExpires: refreshed.accessTokenExpires,
|
|
252
|
-
zkEvm: zkEvm ?? token.zkEvm,
|
|
253
|
-
// Keep existing zkEvm if not in new token
|
|
254
|
-
error: void 0
|
|
255
|
-
// Clear any previous error
|
|
256
|
-
};
|
|
257
|
-
} catch (error) {
|
|
258
|
-
console.error("[auth-next-server] Token refresh failed:", error);
|
|
259
|
-
return {
|
|
260
|
-
...token,
|
|
261
|
-
error: "RefreshTokenError"
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
return {
|
|
266
|
-
...token,
|
|
267
|
-
error: "TokenExpired"
|
|
268
|
-
};
|
|
269
292
|
},
|
|
270
293
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
271
294
|
async session({ session, token }) {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
295
|
+
try {
|
|
296
|
+
return {
|
|
297
|
+
...session,
|
|
298
|
+
user: {
|
|
299
|
+
...session.user,
|
|
300
|
+
sub: token.sub,
|
|
301
|
+
email: token.email,
|
|
302
|
+
nickname: token.nickname
|
|
303
|
+
},
|
|
304
|
+
accessToken: token.accessToken,
|
|
305
|
+
refreshToken: token.refreshToken,
|
|
306
|
+
idToken: token.idToken,
|
|
307
|
+
accessTokenExpires: token.accessTokenExpires,
|
|
308
|
+
zkEvm: token.zkEvm,
|
|
309
|
+
...token.error && { error: token.error }
|
|
310
|
+
};
|
|
311
|
+
} catch (error) {
|
|
312
|
+
console.error("[auth-next-server] Session callback error:", error);
|
|
313
|
+
throw error;
|
|
314
|
+
}
|
|
287
315
|
}
|
|
288
316
|
},
|
|
289
317
|
session: {
|
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// @ts-ignore - Type exists in next-auth v5 but TS resolver may use stale types
|
|
4
4
|
import type { NextAuthConfig } from 'next-auth';
|
|
5
5
|
import CredentialsImport from 'next-auth/providers/credentials';
|
|
6
|
+
import { encode as encodeImport } from 'next-auth/jwt';
|
|
6
7
|
import type { ImmutableAuthConfig, ImmutableTokenData, UserInfoResponse } from './types';
|
|
7
8
|
import { isTokenExpired, refreshAccessToken, extractZkEvmFromIdToken } from './refresh';
|
|
8
9
|
import {
|
|
@@ -15,6 +16,8 @@ import {
|
|
|
15
16
|
// may be nested under a 'default' property
|
|
16
17
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
18
|
const Credentials = ((CredentialsImport as any).default || CredentialsImport) as typeof CredentialsImport;
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
|
+
const defaultJwtEncode = ((encodeImport as any).default || encodeImport) as typeof encodeImport;
|
|
18
21
|
|
|
19
22
|
/**
|
|
20
23
|
* Validate tokens by calling the userinfo endpoint.
|
|
@@ -72,6 +75,23 @@ export function createAuthConfig(config: ImmutableAuthConfig): NextAuthConfig {
|
|
|
72
75
|
const authDomain = config.authenticationDomain || DEFAULT_AUTH_DOMAIN;
|
|
73
76
|
|
|
74
77
|
return {
|
|
78
|
+
// Custom jwt.encode: strip idToken from the cookie to reduce size and avoid
|
|
79
|
+
// CloudFront 413 "Request Entity Too Large" errors. The idToken (~1-2 KB) is
|
|
80
|
+
// still available in session responses (after sign-in or token refresh) because
|
|
81
|
+
// the session callback runs BEFORE encode. All data extracted FROM idToken
|
|
82
|
+
// (email, nickname, zkEvm) remains in the cookie as separate fields.
|
|
83
|
+
// On the client, idToken is persisted in localStorage by @imtbl/auth-next-client.
|
|
84
|
+
jwt: {
|
|
85
|
+
async encode(params) {
|
|
86
|
+
const { token, ...rest } = params;
|
|
87
|
+
if (token) {
|
|
88
|
+
const { idToken, ...cookieToken } = token as Record<string, unknown>;
|
|
89
|
+
return defaultJwtEncode({ ...rest, token: cookieToken });
|
|
90
|
+
}
|
|
91
|
+
return defaultJwtEncode(params);
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
|
|
75
95
|
providers: [
|
|
76
96
|
Credentials({
|
|
77
97
|
id: IMMUTABLE_PROVIDER_ID,
|
|
@@ -154,31 +174,80 @@ export function createAuthConfig(config: ImmutableAuthConfig): NextAuthConfig {
|
|
|
154
174
|
async jwt({
|
|
155
175
|
token, user, trigger, session: sessionUpdate,
|
|
156
176
|
}: any) {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
177
|
+
try {
|
|
178
|
+
// Initial sign in - store all token data
|
|
179
|
+
if (user) {
|
|
180
|
+
return {
|
|
181
|
+
...token,
|
|
182
|
+
sub: user.sub,
|
|
183
|
+
email: user.email,
|
|
184
|
+
nickname: user.nickname,
|
|
185
|
+
accessToken: user.accessToken,
|
|
186
|
+
refreshToken: user.refreshToken,
|
|
187
|
+
idToken: user.idToken,
|
|
188
|
+
accessTokenExpires: user.accessTokenExpires,
|
|
189
|
+
zkEvm: user.zkEvm,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Handle session update (for client-side token sync or forceRefresh)
|
|
194
|
+
// When client-side Auth refreshes tokens via TOKEN_REFRESHED event,
|
|
195
|
+
// it calls updateSession() which triggers this callback with the new tokens.
|
|
196
|
+
// We clear any stale error (e.g., TokenExpired) on successful update.
|
|
197
|
+
if (trigger === 'update' && sessionUpdate) {
|
|
198
|
+
const update = sessionUpdate as Record<string, unknown>;
|
|
199
|
+
|
|
200
|
+
// If forceRefresh is requested, perform server-side token refresh
|
|
201
|
+
// This is used after zkEVM registration to get updated claims from IDP
|
|
202
|
+
if (update.forceRefresh && token.refreshToken) {
|
|
203
|
+
try {
|
|
204
|
+
const refreshed = await refreshAccessToken(
|
|
205
|
+
token.refreshToken as string,
|
|
206
|
+
config.clientId,
|
|
207
|
+
authDomain,
|
|
208
|
+
);
|
|
209
|
+
// Extract zkEvm claims from the refreshed idToken
|
|
210
|
+
const zkEvm = extractZkEvmFromIdToken(refreshed.idToken);
|
|
211
|
+
return {
|
|
212
|
+
...token,
|
|
213
|
+
accessToken: refreshed.accessToken,
|
|
214
|
+
refreshToken: refreshed.refreshToken,
|
|
215
|
+
idToken: refreshed.idToken,
|
|
216
|
+
accessTokenExpires: refreshed.accessTokenExpires,
|
|
217
|
+
zkEvm: zkEvm ?? token.zkEvm, // Keep existing zkEvm if not in new token
|
|
218
|
+
error: undefined,
|
|
219
|
+
};
|
|
220
|
+
} catch (error) {
|
|
221
|
+
// eslint-disable-next-line no-console
|
|
222
|
+
console.error('[auth-next-server] Force refresh failed:', error);
|
|
223
|
+
return {
|
|
224
|
+
...token,
|
|
225
|
+
error: 'RefreshTokenError',
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Standard session update - merge provided values
|
|
231
|
+
return {
|
|
232
|
+
...token,
|
|
233
|
+
...(update.accessToken ? { accessToken: update.accessToken } : {}),
|
|
234
|
+
...(update.refreshToken ? { refreshToken: update.refreshToken } : {}),
|
|
235
|
+
...(update.idToken ? { idToken: update.idToken } : {}),
|
|
236
|
+
...(update.accessTokenExpires ? { accessTokenExpires: update.accessTokenExpires } : {}),
|
|
237
|
+
...(update.zkEvm ? { zkEvm: update.zkEvm } : {}),
|
|
238
|
+
// Clear any stale error when valid tokens are synced from client-side
|
|
239
|
+
error: undefined,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
171
242
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (trigger === 'update' && sessionUpdate) {
|
|
177
|
-
const update = sessionUpdate as Record<string, unknown>;
|
|
243
|
+
// Return token if not expired
|
|
244
|
+
if (!isTokenExpired(token.accessTokenExpires as number)) {
|
|
245
|
+
return token;
|
|
246
|
+
}
|
|
178
247
|
|
|
179
|
-
//
|
|
180
|
-
// This
|
|
181
|
-
if (
|
|
248
|
+
// Token expired - attempt server-side refresh
|
|
249
|
+
// This ensures clients always get fresh tokens from session callbacks
|
|
250
|
+
if (token.refreshToken) {
|
|
182
251
|
try {
|
|
183
252
|
const refreshed = await refreshAccessToken(
|
|
184
253
|
token.refreshToken as string,
|
|
@@ -194,11 +263,11 @@ export function createAuthConfig(config: ImmutableAuthConfig): NextAuthConfig {
|
|
|
194
263
|
idToken: refreshed.idToken,
|
|
195
264
|
accessTokenExpires: refreshed.accessTokenExpires,
|
|
196
265
|
zkEvm: zkEvm ?? token.zkEvm, // Keep existing zkEvm if not in new token
|
|
197
|
-
error: undefined,
|
|
266
|
+
error: undefined, // Clear any previous error
|
|
198
267
|
};
|
|
199
268
|
} catch (error) {
|
|
200
|
-
|
|
201
|
-
console.error('[auth-next-server]
|
|
269
|
+
// eslint-disable-next-line no-console
|
|
270
|
+
console.error('[auth-next-server] Token refresh failed:', error);
|
|
202
271
|
return {
|
|
203
272
|
...token,
|
|
204
273
|
error: 'RefreshTokenError',
|
|
@@ -206,79 +275,42 @@ export function createAuthConfig(config: ImmutableAuthConfig): NextAuthConfig {
|
|
|
206
275
|
}
|
|
207
276
|
}
|
|
208
277
|
|
|
209
|
-
//
|
|
278
|
+
// No refresh token available
|
|
210
279
|
return {
|
|
211
280
|
...token,
|
|
212
|
-
|
|
213
|
-
...(update.refreshToken ? { refreshToken: update.refreshToken } : {}),
|
|
214
|
-
...(update.idToken ? { idToken: update.idToken } : {}),
|
|
215
|
-
...(update.accessTokenExpires ? { accessTokenExpires: update.accessTokenExpires } : {}),
|
|
216
|
-
...(update.zkEvm ? { zkEvm: update.zkEvm } : {}),
|
|
217
|
-
// Clear any stale error when valid tokens are synced from client-side
|
|
218
|
-
error: undefined,
|
|
281
|
+
error: 'TokenExpired',
|
|
219
282
|
};
|
|
283
|
+
} catch (error) {
|
|
284
|
+
// eslint-disable-next-line no-console
|
|
285
|
+
console.error('[auth-next-server] JWT callback error:', error);
|
|
286
|
+
throw error;
|
|
220
287
|
}
|
|
221
|
-
|
|
222
|
-
// Return token if not expired
|
|
223
|
-
if (!isTokenExpired(token.accessTokenExpires as number)) {
|
|
224
|
-
return token;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Token expired - attempt server-side refresh
|
|
228
|
-
// This ensures clients always get fresh tokens from session callbacks
|
|
229
|
-
if (token.refreshToken) {
|
|
230
|
-
try {
|
|
231
|
-
const refreshed = await refreshAccessToken(
|
|
232
|
-
token.refreshToken as string,
|
|
233
|
-
config.clientId,
|
|
234
|
-
authDomain,
|
|
235
|
-
);
|
|
236
|
-
// Extract zkEvm claims from the refreshed idToken
|
|
237
|
-
const zkEvm = extractZkEvmFromIdToken(refreshed.idToken);
|
|
238
|
-
return {
|
|
239
|
-
...token,
|
|
240
|
-
accessToken: refreshed.accessToken,
|
|
241
|
-
refreshToken: refreshed.refreshToken,
|
|
242
|
-
idToken: refreshed.idToken,
|
|
243
|
-
accessTokenExpires: refreshed.accessTokenExpires,
|
|
244
|
-
zkEvm: zkEvm ?? token.zkEvm, // Keep existing zkEvm if not in new token
|
|
245
|
-
error: undefined, // Clear any previous error
|
|
246
|
-
};
|
|
247
|
-
} catch (error) {
|
|
248
|
-
// eslint-disable-next-line no-console
|
|
249
|
-
console.error('[auth-next-server] Token refresh failed:', error);
|
|
250
|
-
return {
|
|
251
|
-
...token,
|
|
252
|
-
error: 'RefreshTokenError',
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// No refresh token available
|
|
258
|
-
return {
|
|
259
|
-
...token,
|
|
260
|
-
error: 'TokenExpired',
|
|
261
|
-
};
|
|
262
288
|
},
|
|
263
289
|
|
|
264
290
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
265
291
|
async session({ session, token }: any) {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
292
|
+
try {
|
|
293
|
+
// Expose token data to the session
|
|
294
|
+
return {
|
|
295
|
+
...session,
|
|
296
|
+
user: {
|
|
297
|
+
...session.user,
|
|
298
|
+
sub: token.sub as string,
|
|
299
|
+
email: token.email as string | undefined,
|
|
300
|
+
nickname: token.nickname as string | undefined,
|
|
301
|
+
},
|
|
302
|
+
accessToken: token.accessToken as string,
|
|
303
|
+
refreshToken: token.refreshToken as string | undefined,
|
|
304
|
+
idToken: token.idToken as string | undefined,
|
|
305
|
+
accessTokenExpires: token.accessTokenExpires as number,
|
|
306
|
+
zkEvm: token.zkEvm,
|
|
307
|
+
...(token.error && { error: token.error as string }),
|
|
308
|
+
};
|
|
309
|
+
} catch (error) {
|
|
310
|
+
// eslint-disable-next-line no-console
|
|
311
|
+
console.error('[auth-next-server] Session callback error:', error);
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
282
314
|
},
|
|
283
315
|
},
|
|
284
316
|
|