Skip to content

Commit e77a2cc

Browse files
Merge branch 'main' into ID-4319
2 parents 6b67639 + bbcb69f commit e77a2cc

6 files changed

Lines changed: 201 additions & 64 deletions

File tree

packages/auth-next-client/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -582,7 +582,7 @@ The session type returned by `useImmutableSession`. Note that `accessToken` is i
582582
interface ImmutableSession {
583583
// accessToken is NOT exposed -- use getAccessToken() instead
584584
refreshToken?: string;
585-
idToken?: string;
585+
idToken?: string; // Only present transiently after sign-in or token refresh (not stored in cookie)
586586
accessTokenExpires: number;
587587
zkEvm?: {
588588
ethAddress: string;
@@ -597,6 +597,8 @@ interface ImmutableSession {
597597
}
598598
```
599599

600+
> **Note:** The `idToken` is **not** stored in the session cookie (to avoid CloudFront 413 errors from oversized headers). It is only present in the session response transiently after sign-in or token refresh. `@imtbl/auth-next-client` automatically persists it in `localStorage` so that `getUser()` always returns a valid `idToken` for wallet operations. All data extracted from the idToken (`email`, `nickname`, `zkEvm`) remains in the cookie as separate fields and is always available in the session.
601+
600602
### LoginConfig
601603

602604
Configuration for the `useLogin` hook's login functions:

packages/auth-next-client/src/callback.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { signIn } from 'next-auth/react';
66
import { handleLoginCallback as handleAuthCallback, type TokenResponse } from '@imtbl/auth';
77
import type { ImmutableUserClient } from './types';
88
import { IMMUTABLE_PROVIDER_ID } from './constants';
9+
import { storeIdToken } from './idTokenStorage';
910

1011
/**
1112
* Config for CallbackPage - matches LoginConfig from @imtbl/auth
@@ -159,6 +160,12 @@ export function CallbackPage({
159160
// Not in a popup - sign in to NextAuth with the tokens
160161
const tokenData = mapTokensToSignInData(tokens);
161162

163+
// Persist idToken to localStorage before signIn so it's available
164+
// immediately. The cookie won't contain idToken (stripped by jwt.encode).
165+
if (tokens.idToken) {
166+
storeIdToken(tokens.idToken);
167+
}
168+
162169
const result = await signIn(IMMUTABLE_PROVIDER_ID, {
163170
tokens: JSON.stringify(tokenData),
164171
redirect: false,

packages/auth-next-client/src/hooks.tsx

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
DEFAULT_SCOPE,
3131
DEFAULT_AUDIENCE,
3232
} from './constants';
33+
import { storeIdToken, getStoredIdToken, clearStoredIdToken } from './idTokenStorage';
3334

3435
// ---------------------------------------------------------------------------
3536
// Module-level deduplication for session refresh
@@ -307,6 +308,20 @@ export function useImmutableSession(): UseImmutableSessionReturn {
307308
}
308309
}, [session?.accessTokenExpires]);
309310

311+
// ---------------------------------------------------------------------------
312+
// Sync idToken to localStorage
313+
// ---------------------------------------------------------------------------
314+
315+
// The idToken is stripped from the cookie by jwt.encode on the server to avoid
316+
// CloudFront 413 errors. It is only present in the session response transiently
317+
// after sign-in or token refresh. When present, persist it in localStorage so
318+
// that getUser() can always return it (used by wallet's MagicTEESigner).
319+
useEffect(() => {
320+
if (session?.idToken) {
321+
storeIdToken(session.idToken);
322+
}
323+
}, [session?.idToken]);
324+
310325
/**
311326
* Get user function for wallet integration.
312327
* Returns a User object compatible with @imtbl/wallet's getUser option.
@@ -331,6 +346,10 @@ export function useImmutableSession(): UseImmutableSessionReturn {
331346
// Also update the ref so subsequent calls get the fresh data
332347
if (currentSession) {
333348
sessionRef.current = currentSession;
349+
// Immediately persist fresh idToken to localStorage (avoids race with useEffect)
350+
if (currentSession.idToken) {
351+
storeIdToken(currentSession.idToken);
352+
}
334353
}
335354
} catch (error) {
336355
// eslint-disable-next-line no-console
@@ -347,6 +366,10 @@ export function useImmutableSession(): UseImmutableSessionReturn {
347366
if (refreshed) {
348367
currentSession = refreshed as ImmutableSessionInternal;
349368
sessionRef.current = currentSession;
369+
// Persist fresh idToken to localStorage immediately
370+
if (currentSession.idToken) {
371+
storeIdToken(currentSession.idToken);
372+
}
350373
} else {
351374
currentSession = sessionRef.current;
352375
}
@@ -370,7 +393,9 @@ export function useImmutableSession(): UseImmutableSessionReturn {
370393
return {
371394
accessToken: currentSession.accessToken,
372395
refreshToken: currentSession.refreshToken,
373-
idToken: currentSession.idToken,
396+
// Prefer session idToken (fresh after sign-in or refresh, before useEffect
397+
// stores it), fall back to localStorage for normal reads (cookie has no idToken).
398+
idToken: currentSession.idToken || getStoredIdToken(),
374399
profile: {
375400
sub: currentSession.user?.sub ?? '',
376401
email: currentSession.user?.email ?? undefined,
@@ -535,6 +560,12 @@ export function useLogin(): UseLoginReturn {
535560
profile: { sub: string; email?: string; nickname?: string };
536561
zkEvm?: ZkEvmInfo;
537562
}) => {
563+
// Persist idToken to localStorage before signIn so it's available immediately.
564+
// The cookie won't contain idToken (stripped by jwt.encode on the server).
565+
if (tokens.idToken) {
566+
storeIdToken(tokens.idToken);
567+
}
568+
538569
const result = await signIn(IMMUTABLE_PROVIDER_ID, {
539570
tokens: JSON.stringify(tokens),
540571
redirect: false,
@@ -735,6 +766,9 @@ export function useLogout(): UseLogoutReturn {
735766
setError(null);
736767

737768
try {
769+
// Clear idToken from localStorage before clearing session
770+
clearStoredIdToken();
771+
738772
// First, clear the NextAuth session (this clears the JWT cookie)
739773
// We use redirect: false to handle the redirect ourselves for federated logout
740774
await signOut({ redirect: false });
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* Utility for persisting idToken in localStorage.
3+
*
4+
* The idToken is stripped from the NextAuth session cookie (via a custom
5+
* jwt.encode in @imtbl/auth-next-server) to keep cookie size under CDN header
6+
* limits (CloudFront 20 KB). Instead, the client stores idToken in
7+
* localStorage so that wallet operations (e.g., MagicTEESigner) can still
8+
* access it via getUser().
9+
*
10+
* All functions are safe to call during SSR or in restricted environments
11+
* (e.g., incognito mode with localStorage disabled) -- they silently no-op.
12+
*/
13+
14+
const ID_TOKEN_STORAGE_KEY = 'imtbl_id_token';
15+
16+
/**
17+
* Store the idToken in localStorage.
18+
* @param idToken - The raw ID token JWT string
19+
*/
20+
export function storeIdToken(idToken: string): void {
21+
try {
22+
if (typeof window !== 'undefined' && window.localStorage) {
23+
window.localStorage.setItem(ID_TOKEN_STORAGE_KEY, idToken);
24+
}
25+
} catch {
26+
// Silently ignore -- localStorage may be unavailable (SSR, incognito, etc.)
27+
}
28+
}
29+
30+
/**
31+
* Retrieve the idToken from localStorage.
32+
* @returns The stored idToken, or undefined if not available.
33+
*/
34+
export function getStoredIdToken(): string | undefined {
35+
try {
36+
if (typeof window !== 'undefined' && window.localStorage) {
37+
return window.localStorage.getItem(ID_TOKEN_STORAGE_KEY) ?? undefined;
38+
}
39+
} catch {
40+
// Silently ignore
41+
}
42+
return undefined;
43+
}
44+
45+
/**
46+
* Remove the idToken from localStorage (e.g., on logout).
47+
*/
48+
export function clearStoredIdToken(): void {
49+
try {
50+
if (typeof window !== 'undefined' && window.localStorage) {
51+
window.localStorage.removeItem(ID_TOKEN_STORAGE_KEY);
52+
}
53+
} catch {
54+
// Silently ignore
55+
}
56+
}

0 commit comments

Comments
 (0)