@imtbl/auth-next-server 2.12.7-alpha.1 → 2.12.7-alpha.11
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 +128 -64
- package/dist/node/index.cjs +141 -75
- package/dist/node/index.js +131 -75
- package/dist/types/config.d.ts +20 -7
- package/dist/types/constants.d.ts +17 -0
- package/dist/types/index.d.ts +1 -0
- package/package.json +3 -3
- package/src/config.ts +163 -99
- package/src/constants.ts +21 -0
- package/src/index.ts +12 -0
package/src/config.ts
CHANGED
|
@@ -3,10 +3,13 @@
|
|
|
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 {
|
|
9
10
|
DEFAULT_AUTH_DOMAIN,
|
|
11
|
+
DEFAULT_REDIRECT_URI_PATH,
|
|
12
|
+
DEFAULT_SANDBOX_CLIENT_ID,
|
|
10
13
|
IMMUTABLE_PROVIDER_ID,
|
|
11
14
|
DEFAULT_SESSION_MAX_AGE_SECONDS,
|
|
12
15
|
} from './constants';
|
|
@@ -15,6 +18,8 @@ import {
|
|
|
15
18
|
// may be nested under a 'default' property
|
|
16
19
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
20
|
const Credentials = ((CredentialsImport as any).default || CredentialsImport) as typeof CredentialsImport;
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
22
|
+
const defaultJwtEncode = ((encodeImport as any).default || encodeImport) as typeof encodeImport;
|
|
18
23
|
|
|
19
24
|
/**
|
|
20
25
|
* Validate tokens by calling the userinfo endpoint.
|
|
@@ -52,26 +57,73 @@ async function validateTokens(
|
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
/**
|
|
55
|
-
*
|
|
60
|
+
* Resolve redirect URI for zero-config mode.
|
|
61
|
+
* Uses __NEXT_PRIVATE_ORIGIN when available (Next.js internal), otherwise path-only.
|
|
62
|
+
*/
|
|
63
|
+
function resolveDefaultRedirectUri(): string {
|
|
64
|
+
// eslint-disable-next-line no-underscore-dangle -- Next.js internal env var
|
|
65
|
+
const origin = process.env.__NEXT_PRIVATE_ORIGIN;
|
|
66
|
+
if (origin) {
|
|
67
|
+
return new URL(DEFAULT_REDIRECT_URI_PATH, origin).href;
|
|
68
|
+
}
|
|
69
|
+
return DEFAULT_REDIRECT_URI_PATH;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Create Auth.js v5 configuration for Immutable authentication.
|
|
74
|
+
*
|
|
75
|
+
* Policy: provide nothing → full sandbox config; provide config → provide everything.
|
|
76
|
+
* - Zero config: sandbox clientId, auto-derived redirectUri. No conflicts.
|
|
77
|
+
* - With config: clientId and redirectUri required. Pass full config to avoid conflicts.
|
|
78
|
+
*
|
|
79
|
+
* @param config - Optional. When omitted, uses sandbox defaults. When provided, clientId and redirectUri are required.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```typescript
|
|
83
|
+
* // Zero config - sandbox, only AUTH_SECRET required in .env
|
|
84
|
+
* import NextAuth from "next-auth";
|
|
85
|
+
* import { createAuthConfig } from "@imtbl/auth-next-server";
|
|
86
|
+
*
|
|
87
|
+
* export const { handlers, auth, signIn, signOut } = NextAuth(createAuthConfig());
|
|
88
|
+
* ```
|
|
56
89
|
*
|
|
57
90
|
* @example
|
|
58
91
|
* ```typescript
|
|
59
|
-
* //
|
|
92
|
+
* // With config - provide clientId and redirectUri (and optionally audience, scope, authenticationDomain)
|
|
60
93
|
* import NextAuth from "next-auth";
|
|
61
94
|
* import { createAuthConfig } from "@imtbl/auth-next-server";
|
|
62
95
|
*
|
|
63
|
-
* const
|
|
96
|
+
* export const { handlers, auth, signIn, signOut } = NextAuth(createAuthConfig({
|
|
64
97
|
* clientId: process.env.NEXT_PUBLIC_IMMUTABLE_CLIENT_ID!,
|
|
65
98
|
* redirectUri: `${process.env.NEXT_PUBLIC_BASE_URL}/callback`,
|
|
66
|
-
* };
|
|
67
|
-
*
|
|
68
|
-
* export const { handlers, auth, signIn, signOut } = NextAuth(createAuthConfig(config));
|
|
99
|
+
* }));
|
|
69
100
|
* ```
|
|
70
101
|
*/
|
|
71
|
-
export function createAuthConfig(config
|
|
72
|
-
const
|
|
102
|
+
export function createAuthConfig(config?: ImmutableAuthConfig): NextAuthConfig {
|
|
103
|
+
const resolvedConfig: ImmutableAuthConfig = config ?? {
|
|
104
|
+
clientId: DEFAULT_SANDBOX_CLIENT_ID,
|
|
105
|
+
redirectUri: resolveDefaultRedirectUri(),
|
|
106
|
+
};
|
|
107
|
+
const authDomain = resolvedConfig.authenticationDomain || DEFAULT_AUTH_DOMAIN;
|
|
73
108
|
|
|
74
109
|
return {
|
|
110
|
+
// Custom jwt.encode: strip idToken from the cookie to reduce size and avoid
|
|
111
|
+
// CloudFront 413 "Request Entity Too Large" errors. The idToken (~1-2 KB) is
|
|
112
|
+
// still available in session responses (after sign-in or token refresh) because
|
|
113
|
+
// the session callback runs BEFORE encode. All data extracted FROM idToken
|
|
114
|
+
// (email, nickname, zkEvm) remains in the cookie as separate fields.
|
|
115
|
+
// On the client, idToken is persisted in localStorage by @imtbl/auth-next-client.
|
|
116
|
+
jwt: {
|
|
117
|
+
async encode(params) {
|
|
118
|
+
const { token, ...rest } = params;
|
|
119
|
+
if (token) {
|
|
120
|
+
const { idToken, ...cookieToken } = token as Record<string, unknown>;
|
|
121
|
+
return defaultJwtEncode({ ...rest, token: cookieToken });
|
|
122
|
+
}
|
|
123
|
+
return defaultJwtEncode(params);
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
|
|
75
127
|
providers: [
|
|
76
128
|
Credentials({
|
|
77
129
|
id: IMMUTABLE_PROVIDER_ID,
|
|
@@ -154,35 +206,84 @@ export function createAuthConfig(config: ImmutableAuthConfig): NextAuthConfig {
|
|
|
154
206
|
async jwt({
|
|
155
207
|
token, user, trigger, session: sessionUpdate,
|
|
156
208
|
}: any) {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
209
|
+
try {
|
|
210
|
+
// Initial sign in - store all token data
|
|
211
|
+
if (user) {
|
|
212
|
+
return {
|
|
213
|
+
...token,
|
|
214
|
+
sub: user.sub,
|
|
215
|
+
email: user.email,
|
|
216
|
+
nickname: user.nickname,
|
|
217
|
+
accessToken: user.accessToken,
|
|
218
|
+
refreshToken: user.refreshToken,
|
|
219
|
+
idToken: user.idToken,
|
|
220
|
+
accessTokenExpires: user.accessTokenExpires,
|
|
221
|
+
zkEvm: user.zkEvm,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
171
224
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
225
|
+
// Handle session update (for client-side token sync or forceRefresh)
|
|
226
|
+
// When client-side Auth refreshes tokens via TOKEN_REFRESHED event,
|
|
227
|
+
// it calls updateSession() which triggers this callback with the new tokens.
|
|
228
|
+
// We clear any stale error (e.g., TokenExpired) on successful update.
|
|
229
|
+
if (trigger === 'update' && sessionUpdate) {
|
|
230
|
+
const update = sessionUpdate as Record<string, unknown>;
|
|
178
231
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
232
|
+
// If forceRefresh is requested, perform server-side token refresh
|
|
233
|
+
// This is used after zkEVM registration to get updated claims from IDP
|
|
234
|
+
if (update.forceRefresh && token.refreshToken) {
|
|
235
|
+
try {
|
|
236
|
+
const refreshed = await refreshAccessToken(
|
|
237
|
+
token.refreshToken as string,
|
|
238
|
+
resolvedConfig.clientId,
|
|
239
|
+
authDomain,
|
|
240
|
+
);
|
|
241
|
+
// Extract zkEvm claims from the refreshed idToken
|
|
242
|
+
const zkEvm = extractZkEvmFromIdToken(refreshed.idToken);
|
|
243
|
+
return {
|
|
244
|
+
...token,
|
|
245
|
+
accessToken: refreshed.accessToken,
|
|
246
|
+
refreshToken: refreshed.refreshToken,
|
|
247
|
+
idToken: refreshed.idToken,
|
|
248
|
+
accessTokenExpires: refreshed.accessTokenExpires,
|
|
249
|
+
zkEvm: zkEvm ?? token.zkEvm, // Keep existing zkEvm if not in new token
|
|
250
|
+
error: undefined,
|
|
251
|
+
};
|
|
252
|
+
} catch (error) {
|
|
253
|
+
// eslint-disable-next-line no-console
|
|
254
|
+
console.error('[auth-next-server] Force refresh failed:', error);
|
|
255
|
+
return {
|
|
256
|
+
...token,
|
|
257
|
+
error: 'RefreshTokenError',
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Standard session update - merge provided values
|
|
263
|
+
return {
|
|
264
|
+
...token,
|
|
265
|
+
...(update.accessToken ? { accessToken: update.accessToken } : {}),
|
|
266
|
+
...(update.refreshToken ? { refreshToken: update.refreshToken } : {}),
|
|
267
|
+
...(update.idToken ? { idToken: update.idToken } : {}),
|
|
268
|
+
...(update.accessTokenExpires ? { accessTokenExpires: update.accessTokenExpires } : {}),
|
|
269
|
+
...(update.zkEvm ? { zkEvm: update.zkEvm } : {}),
|
|
270
|
+
// Clear any stale error when valid tokens are synced from client-side
|
|
271
|
+
error: undefined,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Return token if not expired
|
|
276
|
+
if (!isTokenExpired(token.accessTokenExpires as number)) {
|
|
277
|
+
return token;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Token expired - attempt server-side refresh
|
|
281
|
+
// This ensures clients always get fresh tokens from session callbacks
|
|
282
|
+
if (token.refreshToken) {
|
|
182
283
|
try {
|
|
183
284
|
const refreshed = await refreshAccessToken(
|
|
184
285
|
token.refreshToken as string,
|
|
185
|
-
|
|
286
|
+
resolvedConfig.clientId,
|
|
186
287
|
authDomain,
|
|
187
288
|
);
|
|
188
289
|
// Extract zkEvm claims from the refreshed idToken
|
|
@@ -194,11 +295,11 @@ export function createAuthConfig(config: ImmutableAuthConfig): NextAuthConfig {
|
|
|
194
295
|
idToken: refreshed.idToken,
|
|
195
296
|
accessTokenExpires: refreshed.accessTokenExpires,
|
|
196
297
|
zkEvm: zkEvm ?? token.zkEvm, // Keep existing zkEvm if not in new token
|
|
197
|
-
error: undefined,
|
|
298
|
+
error: undefined, // Clear any previous error
|
|
198
299
|
};
|
|
199
300
|
} catch (error) {
|
|
200
301
|
// eslint-disable-next-line no-console
|
|
201
|
-
console.error('[auth-next-server]
|
|
302
|
+
console.error('[auth-next-server] Token refresh failed:', error);
|
|
202
303
|
return {
|
|
203
304
|
...token,
|
|
204
305
|
error: 'RefreshTokenError',
|
|
@@ -206,79 +307,42 @@ export function createAuthConfig(config: ImmutableAuthConfig): NextAuthConfig {
|
|
|
206
307
|
}
|
|
207
308
|
}
|
|
208
309
|
|
|
209
|
-
//
|
|
310
|
+
// No refresh token available
|
|
210
311
|
return {
|
|
211
312
|
...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,
|
|
313
|
+
error: 'TokenExpired',
|
|
219
314
|
};
|
|
315
|
+
} catch (error) {
|
|
316
|
+
// eslint-disable-next-line no-console
|
|
317
|
+
console.error('[auth-next-server] JWT callback error:', error);
|
|
318
|
+
throw error;
|
|
220
319
|
}
|
|
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
320
|
},
|
|
263
321
|
|
|
264
322
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
265
323
|
async session({ session, token }: any) {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
324
|
+
try {
|
|
325
|
+
// Expose token data to the session
|
|
326
|
+
return {
|
|
327
|
+
...session,
|
|
328
|
+
user: {
|
|
329
|
+
...session.user,
|
|
330
|
+
sub: token.sub as string,
|
|
331
|
+
email: token.email as string | undefined,
|
|
332
|
+
nickname: token.nickname as string | undefined,
|
|
333
|
+
},
|
|
334
|
+
accessToken: token.accessToken as string,
|
|
335
|
+
refreshToken: token.refreshToken as string | undefined,
|
|
336
|
+
idToken: token.idToken as string | undefined,
|
|
337
|
+
accessTokenExpires: token.accessTokenExpires as number,
|
|
338
|
+
zkEvm: token.zkEvm,
|
|
339
|
+
...(token.error && { error: token.error as string }),
|
|
340
|
+
};
|
|
341
|
+
} catch (error) {
|
|
342
|
+
// eslint-disable-next-line no-console
|
|
343
|
+
console.error('[auth-next-server] Session callback error:', error);
|
|
344
|
+
throw error;
|
|
345
|
+
}
|
|
282
346
|
},
|
|
283
347
|
},
|
|
284
348
|
|
package/src/constants.ts
CHANGED
|
@@ -44,8 +44,29 @@ export const DEFAULT_TOKEN_EXPIRY_MS = DEFAULT_TOKEN_EXPIRY_SECONDS * 1000;
|
|
|
44
44
|
*/
|
|
45
45
|
export const TOKEN_EXPIRY_BUFFER_SECONDS = 60;
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Buffer time in milliseconds before token expiry to trigger refresh.
|
|
49
|
+
* Used by auth-next-client for client-side refresh timing.
|
|
50
|
+
*/
|
|
51
|
+
export const TOKEN_EXPIRY_BUFFER_MS = TOKEN_EXPIRY_BUFFER_SECONDS * 1000;
|
|
52
|
+
|
|
47
53
|
/**
|
|
48
54
|
* Default session max age in seconds (365 days)
|
|
49
55
|
* This is how long the NextAuth session cookie will be valid
|
|
50
56
|
*/
|
|
51
57
|
export const DEFAULT_SESSION_MAX_AGE_SECONDS = 365 * 24 * 60 * 60;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Sandbox client ID for auth-next zero-config.
|
|
61
|
+
*/
|
|
62
|
+
export const DEFAULT_SANDBOX_CLIENT_ID = 'mjtCL8mt06BtbxSkp2vbrYStKWnXVZfo';
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Default redirect URI path for sandbox zero-config.
|
|
66
|
+
*/
|
|
67
|
+
export const DEFAULT_REDIRECT_URI_PATH = '/callback';
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Default logout redirect URI path
|
|
71
|
+
*/
|
|
72
|
+
export const DEFAULT_LOGOUT_REDIRECT_URI_PATH = '/';
|
package/src/index.ts
CHANGED
|
@@ -45,6 +45,18 @@ export {
|
|
|
45
45
|
type RefreshedTokens,
|
|
46
46
|
type ZkEvmData,
|
|
47
47
|
} from './refresh';
|
|
48
|
+
export {
|
|
49
|
+
DEFAULT_AUTH_DOMAIN,
|
|
50
|
+
DEFAULT_AUDIENCE,
|
|
51
|
+
DEFAULT_SCOPE,
|
|
52
|
+
IMMUTABLE_PROVIDER_ID,
|
|
53
|
+
DEFAULT_NEXTAUTH_BASE_PATH,
|
|
54
|
+
DEFAULT_SANDBOX_CLIENT_ID,
|
|
55
|
+
DEFAULT_REDIRECT_URI_PATH,
|
|
56
|
+
DEFAULT_LOGOUT_REDIRECT_URI_PATH,
|
|
57
|
+
DEFAULT_TOKEN_EXPIRY_MS,
|
|
58
|
+
TOKEN_EXPIRY_BUFFER_MS,
|
|
59
|
+
} from './constants';
|
|
48
60
|
|
|
49
61
|
// ============================================================================
|
|
50
62
|
// Type exports
|