Prerequisites
Create Passport Client
Set up your Passport client in Immutable Hub to get the credentials needed for authentication.Create Passport Client
Step-by-step guide to creating a Passport client, configuring redirect URIs, and getting your Client ID
- Client ID from your Passport client in Hub
- Publishable Key from your Hub project settings
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). |
Passport uses OpenID Connect (OIDC). Redirect URIs must be exact matches—wildcards aren’t supported for security reasons. Register multiple URIs for different environments (localhost, staging, production).
Installation
Install the Immutable SDK for your platform to get started with Passport:TypeScript
Install via npm or yarn
Next.js
Install via npm for App Router
Unity
Install via Package Manager
Unreal
Install Passport plugin in your Unreal project
Initialize Passport
- Next.js
- TypeScript
- Unity
- Unreal
Create your auth configuration:Create the API route handler:Wrap your app with Set environment variables:For full setup details including server utilities and route protection, see the Next.js integration guide.
// 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,
});
// app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/lib/auth";
export const { GET, POST } = handlers;
SessionProvider:// 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>
);
}
# .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
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 |
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);
}
}
}
#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
- Next.js
- TypeScript
- Unity
- Unreal
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:'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
const { loginWithPopup } = useLogin();
await loginWithPopup(loginConfig);
Redirect Login
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: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) |
Headless Login
For a more streamlined user experience, the standard Passport login prompt can be bypassed by providing thedirectLoginOptions 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.// 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.const provider = await connectWallet({ auth });
const web3Provider = new BrowserProvider(passportProvider);
const accounts = await web3Provider.send('eth_requestAccounts', []);
Identity-only Login
Thelogin method can be used to log in a user and retrieve their profile information without connecting to Immutable zkEVM.const profile: User | null = await auth.login();
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) |
PassportLogin_Windows.prefab- For Windows builds using UWBPassportLogin_Vuplex.prefab- For iOS, Android, and macOS builds using Vuplex
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();
}
}
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;
}
}
For iOS, Android, and macOS, the embedded WebView requires Vuplex WebView, 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 theDirectLoginOptions parameter:// 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 basicLogin() method to show the standard Passport login prompt:await passport.Login();
Stored Credentials
Once the gamer is connected to Passport, the SDK will store credentials (access, ID, and refresh tokens). UseLogin(useCachedSession: true) to re-login using saved credentials.bool hasCredsSaved = await passport.HasCredentialsSaved();
if (hasCredsSaved)
{
await passport.Login(useCachedSession: true);
// Successfully re-logged into Passport
}
Standard Login
After Passport initialization completes, callPerformLogin() to authenticate the user.// 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 theDirectLoginOptions parameter. Once an authentication option (email, Google, Apple, or Facebook) is passed, a popup will be opened so that the user can authenticate securely.// 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 basicLogin() method without any parameters to show the standard Passport login prompt:UImtblConnectionAsyncActions::Login(/* WorldContextObject */ this);
Stored Credentials
Once the gamer is connected to Passport, the SDK will store credentials (access, ID, and refresh tokens).You can use theHasStoredCredentials async action to check if the gamer has stored credentials before deciding whether to show login options in your UI.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:- Next.js
- React
- Vanilla JS
// 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>}
/>
);
}
CallbackPage component handles both redirect and popup flows automatically. For popup logins, it communicates tokens back to the parent window and closes itself.// 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>
);
}
// 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
- Next.js
- TypeScript
- Unity
- Unreal
'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>
);
}
// 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,
});
var userInfo = await passport.GetUserInfo();
Debug.Log($"User ID: {userInfo.Sub}");
Debug.Log($"Email: {userInfo.Email}");
// 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: The Unreal SDK provides separate methods (
GetEmail, GetAddress) rather than a unified GetUserInfo() method. See Get ID Token and Get Access Token sections for retrieving tokens.Session Management
Check If Logged In
- Next.js
- TypeScript
- Unity
- Unreal
Always use
isAuthenticated from useImmutableSession to determine if a user is logged in.'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>;
}
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:- NextAuth reports
'authenticated'status - The session object exists
- A valid access token is present in the session
- No session-level error exists (such as
RefreshTokenError)
getUser(true)), isAuthenticated remains true if the user was previously authenticated, preventing UI flicker.// 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>;
async function isLoggedIn(): Promise<boolean> {
return auth.isLoggedIn();
}
async Task<bool> IsLoggedIn()
{
return await passport.HasCredentialsSaved();
}
// 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:- Next.js
- TypeScript
- Unreal
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
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
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
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 },
);
}
const accessToken = await auth.getAccessToken();
// Use in API requests
const response = await fetch('/api/user/inventory', {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
// 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: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:- Next.js
- TypeScript
- Unreal
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.'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>;
}
const idToken = await auth.getIdToken();
// JWT containing user claims (sub, email, etc.)
// 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
- Next.js
- TypeScript
- Unity
- Unreal
The To customize the logout redirect URI:
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.'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>}
</>
);
}
const { logout } = useLogout();
await logout({
clientId: process.env.NEXT_PUBLIC_IMMUTABLE_CLIENT_ID!,
logoutRedirectUri: `${window.location.origin}/logged-out`,
});
Default behavior - logs out without redirecting.
async function logout() {
try {
await passportInstance.logout();
} catch (error) {
console.error('Logout failed:', error);
}
}
Logout with Redirect
To redirect users after logout, configurelogoutMode: 'redirect' at initialization, then call logout:// 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);
}
}
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}");
}
}
Hard vs Soft Logout:
hardLogout: true(default): Clears SDK session and browser cookies (recommended)hardLogout: false: Clears SDK session only; browser session persists
// 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"));
}
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 |
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
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
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:- Next.js
- TypeScript
Check the session
error field for token-level issues and use try/catch around getAccessToken():'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 |
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
Framework Setup
Integrate with Next.js, Vite, and other frameworks
Wallet Operations
Connect wallet, send transactions, and sign messages
Architecture
Understand the security model