Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/auth-next-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,7 @@ The session type returned by `useImmutableSession`. Note that `accessToken` is i
interface ImmutableSession {
// accessToken is NOT exposed -- use getAccessToken() instead
refreshToken?: string;
idToken?: string;
idToken?: string; // Only present transiently after sign-in or token refresh (not stored in cookie)
accessTokenExpires: number;
zkEvm?: {
ethAddress: string;
Expand All @@ -597,6 +597,8 @@ interface ImmutableSession {
}
```

> **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.

### LoginConfig

Configuration for the `useLogin` hook's login functions:
Expand Down
7 changes: 7 additions & 0 deletions packages/auth-next-client/src/callback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { signIn } from 'next-auth/react';
import { handleLoginCallback as handleAuthCallback, type TokenResponse } from '@imtbl/auth';
import type { ImmutableUserClient } from './types';
import { IMMUTABLE_PROVIDER_ID } from './constants';
import { storeIdToken } from './idTokenStorage';

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

// Persist idToken to localStorage before signIn so it's available
// immediately. The cookie won't contain idToken (stripped by jwt.encode).
if (tokens.idToken) {
storeIdToken(tokens.idToken);
}

const result = await signIn(IMMUTABLE_PROVIDER_ID, {
tokens: JSON.stringify(tokenData),
redirect: false,
Expand Down
36 changes: 35 additions & 1 deletion packages/auth-next-client/src/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
logoutWithRedirect as rawLogoutWithRedirect,
} from '@imtbl/auth';
import { IMMUTABLE_PROVIDER_ID, TOKEN_EXPIRY_BUFFER_MS } from './constants';
import { storeIdToken, getStoredIdToken, clearStoredIdToken } from './idTokenStorage';

// ---------------------------------------------------------------------------
// Module-level deduplication for session refresh
Expand Down Expand Up @@ -189,6 +190,20 @@ export function useImmutableSession(): UseImmutableSessionReturn {
}
}, [session?.accessTokenExpires]);

// ---------------------------------------------------------------------------
// Sync idToken to localStorage
// ---------------------------------------------------------------------------

// The idToken is stripped from the cookie by jwt.encode on the server to avoid
// CloudFront 413 errors. It is only present in the session response transiently
// after sign-in or token refresh. When present, persist it in localStorage so
// that getUser() can always return it (used by wallet's MagicTEESigner).
useEffect(() => {
if (session?.idToken) {
storeIdToken(session.idToken);
}
}, [session?.idToken]);

/**
* Get user function for wallet integration.
* Returns a User object compatible with @imtbl/wallet's getUser option.
Expand All @@ -213,6 +228,10 @@ export function useImmutableSession(): UseImmutableSessionReturn {
// Also update the ref so subsequent calls get the fresh data
if (currentSession) {
sessionRef.current = currentSession;
// Immediately persist fresh idToken to localStorage (avoids race with useEffect)
if (currentSession.idToken) {
storeIdToken(currentSession.idToken);
}
}
} catch (error) {
// eslint-disable-next-line no-console
Expand All @@ -229,6 +248,10 @@ export function useImmutableSession(): UseImmutableSessionReturn {
if (refreshed) {
currentSession = refreshed as ImmutableSessionInternal;
sessionRef.current = currentSession;
// Persist fresh idToken to localStorage immediately
if (currentSession.idToken) {
storeIdToken(currentSession.idToken);
}
} else {
currentSession = sessionRef.current;
}
Expand All @@ -252,7 +275,9 @@ export function useImmutableSession(): UseImmutableSessionReturn {
return {
accessToken: currentSession.accessToken,
refreshToken: currentSession.refreshToken,
idToken: currentSession.idToken,
// Prefer session idToken (fresh after sign-in or refresh, before useEffect
// stores it), fall back to localStorage for normal reads (cookie has no idToken).
idToken: currentSession.idToken || getStoredIdToken(),
profile: {
sub: currentSession.user?.sub ?? '',
email: currentSession.user?.email ?? undefined,
Expand Down Expand Up @@ -387,6 +412,12 @@ export function useLogin(): UseLoginReturn {
profile: { sub: string; email?: string; nickname?: string };
zkEvm?: ZkEvmInfo;
}) => {
// Persist idToken to localStorage before signIn so it's available immediately.
// The cookie won't contain idToken (stripped by jwt.encode on the server).
if (tokens.idToken) {
storeIdToken(tokens.idToken);
}

const result = await signIn(IMMUTABLE_PROVIDER_ID, {
tokens: JSON.stringify(tokens),
redirect: false,
Expand Down Expand Up @@ -550,6 +581,9 @@ export function useLogout(): UseLogoutReturn {
setError(null);

try {
// Clear idToken from localStorage before clearing session
clearStoredIdToken();

// First, clear the NextAuth session (this clears the JWT cookie)
// We use redirect: false to handle the redirect ourselves for federated logout
await signOut({ redirect: false });
Expand Down
56 changes: 56 additions & 0 deletions packages/auth-next-client/src/idTokenStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* Utility for persisting idToken in localStorage.
*
* The idToken is stripped from the NextAuth session cookie (via a custom
* jwt.encode in @imtbl/auth-next-server) to keep cookie size under CDN header
* limits (CloudFront 20 KB). Instead, the client stores idToken in
* localStorage so that wallet operations (e.g., MagicTEESigner) can still
* access it via getUser().
*
* All functions are safe to call during SSR or in restricted environments
* (e.g., incognito mode with localStorage disabled) -- they silently no-op.
*/

const ID_TOKEN_STORAGE_KEY = 'imtbl_id_token';

/**
* Store the idToken in localStorage.
* @param idToken - The raw ID token JWT string
*/
export function storeIdToken(idToken: string): void {
try {
if (typeof window !== 'undefined' && window.localStorage) {
window.localStorage.setItem(ID_TOKEN_STORAGE_KEY, idToken);
}
} catch {
// Silently ignore -- localStorage may be unavailable (SSR, incognito, etc.)
}
}

/**
* Retrieve the idToken from localStorage.
* @returns The stored idToken, or undefined if not available.
*/
export function getStoredIdToken(): string | undefined {
try {
if (typeof window !== 'undefined' && window.localStorage) {
return window.localStorage.getItem(ID_TOKEN_STORAGE_KEY) ?? undefined;
}
} catch {
// Silently ignore
}
return undefined;
}

/**
* Remove the idToken from localStorage (e.g., on logout).
*/
export function clearStoredIdToken(): void {
try {
if (typeof window !== 'undefined' && window.localStorage) {
window.localStorage.removeItem(ID_TOKEN_STORAGE_KEY);
}
} catch {
// Silently ignore
}
}
Loading