> ## 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 title="Create Passport Client" icon="id-card" href="/docs/products/hub/passport-clients">
  Step-by-step guide to creating a Passport client, configuring redirect URIs, and getting your Client ID
</Card>

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

<Info>
  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).
</Info>

For complete details on client configuration, see [Passport Clients in Hub](/docs/products/hub/passport-clients).

## Installation

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

<CardGroup cols={4}>
  <Card title="TypeScript" icon="code" href="/docs/sdks/typescript/overview">
    Install via npm or yarn
  </Card>

  <Card title="Next.js" icon="react" href="/docs/sdks/typescript/overview">
    Install via npm for App Router
  </Card>

  <Card title="Unity" icon="unity" href="/docs/sdks/unity/overview">
    Install via Package Manager
  </Card>

  <Card title="Unreal" icon="gamepad" href="/docs/sdks/unreal/overview">
    Install Passport plugin in your Unreal project
  </Card>
</CardGroup>

## Initialize Passport

<Tabs>
  <Tab title="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](/docs/sdks/typescript/overview#nextjs-server-utilities).
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    import { Auth, AuthConfiguration } from '@imtbl/auth';
    import { Environment } from '@imtbl/config';

    export const auth = new Auth(new AuthConfiguration({
      environment: Environment.SANDBOX,
      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>

  <Tab title="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>

  <Tab title="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;
        }
    };
    ```
  </Tab>
</Tabs>

## Login

<Tabs>
  <Tab title="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>

  <Tab title="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>

  <Tab title="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;
        }
    }
    ```

    <Info>
      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.
    </Info>

    ### 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>

  <Tab title="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.

    <Info>
      The `HasStoredCredentials` async action is currently Blueprint-compatible only. For C++, use the Passport instance methods directly.
    </Info>
  </Tab>
</Tabs>

## Handle the Callback

On your redirect URI page, process the authentication callback:

<Tabs>
  <Tab title="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>

  <Tab title="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>

  <Tab title="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();
    ```
  </Tab>
</Tabs>

## Get User Information

### User Profile

<Tabs>
  <Tab title="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>

  <Tab title="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>

  <Tab title="Unity">
    ```csharp theme={null}
    var userInfo = await passport.GetUserInfo();

    Debug.Log($"User ID: {userInfo.Sub}");
    Debug.Log($"Email: {userInfo.Email}");
    ```
  </Tab>

  <Tab title="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);
                }
            }));
        }
    ```

    <Info>
      **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.
    </Info>
  </Tab>
</Tabs>

## Session Management

### Check If Logged In

<Tabs>
  <Tab title="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`.
    </Warning>

    `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>

  <Tab title="TypeScript">
    ```typescript theme={null}
    async function isLoggedIn(): Promise<boolean> {
      return auth.isLoggedIn();
    }
    ```
  </Tab>

  <Tab title="Unity">
    ```csharp theme={null}
    async Task<bool> IsLoggedIn()
    {
        return await passport.HasCredentialsSaved();
    }
    ```
  </Tab>

  <Tab title="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"));
            }
        }
    ```
  </Tab>
</Tabs>

### Get Access Token

For authenticated API calls to your backend:

<Tabs>
  <Tab title="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>

  <Tab title="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>

  <Tab title="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);
                }
            }));
        }
    ```
  </Tab>
</Tabs>

### 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:

<Tabs>
  <Tab title="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>

  <Tab title="TypeScript">
    ```typescript theme={null}
    const idToken = await auth.getIdToken();
    // JWT containing user claims (sub, email, etc.)
    ```
  </Tab>

  <Tab title="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);
                }
            }));
        }
    ```
  </Tab>
</Tabs>

## Logout

<Tabs>
  <Tab title="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>

  <Tab title="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>

  <Tab title="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}");
        }
    }
    ```

    <Info>
      **Hard vs Soft Logout:**

      * `hardLogout: true` (default): Clears SDK session and browser cookies (recommended)
      * `hardLogout: false`: Clears SDK session only; browser session persists
    </Info>
  </Tab>

  <Tab title="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"));
        }
    ```

    <Info>
      **Hard vs Soft Logout:**

      * `DoHardLogout = true`: Clears SDK session and browser cookies (recommended)
      * `DoHardLogout = false`: Clears SDK session only; browser session persists
    </Info>
  </Tab>
</Tabs>

### 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:

<Tabs>
  <Tab title="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>

  <Tab title="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;
      }
    }
    ```
  </Tab>
</Tabs>

### 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

<CardGroup cols={3}>
  <Card title="Framework Setup" icon="code" href="/docs/sdks/typescript/overview#framework-setup">
    Integrate with Next.js, Vite, and other frameworks
  </Card>

  <Card title="Wallet Operations" icon="wallet" href="/docs/products/passport/wallet">
    Connect wallet, send transactions, and sign messages
  </Card>

  <Card title="Architecture" icon="sitemap" href="/docs/products/passport/architecture">
    Understand the security model
  </Card>
</CardGroup>
