> ## Documentation Index
> Fetch the complete documentation index at: https://docs.immutable.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

## Prerequisites

### Create Passport Client

Set up your Passport client in Immutable Hub to get the credentials needed for authentication.

> [card] [Create Passport Client](https://docs.immutable.com/docs/products/passport/clients.md)
>
> Step-by-step guide to creating a Passport client, configuring redirect URIs, and getting your Client ID

You'll need:

* **Client ID** from your Passport client in Hub
* **Publishable Key** from your Hub project settings

These values are required to initialize Passport in the next section.

### Passport Credentials Reference

| Field | Type | Description |
| - | - | - |
| **Client ID** | Provided by Hub | Unique identifier for your application. Copy from Hub and use in SDK initialization. |
| **Publishable Key** | Provided by Hub | Public key safe for client-side code. Copy from Hub project settings. |
| **Application Type** | You configure | Select **Web** for TypeScript/web apps, **Native** for Unity/Unreal games. |
| **Application Name** | You configure | Identifier for your project inside Passport (e.g., "My Game"). |
| **Redirect URIs** | You configure | Where users land after successful authentication. Must exactly match `redirectUri` in your code. Examples: `http://localhost:3000/redirect` (web), `mygame://callback` (native). |
| **Logout URIs** | You configure | Where users land after logout. Must exactly match `logoutRedirectUri` in your code. Examples: `http://localhost:3000/logout` (web), `mygame://logout` (native). |

> [!NOTE]
>
> Passport uses [OpenID Connect](https://openid.net/connect/) (OIDC). Redirect URIs must be exact matches—wildcards aren't supported for security reasons. Register multiple URIs for different environments (localhost, staging, production).

For complete details on client configuration, see [Passport Clients in Hub](https://docs.immutable.com/docs/products/passport/clients.md).

## Installation

Install the Immutable SDK for your platform to get started with Passport:

> [card] [TypeScript](https://docs.immutable.com/docs/sdks/typescript/overview.md)
>
> Install via npm or yarn

> [card] [Next.js](https://docs.immutable.com/docs/sdks/typescript/overview.md)
>
> Install via npm for App Router

> [card] [Unity](https://docs.immutable.com/docs/sdks/unity/overview.md)
>
> Install via Package Manager

> [card] [Unreal](https://docs.immutable.com/docs/sdks/unreal/overview.md)
>
> Install Passport plugin in your Unreal project

## Initialize Passport

> [tab] Next.js
>
> Create your auth configuration:
>
> ```typescript theme={null}
> // lib/auth.ts
> import { NextAuth, createAuthConfig } from "@imtbl/auth-next-server";
>
> export const { handlers, auth, signIn, signOut } = NextAuth({
>   ...createAuthConfig({
>     clientId: process.env.NEXT_PUBLIC_IMMUTABLE_CLIENT_ID!,
>     redirectUri: `${process.env.NEXT_PUBLIC_BASE_URL}/callback`,
>   }),
>   secret: process.env.AUTH_SECRET,
>   trustHost: true,
> });
> ```
>
> Create the API route handler:
>
> ```typescript theme={null}
> // app/api/auth/[...nextauth]/route.ts
> import { handlers } from "@/lib/auth";
>
> export const { GET, POST } = handlers;
> ```
>
> Wrap your app with `SessionProvider`:
>
> ```tsx theme={null}
> // app/layout.tsx
> import { SessionProvider } from "next-auth/react";
>
> export default function RootLayout({ children }: { children: React.ReactNode }) {
>   return (
>     <html lang="en">
>       <body>
>         <SessionProvider>{children}</SessionProvider>
>       </body>
>     </html>
>   );
> }
> ```
>
> Set environment variables:
>
> ```env theme={null}
> # .env.local
> NEXT_PUBLIC_IMMUTABLE_CLIENT_ID=your_client_id
> NEXT_PUBLIC_BASE_URL=http://localhost:3000
> AUTH_SECRET=your-secret-key-min-32-characters
> ```
>
> For full setup details including server utilities and route protection, see the [Next.js integration guide](https://docs.immutable.com/docs/sdks/typescript/overview.md#nextjs-server-utilities).

> [tab] TypeScript
>
> ```typescript theme={null}
> import { Auth } from '@imtbl/auth';
>
> export const auth = new Auth({
>   clientId: process.env.NEXT_PUBLIC_CLIENT_ID || 'YOUR_CLIENT_ID',
>   redirectUri: 'http://localhost:3000/redirect',
>   logoutRedirectUri: 'http://localhost:3000/logout',
>   logoutMode: 'redirect', // or 'silent' - see Logout section for details
>   audience: 'platform_api',
>   scope: 'openid offline_access email transact',
> });
> ```
>
> ### Scopes
>
> | Scope | Required | Description |
> | - | - | - |
> | `openid` | ✅ Yes | Returns user ID (sub claim) |
> | `offline_access` | ✅ Yes | Enables refresh tokens for persistent sessions |
> | `email` | Optional | Access to user's email address |
> | `transact` | Optional | Permission to sign transactions |

> [tab] Unity
>
> ```csharp theme={null}
> using Immutable.Passport;
>
> public class GameManager : MonoBehaviour
> {
>     private Passport passport;
>
>     async void Start()
>     {
>         // Initialize Passport
>         passport = await Passport.Init(
>             clientId: "YOUR_CLIENT_ID",
>             environment: Immutable.Passport.Model.Environment.SANDBOX,
>             redirectUri: "mygame://callback",
>             logoutRedirectUri: "mygame://logout"
>         );
>
>         // Restore session if available
>         if (await passport.HasCredentialsSaved())
>         {
>             await passport.Login(useCachedSession: true);
>         }
>     }
> }
> ```

> [tab] Unreal
>
> ```cpp theme={null}
> #pragma once
>
> #include "Immutable/ImmutableSubsystem.h"
> #include "Immutable/ImmutablePassport.h"
> #include "PassportQuickStartSubsystem.generated.h"
>
> UCLASS()
> class UPassportQuickStartSubsystem : public UGameInstanceSubsystem
> {
>     GENERATED_BODY()
>
> public:
>     virtual void Initialize(FSubsystemCollectionBase& Collection) override
>     {
>         Super::Initialize(Collection);
>
>         auto Subsystem = Collection.InitializeDependency<UImmutableSubsystem>();
>         Subsystem->WhenReady(this, &UPassportQuickStartSubsystem::OnPassportReady);
>     }
>
> private:
>     void OnPassportReady(TWeakObjectPtr<UImtblJSConnector> JSConnector)
>     {
>         auto Passport = GetPassport();
>         if (!Passport)
>         {
>             UE_LOG(LogTemp, Error, TEXT("Failed to get Passport instance"));
>             return;
>         }
>
>         Passport->Initialize(FImmutablePassportInitData
>         {
>             TEXT("YOUR_CLIENT_ID"),
>             TEXT("mygame://callback"),
>             TEXT("mygame://logout"),
>             TEXT("sandbox"),
>             false
>         }, UImmutablePassport::FImtblPassportResponseDelegate::CreateUObject(this, &UPassportQuickStartSubsystem::OnPassportInitialized));
>     }
>
>     void OnPassportInitialized(FImmutablePassportResult Result)
>     {
>         if (!Result.Success)
>         {
>             UE_LOG(LogTemp, Error, TEXT("Passport Initialization Failed: %s"), *Result.Error);
>             return;
>         }
>
>         UE_LOG(LogTemp, Log, TEXT("Passport Initialized Successfully"));
>     }
>
>     UImmutablePassport* GetPassport() const
>     {
>         if (GetGameInstance())
>         {
>             auto Subsystem = GetGameInstance()->GetSubsystem<UImmutableSubsystem>();
>             if (Subsystem)
>             {
>                 return Subsystem->GetPassport().Get();
>             }
>         }
>         return nullptr;
>     }
> };
> ```

## Login

> [tab] Next.js
>
> The `useLogin` hook provides embedded, popup, and redirect login flows. All functions accept an optional config; when omitted, sandbox defaults are used.
>
> ### Embedded Login
>
> Shows an in-page modal for login method selection — no popup window required:
>
> ```tsx theme={null}
> 'use client';
>
> import { useLogin, useImmutableSession, type LoginConfig } from '@imtbl/auth-next-client';
>
> function LoginButton() {
>   const { loginWithEmbedded, isLoggingIn } = useLogin();
>   const { isAuthenticated } = useImmutableSession();
>
>   const loginConfig: LoginConfig = {
>     clientId: process.env.NEXT_PUBLIC_IMMUTABLE_CLIENT_ID!,
>     redirectUri: `${window.location.origin}/callback`,
>     audience: 'platform_api',
>     scope: 'openid profile email offline_access transact',
>     authenticationDomain: 'https://auth.immutable.com',
>   };
>
>   if (isAuthenticated) return <p>Logged in</p>;
>
>   return (
>     <button onClick={() => loginWithEmbedded(loginConfig)} disabled={isLoggingIn}>
>       {isLoggingIn ? 'Signing in...' : 'Login with Passport'}
>     </button>
>   );
> }
> ```
>
> ### Popup Login
>
> ```tsx theme={null}
> const { loginWithPopup } = useLogin();
>
> await loginWithPopup(loginConfig);
> ```
>
> ### Redirect Login
>
> ```tsx theme={null}
> const { loginWithRedirect } = useLogin();
>
> // Navigates away from the page for OAuth authentication
> await loginWithRedirect(loginConfig);
> ```
>
> ### Direct Login Options
>
> Skip the login method selection screen and go directly to a specific provider:
>
> ```tsx theme={null}
> import { MarketingConsentStatus } from '@imtbl/auth-next-client';
>
> const { loginWithPopup } = useLogin();
>
> // Direct to Google login
> await loginWithPopup(config, {
>   directLoginOptions: {
>     directLoginMethod: 'google',
>     marketingConsentStatus: MarketingConsentStatus.OptedIn,
>   },
> });
>
> // Direct to email login with marketing consent
> await loginWithPopup(config, {
>   directLoginOptions: {
>     directLoginMethod: 'email',
>     email: 'user@example.com',
>     marketingConsentStatus: MarketingConsentStatus.OptedIn,
>   },
> });
> ```
>
> | Option | Description |
> | - | - |
> | `directLoginMethod` | The authentication provider (`email`, `google`, or `apple`) |
> | `email` | Required when `directLoginMethod` is `email` |
> | `marketingConsentStatus` | Marketing consent (`OptedIn` or `Unsubscribed`) |

> [tab] TypeScript
>
> ### Headless Login
>
> For a more streamlined user experience, the standard Passport login prompt can be bypassed by providing the `directLoginOptions` parameter to the `login` method. This allows you to render a customised authentication prompt within your own application.
>
> Once an authentication option (email, Google, Apple, or Facebook) is passed to the `login` method, a popup will be opened so that the user can authenticate securely.
>
> ```typescript theme={null}
> // Headless login with email
> const user = await auth.login({
>   directLoginOptions: {
>     directLoginMethod: 'email',
>     email: 'user@example.com',
>     marketingConsentStatus: 'opted_in'
>   }
> });
>
> // Headless login with specific provider
> const user = await auth.login({
>   directLoginOptions: {
>     directLoginMethod: 'google', // or 'apple', 'facebook'
>     marketingConsentStatus: 'opted_in'
>   }
> });
> ```
>
> | Option | Description |
> | - | - |
> | `directLoginMethod` | The authentication provider (`email`, `google`, `apple`, or `facebook`) |
> | `email` | Required when `directLoginMethod` is `email`, specifies the user's email address |
> | `marketingConsentStatus` | Marketing consent setting (`opted_in` or `unsubscribed`) |
>
> ### Login with EthersJS
>
> Integrates Passport authentication with EthersJS for wallet connection and interaction.
>
> ```typescript theme={null}
> const provider = await connectWallet({ auth });
> const web3Provider = new BrowserProvider(passportProvider);
> const accounts = await web3Provider.send('eth_requestAccounts', []);
> ```
>
> This implementation uses EthersJS's BrowserProvider to interact with the Passport provider. It requests user accounts and manages the connection state, displaying the connected account address when successful.
>
> ### Identity-only Login
>
> The `login` method can be used to log in a user and retrieve their profile information without connecting to Immutable zkEVM.
>
> ```typescript theme={null}
> const profile: User | null = await auth.login();
> ```
>
> This will prompt the user to select an authentication option from within an iFrame, and open a popup to securely complete the authentication process.

> [tab] Unity
>
> ### Embedded Login
>
> The PassportUI prefab provides a seamless, embedded authentication experience that keeps users within your application. This approach offers better user experience and higher conversion rates.
>
> #### Setting up the PassportUI Prefab
>
> | Platform | WebView Package |
> | - | - |
> | Windows | Volt Unity Web Browser (UWB) - no additional packages required |
> | iOS/Android/macOS | Vuplex WebView (paid third-party product) |
>
> **1. Add the PassportUI Prefab to Your Scene**
>
> Choose the appropriate prefab for your target platform:
>
> * `PassportLogin_Windows.prefab` - For Windows builds using UWB
> * `PassportLogin_Vuplex.prefab` - For iOS, Android, and macOS builds using Vuplex
>
> **2. Initialize PassportUI**
>
> ```csharp theme={null}
> public class MyAuthScript : MonoBehaviour
> {
>     public PassportUI passportUI;
>
>     async void Start()
>     {
>         // Initialize with existing Passport instance
>         await passportUI.InitializeWithPassport(Passport.Instance);
>
>         // Or initialize PassportUI with automatic Passport creation
>         // (uses the clientId/environment/URIs set in the Inspector)
>         await passportUI.InitializeWithPassport();
>     }
> }
> ```
>
> **3. Handle Authentication Events**
>
> ```csharp theme={null}
> void Start()
> {
>     // Subscribe to authentication events
>     Passport.Instance.OnAuthEvent += OnAuthEvent;
> }
>
> private void OnAuthEvent(PassportAuthEvent authEvent)
> {
>     switch (authEvent)
>     {
>         case PassportAuthEvent.LoginPKCESuccess:
>             Debug.Log("User logged in successfully");
>             break;
>         case PassportAuthEvent.LogoutPKCESuccess:
>             Debug.Log("User logged out");
>             break;
>         case PassportAuthEvent.ReloginSuccess:
>             Debug.Log("User re-logged in with cached credentials");
>             break;
>     }
> }
> ```
>
> > [!NOTE]
> >
> > For iOS, Android, and macOS, the embedded WebView requires [Vuplex WebView](https://developer.vuplex.com/webview/overview), a paid third-party package. Alternatively, use Standard Login which doesn't require any WebView.
>
> ### Headless Login
>
> For a more streamlined user experience, bypass the standard Passport login prompt by providing the `DirectLoginOptions` parameter:
>
> ```csharp theme={null}
> // Customized login with email
> await passport.Login(directLoginOptions: new DirectLoginOptions(
>     DirectLoginMethod.Email,
>     "user@example.com",
>     MarketingConsentStatus.OptedIn
> ));
>
> // Customized login with specific provider
> await passport.Login(directLoginOptions: new DirectLoginOptions(
>     DirectLoginMethod.Google // or Apple, Facebook
> ));
> ```
>
> | Option | Description |
> | - | - |
> | `DirectLoginMethod` | The authentication provider (`Email`, `Google`, `Apple`, or `Facebook`) |
> | `email` | Required when `DirectLoginMethod` is `Email`, specifies the user's email address |
> | `marketingConsentStatus` | Marketing consent setting (`OptedIn` or `Unsubscribed`) |
>
> ### Standard Login
>
> Use the basic `Login()` method to show the standard Passport login prompt:
>
> ```csharp theme={null}
> await passport.Login();
> ```
>
> This will open an external browser window on desktop or an in-app browser on mobile devices, prompting the user to select an authentication option and complete the authentication process securely.
>
> ### Stored Credentials
>
> Once the gamer is connected to Passport, the SDK will store credentials (access, ID, and refresh tokens). Use `Login(useCachedSession: true)` to re-login using saved credentials.
>
> ```csharp theme={null}
> bool hasCredsSaved = await passport.HasCredentialsSaved();
> if (hasCredsSaved)
> {
>     await passport.Login(useCachedSession: true);
>     // Successfully re-logged into Passport
> }
> ```

> [tab] Unreal
>
> ### Standard Login
>
> After Passport initialization completes, call `PerformLogin()` to authenticate the user.
>
> ```cpp theme={null}
> // Add to your subsystem:
> public:
>     void PerformLogin()
>     {
>         auto Passport = GetPassport();
>         if (!Passport)
>         {
>             return;
>         }
>
>         Passport->Connect
>         (
>             true,
>             UImmutablePassport::FImtblPassportResponseDelegate::CreateUObject(this, &UPassportQuickStartSubsystem::OnLoginComplete)
>         );
>     }
>
> private:
>     void OnLoginComplete(FImmutablePassportResult Result)
>     {
>         if (!Result.Success)
>         {
>             UE_LOG(LogTemp, Error, TEXT("Login Failed: %s"), *Result.Error);
>             return;
>         }
>
>         UE_LOG(LogTemp, Log, TEXT("Login Successful"));
>     }
>
> void OnPassportInitialized(FImmutablePassportResult Result)
> {
>     if (!Result.Success) return;
>
>     UE_LOG(LogTemp, Log, TEXT("Passport Initialized Successfully"));
>     PerformLogin();
> }
> ```
>
> ### Headless Login
>
> For a more streamlined user experience, bypass the standard Passport login prompt by providing the `DirectLoginOptions` parameter. Once an authentication option (email, Google, Apple, or Facebook) is passed, a popup will be opened so that the user can authenticate securely.
>
> ```cpp theme={null}
> // Add to your subsystem:
> public:
>     void PerformHeadlessLogin()
>     {
>         auto Passport = GetPassport();
>         if (!Passport)
>         {
>             return;
>         }
>
>         // Customised login with email
>         Passport->Connect
>         (
>             true,
>             UImmutablePassport::FImtblPassportResponseDelegate::CreateUObject(this, &UPassportQuickStartSubsystem::OnLoginComplete),
>             {.DirectLoginMethod = EImmutableDirectLoginMethod::Email}
>         );
>     }
>
> private:
>     void OnLoginComplete(FImmutablePassportResult Result)
>     {
>         if (!Result.Success)
>         {
>             UE_LOG(LogTemp, Error, TEXT("Login Failed: %s"), *Result.Error);
>             return;
>         }
>
>         UE_LOG(LogTemp, Log, TEXT("Login Successful"));
>     }
>
> void OnPassportInitialized(FImmutablePassportResult Result)
> {
>     if (!Result.Success) return;
>
>     UE_LOG(LogTemp, Log, TEXT("Passport Initialized Successfully"));
>     PerformHeadlessLogin();
> }
> ```
>
> | Option | Description |
> | - | - |
> | `DirectLoginMethod` | The authentication provider (`Email`, `Google`, `Apple`, or `Facebook`) |
> | `Email` | Required when `DirectLoginMethod` is `Email`, specifies the user's email address |
> | `MarketingConsentStatus` | Marketing consent setting (`Opted_In` or `Unsubscribed`) |
>
> ### Standard Login
>
> Use the basic `Login()` method without any parameters to show the standard Passport login prompt:
>
> ```cpp theme={null}
> UImtblConnectionAsyncActions::Login(/* WorldContextObject */ this);
> ```
>
> This will open an external browser window on desktop or an in-app browser on mobile devices, prompting the user to select an authentication option and complete the authentication process securely.
>
> ### Stored Credentials
>
> Once the gamer is connected to Passport, the SDK will store credentials (access, ID, and refresh tokens).
>
> You can use the `HasStoredCredentials` async action to check if the gamer has stored credentials before deciding whether to show login options in your UI.
>
> > [!NOTE]
> >
> > The `HasStoredCredentials` async action is currently Blueprint-compatible only. For C++, use the Passport instance methods directly.

## Handle the Callback

On your redirect URI page, process the authentication callback:

> [tab] Next.js
>
> ```tsx theme={null}
> // app/callback/page.tsx
> 'use client';
>
> import { CallbackPage, type ImmutableAuthConfig } from '@imtbl/auth-next-client';
>
> const config: ImmutableAuthConfig = {
>   clientId: process.env.NEXT_PUBLIC_IMMUTABLE_CLIENT_ID!,
>   redirectUri: `${process.env.NEXT_PUBLIC_BASE_URL}/callback`,
>   audience: 'platform_api',
>   scope: 'openid profile email offline_access transact',
> };
>
> export default function Callback() {
>   return (
>     <CallbackPage
>       config={config}
>       loadingComponent={<p>Completing authentication...</p>}
>     />
>   );
> }
> ```
>
> The `CallbackPage` component handles both redirect and popup flows automatically. For popup logins, it communicates tokens back to the parent window and closes itself.

> [tab] React
>
> ```tsx theme={null}
> // pages/callback.tsx or app/callback/page.tsx
> import { useEffect } from 'react';
> import { useRouter } from 'next/router';
>
> export default function Callback() {
>   const router = useRouter();
>
>   useEffect(() => {
>     async function handleCallback() {
>       try {
>         await auth.loginCallback();
>         router.push('/dashboard');
>       } catch (error) {
>         console.error('Callback error:', error);
>         router.push('/login?error=callback_failed');
>       }
>     }
>     handleCallback();
>   }, []);
>
>   return (
>     <div className="flex items-center justify-center min-h-screen">
>       <p>Completing login...</p>
>     </div>
>   );
> }
> ```

> [tab] Vanilla JS
>
> ```typescript theme={null}
> // callback.ts
> async function handleCallback() {
>   try {
>     await auth.loginCallback();
>     window.location.href = '/dashboard';
>   } catch (error) {
>     console.error('Callback error:', error);
>     window.location.href = '/login?error=callback_failed';
>   }
> }
>
> handleCallback();
> ```

## Get User Information

### User Profile

> [tab] Next.js
>
> ```tsx theme={null}
> 'use client';
>
> import { useImmutableSession } from '@imtbl/auth-next-client';
>
> function UserProfile() {
>   const { session, isAuthenticated } = useImmutableSession();
>
>   if (!isAuthenticated) return null;
>
>   return (
>     <div>
>       <p>User ID: {session?.user?.sub}</p>
>       <p>Email: {session?.user?.email}</p>
>       <p>Wallet: {session?.zkEvm?.ethAddress}</p>
>     </div>
>   );
> }
> ```

> [tab] TypeScript
>
> ```typescript theme={null}
> // Get user info from the ID token
> const userInfo = await auth.getUserInfo();
>
> console.log({
>   userId: userInfo.sub,          // Unique Passport user ID
>   email: userInfo.email,         // If email scope granted
>   emailVerified: userInfo.email_verified,
> });
> ```

> [tab] Unity
>
> ```csharp theme={null}
> var userInfo = await passport.GetUserInfo();
>
> Debug.Log($"User ID: {userInfo.Sub}");
> Debug.Log($"Email: {userInfo.Email}");
> ```

> [tab] Unreal
>
> ```cpp theme={null}
> // Add to your subsystem:
> private:
>     void OnLoginComplete(FImmutablePassportResult Result)
>     {
>         if (!Result.Success)
>         {
>             UE_LOG(LogTemp, Error, TEXT("Login Failed: %s"), *Result.Error);
>             return;
>         }
>
>         UE_LOG(LogTemp, Log, TEXT("Login Successful"));
>
>         auto Passport = GetPassport();
>
>         // Get user email
>         Passport->GetEmail(UImmutablePassport::FImtblPassportResponseDelegate::CreateWeakLambda(this, [this](FImmutablePassportResult EmailResult)
>         {
>             if (EmailResult.Success)
>             {
>                 FString Email = UImmutablePassport::GetResponseResultAsString(EmailResult.Response);
>                 UE_LOG(LogTemp, Log, TEXT("User Email: %s"), *Email);
>             }
>         }));
>
>         // Get user wallet address
>         Passport->GetAddress(UImmutablePassport::FImtblPassportResponseDelegate::CreateWeakLambda(this, [this](FImmutablePassportResult AddressResult)
>         {
>             if (AddressResult.Success)
>             {
>                 FString Address = UImmutablePassport::GetResponseResultAsString(AddressResult.Response);
>                 UE_LOG(LogTemp, Log, TEXT("Wallet Address: %s"), *Address);
>             }
>         }));
>     }
> ```
>
> > [!NOTE]
> >
> > **Note:** The Unreal SDK provides separate methods (`GetEmail`, `GetAddress`) rather than a unified `GetUserInfo()` method. See [Get ID Token](#get-id-token) and [Get Access Token](#get-access-token) sections for retrieving tokens.

## Session Management

### Check If Logged In

> [tab] Next.js
>
> Always use `isAuthenticated` from `useImmutableSession` to determine if a user is logged in.
>
> ```tsx theme={null}
> 'use client';
>
> import { useImmutableSession } from '@imtbl/auth-next-client';
>
> function ProtectedContent() {
>   const { isAuthenticated, isLoading } = useImmutableSession();
>
>   if (isLoading) return <div>Loading...</div>;
>   if (!isAuthenticated) return <div>Please log in</div>;
>
>   return <div>Protected content</div>;
> }
> ```
>
> > [!WARNING]
> >
> > Do not use `!!session` or `status === 'authenticated'` to check auth state. A session object can exist with expired or invalid tokens, and `status` does not account for token-level errors like `RefreshTokenError`.
>
> `isAuthenticated` validates all of the following:
>
> 1. NextAuth reports `'authenticated'` status
> 2. The session object exists
> 3. A valid access token is present in the session
> 4. No session-level error exists (such as `RefreshTokenError`)
>
> It also handles transient states gracefully -- during session refetches (window focus) or manual refreshes (after wallet registration via `getUser(true)`), `isAuthenticated` remains `true` if the user was previously authenticated, preventing UI flicker.
>
> ```tsx theme={null}
> // Correct
> const { isAuthenticated } = useImmutableSession();
> if (!isAuthenticated) return <div>Please log in</div>;
>
> // Incorrect -- session can exist with expired/invalid tokens
> const { session } = useImmutableSession();
> if (!session) return <div>Please log in</div>;
>
> // Incorrect -- status doesn't account for token errors
> const { status } = useImmutableSession();
> if (status !== "authenticated") return <div>Please log in</div>;
> ```

> [tab] TypeScript
>
> ```typescript theme={null}
> async function isLoggedIn(): Promise<boolean> {
>   return auth.isLoggedIn();
> }
> ```

> [tab] Unity
>
> ```csharp theme={null}
> async Task<bool> IsLoggedIn()
> {
>     return await passport.HasCredentialsSaved();
> }
> ```

> [tab] Unreal
>
> ```cpp theme={null}
> // Add to your subsystem:
> public:
>     void CheckStoredCredentials()
>     {
>         auto Passport = GetPassport();
>         if (!Passport)
>         {
>             return;
>         }
>
>         Passport->HasStoredCredentials(UImmutablePassport::FImtblPassportResponseDelegate::CreateUObject(this, &UPassportQuickStartSubsystem::OnCredentialsChecked));
>     }
>
> private:
>     void OnCredentialsChecked(FImmutablePassportResult Result)
>     {
>         if (Result.Success)
>         {
>             UE_LOG(LogTemp, Log, TEXT("Stored Credentials Found - Proceeding to Login"));
>         }
>         else
>         {
>             UE_LOG(LogTemp, Log, TEXT("No Stored Credentials - New Login Required"));
>         }
>     }
> ```

### Get Access Token

For authenticated API calls to your backend:

> [tab] Next.js
>
> `getAccessToken()` returns a guaranteed-fresh access token. If the current token is valid it returns immediately; if expired, it triggers a server-side refresh and blocks until the fresh token is available. Multiple concurrent calls share a single refresh request.
>
> ### SWR Fetcher
>
> ```tsx theme={null}
> import useSWR from 'swr';
> import { useImmutableSession } from '@imtbl/auth-next-client';
>
> function useProfile() {
>   const { getAccessToken, isAuthenticated } = useImmutableSession();
>
>   return useSWR(
>     isAuthenticated ? '/passport-profile/v1/profile' : null,
>     async (path) => {
>       const token = await getAccessToken();
>       const res = await fetch(path, {
>         headers: { Authorization: `Bearer ${token}` },
>       });
>       return res.json();
>     },
>   );
> }
> ```
>
> ### Event Handler
>
> ```tsx theme={null}
> import { useImmutableSession } from '@imtbl/auth-next-client';
>
> function ClaimRewardButton({ questId }: { questId: string }) {
>   const { getAccessToken } = useImmutableSession();
>
>   const handleClaim = async () => {
>     const token = await getAccessToken();
>     await fetch('/v1/quests/claim', {
>       method: 'POST',
>       headers: { Authorization: `Bearer ${token}` },
>       body: JSON.stringify({ questId }),
>     });
>   };
>
>   return <button onClick={handleClaim}>Claim</button>;
> }
> ```
>
> ### Periodic Polling
>
> ```tsx theme={null}
> import useSWR from 'swr';
> import { useImmutableSession } from '@imtbl/auth-next-client';
>
> function ActivityFeed() {
>   const { getAccessToken, isAuthenticated } = useImmutableSession();
>
>   return useSWR(
>     isAuthenticated ? '/v1/activities' : null,
>     async (path) => {
>       const token = await getAccessToken();
>       const res = await fetch(path, {
>         headers: { Authorization: `Bearer ${token}` },
>       });
>       return res.json();
>     },
>     { refreshInterval: 10000 },
>   );
> }
> ```

> [tab] TypeScript
>
> ```typescript theme={null}
> const accessToken = await auth.getAccessToken();
>
> // Use in API requests
> const response = await fetch('/api/user/inventory', {
>   headers: {
>     Authorization: `Bearer ${accessToken}`,
>   },
> });
> ```

> [tab] Unreal
>
> ```cpp theme={null}
> // Add to your subsystem:
> private:
>     void OnLoginComplete(FImmutablePassportResult Result)
>     {
>         if (!Result.Success)
>         {
>             UE_LOG(LogTemp, Error, TEXT("Login Failed: %s"), *Result.Error);
>             return;
>         }
>
>         UE_LOG(LogTemp, Log, TEXT("Login Successful"));
>
>         auto Passport = GetPassport();
>
>         Passport->GetAccessToken(UImmutablePassport::FImtblPassportResponseDelegate::CreateWeakLambda(this, [this](FImmutablePassportResult AccessTokenResult)
>         {
>             if (AccessTokenResult.Success)
>             {
>                 FString AccessToken = UImmutablePassport::GetResponseResultAsString(AccessTokenResult.Response);
>                 // Use access token for authenticated API requests to your backend
>                 UE_LOG(LogTemp, Log, TEXT("Access Token: %s"), *AccessToken);
>             }
>         }));
>     }
> ```

### Token Refresh

#### Automatic Refresh

The server-side JWT callback automatically refreshes tokens when the access token expires. This happens transparently during any session access.

#### Force Refresh

After operations that update user claims on the identity provider (such as zkEVM wallet registration), force a token refresh to get the updated data:

```tsx theme={null}
const { getUser } = useImmutableSession();

async function handleRegistration() {
  // ... after zkEVM registration completes

  const user = await getUser(true);
  console.log("Updated zkEvm:", user?.zkEvm);
}
```

### Get ID Token

The ID token contains user identity claims:

> [tab] Next.js
>
> The ID token is not stored in the session cookie (to stay within CDN header size limits). Use `getUser()` to access it -- the client persists it in `localStorage` automatically.
>
> ```tsx theme={null}
> 'use client';
>
> import { useImmutableSession } from '@imtbl/auth-next-client';
>
> function GetIdToken() {
>   const { getUser } = useImmutableSession();
>
>   async function handleGetToken() {
>     const user = await getUser();
>     console.log('ID Token:', user?.idToken);
>   }
>
>   return <button onClick={handleGetToken}>Get ID Token</button>;
> }
> ```

> [tab] TypeScript
>
> ```typescript theme={null}
> const idToken = await auth.getIdToken();
> // JWT containing user claims (sub, email, etc.)
> ```

> [tab] Unreal
>
> ```cpp theme={null}
> // Add to your subsystem:
> private:
>     void OnLoginComplete(FImmutablePassportResult Result)
>     {
>         if (!Result.Success)
>         {
>             UE_LOG(LogTemp, Error, TEXT("Login Failed: %s"), *Result.Error);
>             return;
>         }
>
>         UE_LOG(LogTemp, Log, TEXT("Login Successful"));
>
>         auto Passport = GetPassport();
>
>         Passport->GetIdToken(UImmutablePassport::FImtblPassportResponseDelegate::CreateWeakLambda(this, [this](FImmutablePassportResult IdTokenResult)
>         {
>             if (IdTokenResult.Success)
>             {
>                 FString IdToken = UImmutablePassport::GetResponseResultAsString(IdTokenResult.Response);
>                 // JWT containing user claims (sub, email, etc.)
>                 FString TruncatedToken = IdToken.Left(50) + TEXT("...");
>                 UE_LOG(LogTemp, Log, TEXT("ID Token: %s"), *TruncatedToken);
>             }
>         }));
>     }
> ```

## Logout

> [tab] Next.js
>
> The `useLogout` hook performs federated logout -- it clears both the local NextAuth session and the upstream Immutable/Auth0 session by redirecting to the logout endpoint. This is important when using social logins like Google: without federated logout, the auth server caches the social session, so users clicking "Login" again would be automatically logged in with the same account instead of being prompted to choose.
>
> ```tsx theme={null}
> 'use client';
>
> import { useLogout, useImmutableSession } from '@imtbl/auth-next-client';
>
> function LogoutButton() {
>   const { logout, isLoggingOut, error } = useLogout();
>   const { isAuthenticated } = useImmutableSession();
>
>   if (!isAuthenticated) return null;
>
>   return (
>     <>
>       <button onClick={() => logout()} disabled={isLoggingOut}>
>         {isLoggingOut ? 'Signing out...' : 'Sign Out'}
>       </button>
>       {error && <p>{error}</p>}
>     </>
>   );
> }
> ```
>
> To customize the logout redirect URI:
>
> ```tsx theme={null}
> const { logout } = useLogout();
>
> await logout({
>   clientId: process.env.NEXT_PUBLIC_IMMUTABLE_CLIENT_ID!,
>   logoutRedirectUri: `${window.location.origin}/logged-out`,
> });
> ```

> [tab] TypeScript
>
> Default behavior - logs out without redirecting.
>
> ```typescript theme={null}
> async function logout() {
>   try {
>     await passportInstance.logout();
>   } catch (error) {
>     console.error('Logout failed:', error);
>   }
> }
> ```
>
> ### Logout with Redirect
>
> To redirect users after logout, configure `logoutMode: 'redirect'` at initialization, then call logout:
>
> ```typescript theme={null}
> // 1. Configure redirect mode
> const passportInstance = new passport.Passport({
>   baseConfig: {
>     environment: Environment.SANDBOX,
>     publishableKey: process.env.NEXT_PUBLIC_PUBLISHABLE_KEY,
>   },
>   clientId: process.env.NEXT_PUBLIC_CLIENT_ID,
>   redirectUri: 'http://localhost:3000/redirect',
>   logoutMode: 'redirect',
>   logoutRedirectUri: 'http://localhost:3000/logout',
>   audience: 'platform_api',
>   scope: 'openid offline_access email transact',
> });
>
> // 2. Call logout - will redirect to logoutRedirectUri
> async function logout() {
>   try {
>     await passportInstance.logout();
>   } catch (error) {
>     console.error('Logout failed:', error);
>   }
> }
> ```

> [tab] Unity
>
> ```csharp theme={null}
> async void Logout()
> {
>     try
>     {
>         // hardLogout: true = clear browser session (default), false = SDK only
>         await Passport.Instance.Logout(hardLogout: true);
>     }
>     catch (Exception ex)
>     {
>         Debug.LogError($"Logout failed: {ex.Message}");
>     }
> }
> ```
>
> > [!NOTE]
> >
> > **Hard vs Soft Logout:**
> >
> > * `hardLogout: true` (default): Clears SDK session and browser cookies (recommended)
> > * `hardLogout: false`: Clears SDK session only; browser session persists

> [tab] Unreal
>
> ```cpp theme={null}
> // Add to your subsystem:
> public:
>     void PerformLogout()
>     {
>         auto Passport = GetPassport();
>         if (!Passport)
>         {
>             return;
>         }
>
>         // DoHardLogout: true = clear browser session, false = SDK only
>         Passport->Logout
>         (
>             true, // DoHardLogout
>             UImmutablePassport::FImtblPassportResponseDelegate::CreateUObject(this, &UPassportQuickStartSubsystem::OnLogoutComplete)
>         );
>     }
>
> private:
>     void OnLogoutComplete(FImmutablePassportResult Result)
>     {
>         if (!Result.Success)
>         {
>             UE_LOG(LogTemp, Error, TEXT("Logout Failed: %s"), *Result.Error);
>             return;
>         }
>
>         UE_LOG(LogTemp, Log, TEXT("Logged Out Successfully"));
>     }
> ```
>
> > [!NOTE]
> >
> > **Hard vs Soft Logout:**
> >
> > * `DoHardLogout = true`: Clears SDK session and browser cookies (recommended)
> > * `DoHardLogout = false`: Clears SDK session only; browser session persists

### Error Handling

**Common Logout Issues:**

| Error | Cause | Solution |
| - | - | - |
| Logout callback timeout | Silent logout page didn't load | Verify logoutRedirectUri is accessible |
| Wallet still connected | Cleanup order wrong | Disconnect wallet before passport.logout() |
| Session persists (Unreal) | Soft logout used | Set DoHardLogout: true |

**Example (TypeScript):**

```typescript theme={null}
try {
  await passportInstance.logout();
} catch (error) {
  console.error('Logout failed:', error);
  // Fallback: clear local state anyway
  clearUserState();
}
```

## Backend JWT Validation

Validate Passport JWTs on your server to secure API endpoints.

### Node.js with jose

```typescript theme={null}
import { createRemoteJWKSet, jwtVerify } from 'jose';

// Create JWKS client (cache this)
const JWKS = createRemoteJWKSet(
  new URL('https://auth.immutable.com/.well-known/jwks.json')
);

export async function validatePassportToken(token: string) {
  try {
    const { payload } = await jwtVerify(token, JWKS, {
      issuer: 'https://auth.immutable.com/',
      audience: 'platform_api',
    });
    
    return {
      valid: true,
      userId: payload.sub,
      email: payload.email,
    };
  } catch (error) {
    return { valid: false, error: error.message };
  }
}
```

### Express Middleware

```typescript theme={null}
import { Request, Response, NextFunction } from 'express';

export async function requireAuth(
  req: Request, 
  res: Response, 
  next: NextFunction
) {
  const authHeader = req.headers.authorization;
  
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Missing token' });
  }
  
  const token = authHeader.slice(7);
  const result = await validatePassportToken(token);
  
  if (!result.valid) {
    return res.status(401).json({ error: 'Invalid token' });
  }
  
  req.user = { id: result.userId, email: result.email };
  next();
}

// Usage
app.get('/api/inventory', requireAuth, (req, res) => {
  const userId = req.user.id;
  // Fetch inventory for this user
});
```

### JWT Claims Reference

| Claim | Type | Description |
| - | - | - |
| `sub` | string | Unique Passport user ID |
| `iss` | string | Always `https://auth.immutable.com/` |
| `aud` | string | Your audience (e.g., `platform_api`) |
| `exp` | number | Expiration timestamp |
| `iat` | number | Issued at timestamp |
| `email` | string | User's email (if scope granted) |
| `email_verified` | boolean | Whether email is verified |

## Error Handling

Handle authentication errors gracefully to provide better user experience:

> [tab] Next.js
>
> Check the session `error` field for token-level issues and use try/catch around `getAccessToken()`:
>
> ```tsx theme={null}
> 'use client';
>
> import { useImmutableSession } from '@imtbl/auth-next-client';
> import { signOut } from 'next-auth/react';
>
> function ProtectedContent() {
>   const { session, isAuthenticated, getAccessToken } = useImmutableSession();
>
>   if (session?.error === 'RefreshTokenError') {
>     return (
>       <div>
>         <p>Your session has expired. Please sign in again.</p>
>         <button onClick={() => signOut()}>Sign Out</button>
>       </div>
>     );
>   }
>
>   if (!isAuthenticated) {
>     return <p>Please sign in to continue.</p>;
>   }
>
>   const handleFetch = async () => {
>     try {
>       const token = await getAccessToken();
>       await fetch('/api/data', {
>         headers: { Authorization: `Bearer ${token}` },
>       });
>     } catch (error) {
>       console.error('Failed to get access token:', error);
>     }
>   };
>
>   return <button onClick={handleFetch}>Fetch Data</button>;
> }
> ```
>
> | Session Error | Description | Action |
> | - | - | - |
> | `"TokenExpired"` | Access token expired | Handled automatically by `getAccessToken()` |
> | `"RefreshTokenError"` | Refresh token invalid | Prompt user to sign in again |

> [tab] TypeScript
>
> ```typescript theme={null}
> import { PassportError, PassportErrorType } from '@imtbl/auth';
>
> async function handleLogin() {
>   try {
>     await auth.login();
>     console.log('Login successful');
>   } catch (error) {
>     if (error instanceof PassportError) {
>       switch (error.type) {
>         case PassportErrorType.AUTHENTICATION_ERROR:
>           console.error('Login failed:', error.message);
>           break;
>         case PassportErrorType.USER_REGISTRATION_ERROR:
>           console.error('User registration failed:', error.message);
>           break;
>         case PassportErrorType.WALLET_CONNECTION_ERROR:
>           console.error('Wallet connection failed:', error.message);
>           break;
>         default:
>           console.error('Unknown error:', error);
>       }
>     }
>     throw error;
>   }
> }
> ```

### Common Error Types

| Error Type | Description | Recommended Action |
| - | - | - |
| `AUTHENTICATION_ERROR` | Login or authentication failed | Ask user to try again or use different provider |
| `USER_REGISTRATION_ERROR` | User registration failed | Check if user already exists or retry |
| `WALLET_CONNECTION_ERROR` | Failed to connect wallet | Retry connection or check network |
| `INVALID_CONFIGURATION` | Invalid Passport configuration | Verify `clientId` and `redirectUri` |

## Next Steps

> [card] [Framework Setup](https://docs.immutable.com/docs/sdks/typescript/overview.md#framework-setup)
>
> Integrate with Next.js, Vite, and other frameworks

> [card] [Wallet Operations](https://docs.immutable.com/docs/products/passport/wallet.md)
>
> Connect wallet, send transactions, and sign messages

> [card] [Architecture](https://docs.immutable.com/docs/products/passport/architecture.md)
>
> Understand the security model


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.