In this guide, I will cover step by step the process of adding immutable passport authentication to an applilcation and creating transactions with it.
Before proceeding, Note that this could be done in plain html/javascript as well as all javascript frameworks including svelte, react, vue, etc. For this guide, we would make use of Nextjs.
However, all the core concepts convered here are applicable to all of them.
The follow this guide, ensure you have the following installed
- npm/nodejs
- A Code Editor
Run the following commands on your terminal to get started
git clone https://github.com/Complexlity/immutable-planner-app-starter immutable-planner-app
cd immutable-planner-app
npm install
npm run devOpen http://localhost:3001 in your browser
You can also find a Live Example
Create a new file .env and copy all the contents of .env.example into it.
NEXT_PUBLIC_LOGOUT_URL=<Your Immutable Hub Logout URL>
NEXT_PUBLIC_CALLBACK_URL=<Your Immutable Hub Redirect URL>
NEXT_PUBLIC_CLIENT_ID=<Your Immutable Hub Client Id>We need these three values to connect
- Logout URL
- Callback URL
- Client Id
Follow the steps below to get the required values
- Go to hub.immutable.com and create an account.
- Initialize a project on Immutable zkEvm and a default Environment on Testnet. If you're unsure how to do that, Complete this Quest 3 Guide
- Add A passport Client

- Fill the form provided with the following steps
- Application Type: Web application (remains unchanged). This represents where the application is intented to be run
- Client Name: give your application any name. This is just an identifier.
- Logout URLs: This is very IMPORTANT. It represents the url the user is redirected to after they logout of the application (In some applications,the default landing page). E.g
https://your-site-name.com/. Since we would be runnig the code locally on port3001. Enter http://localhost:3001 into the input box - Callback URLs: Also very IMPORTANT. When you try to login, it opens a popup direct to this url. This is where the logging in takes place. E.g
https://your-site-name.com/login. Since we are runnign the code on our dev server port3001, Enter http://localhost:3001/login into the input box
IMPORTANT: When you deploy, you also have to change these URLs to point to the site address.
Click Create once you have filled these values.
Copy the three values and replace them in the .env file.
In the course of writing this guide, I ran into a bug in the sdk where it looks for the global and process. If you ever encounter errors such as global object missing or process missing, simply add the code above to be run before all others.
if (typeof global === 'undefined') {
window.global = window;
}
if (typeof process === 'undefined') {
window.process = { env: { NODE_ENV: 'production' } };
}The main package that enables all the passport functions is @imtbl/sdk. First we have to install this package into the project.
npm install @imtbl/sdkTo have access to immutable authentication, you have to import functions config and passport which will be used to create a new passport instance object.
//import the needed functions
import { config, passport } from '@imtbl/sdk';
// Initialize the passport config
const passportConfig = {
baseConfig: new config.ImmutableConfiguration({
environment: config.Environment.SANDBOX
}),
// This is the client id obtained from the immutable hub
clientId: process.env.NEXT_PUBLIC_CLIENT_ID,
// This is the callback url obtained from the immutable hub
redirectUri: process.env.CALLBACK_URL,
// This is the logour url obtained from the immutable hub
logoutRedirectUri: process.env.NEXT_PUBLIC_LOGOUT_URL,
audience: 'platform_api',
scope: 'openid offline_access email transact'
};
// Create a new passport instance
const passportInstance = typeof window !== 'undefined' ? new passport.Passport(passportConfig) : undefinedtypeof window === undefined. This is a very important step for bundlers and in our Nextjs use case. This is intended to be run only on the browser so the window object would be undefined on the server.
In the src folder of the project, create a folder store and create a file passportStore.js in the newly created folder and copy the contents below into it
store/passportStore.js
import { createContext, useContext, useState, useReducer } from 'react';
import { config, passport } from '@imtbl/sdk';
const passportConfig = {
baseConfig: new config.ImmutableConfiguration({
environment: config.Environment.SANDBOX
}),
clientId: process.env.NEXT_PUBLIC_CLIENT_ID,
redirectUri: process.env.NEXT_PUBLIC_CALLBACK_URL,
logoutRedirectUri: process.env.NEXT_PUBLIC_LOGOUT_URL,
audience: 'platform_api',
scope: 'openid offline_access email transact'
};
const passportInstance = typeof window !== 'undefined' ? new passport.Passport(passportConfig) : undefined
export const MyContext = createContext();
export function MyProvider({ children }) {
const [passportState] = useState(passportInstance);
return (
<MyContext.Provider value={{ passportState }}>
{children}
</MyContext.Provider>
);
}
export function useMyContext() {
return useContext(MyContext);
}
Also replace the file contents in src/pages/_app.js with the code below
src/pages/_app.js
import '@/styles/globals.css'
import "@/styles/App.css";
import "@/styles/styles.css"
import { MyProvider } from '@/store/passportStore'
export default function App({ Component, pageProps }) {
return(
<MyProvider>
<Component {...pageProps} />
</MyProvider>
)
}
We have created a react context store and put the passport object. This is done so the same passport object is reusable in multiple components (as we would need it). In a different framework, you could as well do something similar though the syntaxes may differ
After initialising the passport object, we can login a user by running the two commands below
const providerZkevm = passportInstance.connectEvm()
const accounts = await providerZkevm.request({ method: "eth_requestAccounts" })First, we create a zkEVM provider. This initializes an object that can be used to interact directly with the blockchain using the details of the passportInstance
Secondly, we call an RPC named eth_requestAccounts. This is what trigger's the entire login process. It returns an array containing the addresses associated with the user
Aside: An RPC (Remote Procedure Call) is simply a defined method provided by the library (in our case) to interact with the ethereum blockchain. In the course of this guide, we would explore some other examples of it
After calling eth_requestAccoutns, a popup opens the Callback Url (In our case, /login)
In this route, we would handle the logging in inside the popup and return the data to the home page
await passportInstance.loginCallback()This is the single line of code used in the /login route and it should be made to be called on page load
- Plain javascript
window.addEventlistener('load',() => {
await passportInstance.loginCallback()
})- React
useEffect(() => {
(async() => {
await passportInstance.logCallback()
})()
})- Svelte
onMount(async () => {
await passportInstance.loginCallback()
});These are some different ways to handle it in different frameworks. The most important thing is to do so on page load
Update src/components/NavBar.jsx to add the login function
src/components/NavBar.jsx
'use client'
import { useMyContext } from "@/store/passportStore";
import Head from "next/head";
import { useState } from 'react';
export default function NavButton() {
const {passportState: passportInstance, userInfo, dispatch } = useMyContext();
const [buttonState, setButtonState] = useState('Connect Passport')
const [isLoading, setIsLoading] = useState(false)
async function login() {
if (!passportInstance) return
setButtonState("...Connecting")
setIsLoading(true)
try {
console.log("I am connecting now")
const providerZkevm = passportInstance.connectEvm()
const accounts = await providerZkevm.request({ method: "eth_requestAccounts" })
// Set the address
dispatch({
type: 'add_user_info',
key: 'address',
value: accounts[0]
})
} catch (error) {
console.log("Something went wrong")
console.log({ error })
setButtonState('Connect Passport')
throw error
} finally {
setIsLoading(false)
}
setButtonState('Connected')
return
}
async function logout() {
// Logout Function Go Here
return
}
return (
<>
<Head>
<title>Immutable Planner App</title>
<meta name="description" content="Generated by create next app" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<div className="fixed flex justify-end px-4 gap-4 top-0 backdrop-blur-md py-4 w-full">
{
buttonState === 'Connected'
?
<>
<p className="px-4 py-2 bg-teal-600 rounded-lg text-gray-200 flex items-center justify-center">{userInfo.email ?? "Hello world"} </p>
<p className="px-4 py-2 bg-teal-600 rounded-lg text-gray-200 flex items-center justify-center">{userInfo.address ?? "Hello world" }</p>
<button onClick={logout} className="bg-red-500 text-grey-800 px-4 py-2 opacity-100 rounded-full text-lg text-gray-100">Logout</button>
</>
: <button disabled={isLoading} className="text-grey-100 px-4 py-2 opacity-100 rounded-full bg-green-500" onClick={login}>
{buttonState}
</button>
}
</div>
</>
);
}
Create a file in src/pages/ and call it login.js. This is where we would handle the loginCallback(). Also note that this url would match the Callback Url we have set in hub.immutable.com while creating the passport client
src/pages/login.js
import { useEffect } from 'react';
import { useMyContext } from '@/store/passportStore';
export default function LoginPage() {
const { passportState: passportInstance, } = useMyContext();
useEffect(() => {
async function handleLoginCallback() {
if (!passportInstance) {
return
}
try {
console.log("login callback");
await passportInstance.loginCallback();
}
catch (err) {
console.error("login callback error", err);
}
}
handleLoginCallback()
}, []);
return (
<div/>
);
}
Update src/store/passportStore.js to store the user details
src/store/passportStore.js
import { createContext, useContext, useState, useReducer } from 'react';
import { config, passport } from '@imtbl/sdk';
const passportConfig = {
baseConfig: new config.ImmutableConfiguration({
environment: config.Environment.SANDBOX
}),
clientId: process.env.NEXT_PUBLIC_CLIENT_ID,
redirectUri: process.env.NEXT_PUBLIC_CALLBACK_URL,
logoutRedirectUri: process.env.NEXT_PUBLIC_LOGOUT_URL,
audience: 'platform_api',
scope: 'openid offline_access email transact'
};
const passportInstance = typeof window !== 'undefined' ? new passport.Passport(passportConfig) : undefined
export const MyContext = createContext();
export function MyProvider({ children }) {
const [passportState] = useState(passportInstance);
const [userInfo, dispatch] = useReducer(reducer, {address: null, email: null, nickname: null, idToken: null, accessToken: null})
function reducer(state, action) {
const key = action.key
const value = action.value
switch (action.type) {
case "add_user_info": {
return {
...state,
[key]: value
}
}
default: return state
}
}
return (
<MyContext.Provider value={{ passportState, userInfo, dispatch }}>
{children}
</MyContext.Provider>
);
}
export function useMyContext() {
return useContext(MyContext);
}
We added a user object to the store. This enables us re-use and update this object in different parts of the codebase without having to recreate it.
After updating the files, test the login functionality now.
The passportInstance object comes with more functions to get the details of the logged in user. These only work if there's user currently signed in.
In your code, ensure to call the eth_requestAccounts function and be sure it doesn't error before trying to fetch the user details
- User's Email and Nickname
const userInfo = await passportInstance.getUserInfo()On success, the returns an object of the shape:
{
email: <user's email>
sub: <A unique identifier of the logged in user>
nickname: <user's nickname>
}You could then de-structure the object to get the nickname and the email.
const email = userInfo.email
const nickname= userInfo.nickname- User's Access Token
Access tokens are used to re-authenticate the user. This value is important so the entire login process is not triggered every time the user reloads the page.
const accessToken = await passportInstance.getAccessToken()- User's Id Token
This is an identifier for immutable passport users.
const idToken = await passportInstance.getIdToken()Now you cold fetch and insert these values on the front end. We would show the user email and eth address on the Navbar while the other details will be shown on the Immutable Widget
Update src/components/NavBar.jsx with the code below
src/components/NavBar.jsx
'use client'
import { useMyContext } from "@/store/passportStore";
import Head from "next/head";
import Script from "next/script";
import { useReducer, useState } from 'react';
export default function NavButton() {
const {passportState: passportInstance, userInfo, dispatch } = useMyContext();
const [buttonState, setButtonState] = useState('Connect Passport')
const [isLoading, setIsLoading] = useState(false)
async function login() {
if (!passportInstance) return
setButtonState("...Connecting")
setIsLoading(true)
try {
console.log("I am connecting now")
const providerZkevm = passportInstance.connectEvm()
const accounts = await providerZkevm.request({ method: "eth_requestAccounts" })
// Set the address
dispatch({
type: 'add_user_info',
key: 'address',
value: accounts[0]
})
// Fetch user details
const user = await passportInstance.getUserInfo()
// Set the email
dispatch({
type: 'add_user_info',
key: 'email',
value: user.email
})
//set the nickname
dispatch({
type: 'add_user_info',
key: 'nickname',
value: user.nickname
})
// Fetch user access token
const accessToken = await passportInstance.getAccessToken()
// set the access token
dispatch({
type: 'add_user_info',
key: 'accessToken',
value: accessToken
})
// Fetch user's id token
const idToken = await passportInstance.getIdToken()
// set the id token
dispatch({
type: 'add_user_info',
key: 'idToken',
value: idToken
})
} catch (error) {
console.log("Something went wrong")
console.log({ error })
setButtonState('Connect Passport')
throw error
} finally {
setIsLoading(false)
}
setButtonState('Connected')
return
}
async function logout() {
// Logout Function Go Here
return
}
return (
<>
<Head>
<title>Immutable Planner App</title>
<meta name="description" content="Generated by create next app" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<div className="fixed flex justify-end px-4 gap-4 top-0 backdrop-blur-md py-4 w-full">
{
buttonState === 'Connected'
?
<>
<p className="px-4 py-2 bg-teal-600 rounded-lg text-gray-200 flex items-center justify-center">{userInfo.email ?? "Hello world"} </p>
<p className="px-4 py-2 bg-teal-600 rounded-lg text-gray-200 flex items-center justify-center">{userInfo.address ?? "Hello world" }</p>
<button onClick={logout} className="bg-red-500 text-grey-800 px-4 py-2 opacity-100 rounded-full text-lg text-gray-100">Logout</button>
</>
: <button disabled={isLoading} className="text-grey-100 px-4 py-2 opacity-100 rounded-full bg-green-500" onClick={login}>
{buttonState}
</button>
}
</div>
</>
);
}
We're showing the user name and email. Also, we now show the logout button when the user is logged in.
Next, we need to populate the immutable widget with the required data. Update src/components/widgets/ImmutableWidget.jsx with the code below
src/components/widgets/ImmutableWidget.jsx
'use client'
import { useMyContext } from "@/store/passportStore";
import { useRef, useState } from 'react';
export default function ImmutableWidget() {
const { passportState: passportInstance, userInfo } = useMyContext()
return (
<div className="min-w-[400px] max-w-[500px] grid gap-4 py-3 overflow-hidden">
<details open>
<summary className="text-white underline text-xl overflow-x-auto max-w-full mb-4">User Details</summary>
<div className="tokens max-w-[500px]">
<details open className="" ><summary>Id Token</summary>{userInfo.idToken ?? ""}</details>
<details open><summary>Access Token</summary>{userInfo.accessToken ?? ""}</details>
<details ><summary>Nickname</summary>{userInfo.nickname ?? "User has no nickname"}</details>
</div>
</details>
</div>
);
}
Now on the page, you should see the use details on the immutable widget
The passportInstance comes with a logout function which when called, logs the user out and redirect the page to the Logout URLs we specified while creating the passport client in hub.immutable.com
Call passportInstance.logout in src/components/NavBar.jsx
src/components/NavBar.jsx
....Rest of the code
async function logout() {
// Logout Function Go Here
await passportInstance.logout();
setButtonState('Connect Passport')
}
...Restof the code
And that's it. We are now able to login and logout the user.
As stated above, we could call other RPC function and interact with the blockchain once the user is signed in. The functions are called on the providerZkevm object and not the passportInstance
Here are some of them
- Get Immutable X Gas Price
const gasPrice = await providerZkevm.request({ method: 'eth_gasPrice' });- Get The Balance In an ETH address
const userBalance = await providerZkevm.request({
method: 'eth_getBalance',
params: [
userInfo.address,
'latest'
]
});- Get Latest Block Number
const latestBlockNumber = await providerZkevm.request({ method: 'eth_blockNumber' });- Get Chain Id
const chainId = await providerZkevm.request({ method: 'eth_chainId' });- Get Transaction By Hash This function fetch the transaction details of any transaction on the Immutable Testnet Explorer
const transaction = await provider.request({
method: 'eth_getTransactionByHash',
params: [
<transaction hash />
]
});Substitute <transaction hash> with any valid transaction on the immutable testnet and it would return it's value.
In our code, this function has been made to download the file as json the the user's computer
Update src/componets/widgets/Immutable.jsx
src/components/widgets/ImmutableWidget.jsx
'use client'
import { useMyContext } from "@/store/passportStore";
import { useRef, useState } from 'react';
export default function ImmutableWidget() {
const { passportState: passportInstance, userInfo } = useMyContext()
const providerZkevm = passportInstance?.connectEvm()
const [isLoading, setIsLoading] = useState(false);
const[gasPrice, setGasPrice] = useState('');
const[userBalance, setUserBalance] = useState('');
const[latestBlockNumber, setLatestBlockNumber] = useState('');
const[chainId, setChainId] = useState('');
async function getGasPrice() {
if (!passportInstance || !userInfo.address) return
setIsLoading(true)
try {
const gasPrice = await providerZkevm.request({ method: 'eth_gasPrice' });
setGasPrice(gasPrice)
} catch (error) {
console.log(error)
}
finally {
setIsLoading(false)
}
}
async function getUserBalance() {
console.log({user: userInfo.address})
if (!passportInstance || !userInfo.address) return
setIsLoading(true)
try {
const userBalance = await providerZkevm.request({
method: 'eth_getBalance',
params: [
userInfo.address,
'latest'
]
});
setUserBalance(userBalance)
} catch (error) {
console.log(error)
}
finally {
setIsLoading(false)
}
}
async function getLatestBlockNumber() {
console.log({address: userInfo.address})
if (!passportInstance || !userInfo.address) return
setIsLoading(true)
try {
const latestBlockNumber = await providerZkevm.request({ method: 'eth_blockNumber' });
setLatestBlockNumber(latestBlockNumber)
} catch (error) {
console.log(error)
}
finally {
setIsLoading(false)
}
}
async function getChainId() {
if (!passportInstance || !userInfo.address) return
setIsLoading(true)
try {
const chainId = await providerZkevm.request({ method: 'eth_chainId' });
setChainId(chainId)
} catch (error) {
console.log(error)
}
finally {
setIsLoading(false)
}
}
async function getTransactionByHash(e) {
e.preventDefault()
let hash = e.target.hash.value
// if (!passportInstance || !userInfo.address) return
setIsLoading(true)
if (!hash) {
// Default hash value if not provided
hash = "0xa0d300ac90e69f3ba6274ca1a712219951b79ba6c0117f538fe16c016a701951"
}
try {
const transaction = await providerZkevm.request({
method: 'eth_getTransactionByHash',
params: [
hash
]
});
// Download file into user's machine as trasaction.json
const blob = new Blob([JSON.stringify(transaction, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'transaction.json';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
setTransactionHash(transaction)
} catch (error) {
console.log(error)
alert("Something went wrong. Please try again")
}
finally {
setIsLoading(false)
}
}
return (
<div className="min-w-[400px] max-w-[500px] grid gap-4 py-3 overflow-hidden">
<details open>
<summary className="text-white underline text-xl overflow-x-auto max-w-full mb-4">User Details</summary>
<div className="tokens max-w-[500px]">
<details open className="" ><summary>Id Token</summary>{userInfo.idToken ?? ""}</details>
<details open><summary>Access Token</summary>{userInfo.accessToken ?? ""}</details>
<details ><summary>Nickname</summary>{userInfo.nickname ?? "User has no nickname"}</details>
</div>
</details>
<details>
<summary className="text-white text-xl underline mb-4">
{isLoading ?
<svg class="animate-spin mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg> : null}
Rpc Methods</summary>
<div className="grid gap-2">
<div className="flex gap-2">
<button disabled={isLoading} onClick={getGasPrice} className="w-full rounded-full px-3 py-1 bg-green-400 hover:bg-green-500">Get Imx Gas Price</button>
<div className='bg-white w-full rounded-sm py-1 px-2 placeholder:text-gray-800 placeholder:italic'>
{gasPrice}
</div>
</div>
<div className="flex gap-2">
<button disabled={isLoading} onClick={getUserBalance} className="w-full rounded-full px-3 py-1 bg-green-400 hover:bg-green-500">Get User Balance</button>
<div className='bg-white w-full rounded-sm py-1 px-2 placeholder:text-gray-800 placeholder:italic'>
{userBalance}
</div>
</div>
<div className="flex gap-2">
<button disabled={isLoading} onClick={getLatestBlockNumber} className="w-full rounded-full px-3 py-1 bg-green-400 hover:bg-green-500">
Get Latest Block Number
</button>
<div className='bg-white w-full rounded-sm py-1 px-2 placeholder:text-gray-800 placeholder:italic'>{latestBlockNumber}</div>
</div>
<div className="flex gap-2">
<button disabled={isLoading} onClick={getChainId} className="w-full rounded-full px-3 py-1 bg-green-400 hover:bg-green-500">Get Chain Id</button>
<div className='bg-white w-full rounded-sm py-1 px-2 placeholder:text-gray-800 placeholder:italic'>{chainId}</div>
</div>
<form onSubmit={getTransactionByHash} className="px-1">
<p className="mx-auto text-white text-center text-xl mb-2 mt-4">
Get Transaction By Hash
</p>
<div className="flex gap-4">
<input type="text" placeholder="hash" name="hash" className="w-full px-2 py-2 rounded-xl" />
<button disabled={isLoading} className=" rounded-full px-3 py-1 bg-green-400 hover:bg-green-500">Send</button>
</div>
</form>
<small className="text-gray-300 text-center"><span className="text-green-400">Tip</span>: You can get example hashed from <a className="underline hover:no-underline text-amber-400 italic" href="https://explorer.testnet.immutable.com/txs" target="_blank">Immutable Explorer</a></small>
</div>
</details>
</div>
)
}
Fully adding all the RPC functions to the immutable widget. We're now able to call all of them once we've logged in using immutable passport
We have seen how powerful the Immutable zkEvm passport is and how easy it is to integrate into any application and interact with the blockchain.
Using the techniques provided by this guide, you could build web application ranging from simple to complex and add the passport authentication in just minutes.
- The Demp Project Full Source Code - Github
- The Demo Project Live - Immutable Planner App
- The Immutable Passport Official Documentation
- The Writer Of this Awesome Guide - Complexlity


