Skip to main content

Integrating Internet Identity

Intermediate
Tutorial

This guide demonstrates an example of how to integrate Internet Identity authentication into an application. In this example, a user can interact with the application's user interface to log in with their Internet Identity and then make a call to the backend canister's whoami method that returns the principal of the user.

Step 1: Create or open a project.

First, use dfx start to start a local development environment(/docs/building-apps/developer-tools/dfx/dfx-start) (if necessary) and create a new project or open an existing project.

Alternatively, you can open this project in ICP Ninja, a web-based IDE for temporary project deployments, or clone the "Who am I?" sample from GitHub.

Creating a new project

Create a new project with the command:

dfx new internet_identity_app --type=motoko --no-frontend --extras internet-identity
cd internet_identity_app

To use a Rust backend canister, use --type=rust instead.

The --extras flag will add the pullable version of the Internet identity canister to your project. A pullable canister is a canister that provides a public service at a static canister ID. Learn more about pullable canisters.

Using an existing project

If you already have a project that was not created with the extra Internet Identity feature, you can modify the project to use the pullable Internet Identity canister. First, add the canister configuration to the project's dfx.json file:

{
"canisters": {
"backend": {
"main": "backend/app.mo",
"type": "motoko",
"args": "--enhanced-orthogonal-persistence"
},
"frontend": {
"dependencies": ["backend"],
"frontend": {
"entrypoint": "frontend/index.html"
},
"source": ["frontend/dist"],
"type": "assets"
},
"internet_identity": {
"candid": "https://github.com/dfinity/internet-identity/releases/latest/download/internet_identity.did",
"type": "custom",
"specified_id": "rdmx6-jaaaa-aaaaa-aaadq-cai",
"remote": {
"id": {
"ic": "rdmx6-jaaaa-aaaaa-aaadq-cai"
}
},
"wasm": "https://github.com/dfinity/internet-identity/releases/latest/download/internet_identity_dev.wasm.gz"
}
},
"output_env_file": ".env",
"defaults": {
"build": {
"packtool": "mops sources"
}
}
}

Step 2: Create a "Who am I" function.

To add a simple "Who am I?" function, insert the following code into the backend canister's source code file:

import Principal "mo:base/Principal";

persistent actor Whoami {
public query (message) func whoami() : async Principal {
message.caller;
};
};

Step 3: Edit frontend code.

In the frontend canister code, add a method to the App class to call the backend function and initialize the auth client:

import React, { useState, useEffect } from 'react';
import { AuthClient } from '@dfinity/auth-client';
import { createActor } from 'declarations/backend';
import { canisterId } from 'declarations/backend/index.js';

const network = process.env.DFX_NETWORK;
const identityProvider =
network === 'ic'
? 'https://identity.ic0.app' // Mainnet
: 'http://rdmx6-jaaaa-aaaaa-aaadq-cai.localhost:4943'; // Local

// Reusable button component
const Button = ({ onClick, children }) => <button onClick={onClick}>{children}</button>;

const App = () => {
const [state, setState] = useState({
actor: undefined,
authClient: undefined,
isAuthenticated: false,
principal: 'Click "Whoami" to see your principal ID'
});

// Initialize auth client
useEffect(() => {
updateActor();
}, []);

const updateActor = async () => {
const authClient = await AuthClient.create();
const identity = authClient.getIdentity();
const actor = createActor(canisterId, {
agentOptions: {
identity
}
});
const isAuthenticated = await authClient.isAuthenticated();

setState((prev) => ({
...prev,
actor,
authClient,
isAuthenticated
}));
};

const login = async () => {
await state.authClient.login({
identityProvider,
onSuccess: updateActor
});
};

const logout = async () => {
await state.authClient.logout();
updateActor();
};

const whoami = async () => {
setState((prev) => ({
...prev,
principal: 'Loading...'
}));

const result = await state.actor.whoami();
const principal = result.toString();
setState((prev) => ({
...prev,
principal
}));
};

return (
<div>
<h1>Who Am I?</h1>
<div id="info-box" className="info-box">
<div className="info-content">
<p>
<i className="fas fa-info-circle"></i> A <strong>principal</strong> is a unique identifier in the Internet
Computer ecosystem.
</p>
<p>
It represents an entity (user, canister smart contract, or other) and is used for identification and
authorization purposes.
</p>
<p>
In this example, click "Whoami" to find out the principal ID with which you're interacting with the backend.
If you're not signed in, you will see that you're using the so-called anonymous principal, "2vxsx-fae".
</p>
<p>
After you've logged in with Internet Identity, you'll see a longer principal, which is unique to your
identity and the dapp you're using.
</p>
</div>
</div>

{!state.isAuthenticated ? (
<Button onClick={login}>Login with Internet Identity</Button>
) : (
<Button onClick={logout}>Logout</Button>
)}

<Button onClick={whoami}>Whoami</Button>

{state.principal && (
<div>
<h2>Your principal ID is:</h2>
<h4>{state.principal}</h4>
</div>
)}
</div>
);
};

export default App;

Make sure to always create a single AuthClient instance on page load and reuse it within your click handlers.

If you are developing using the Safari web browser, you need to change the value returned for the local development environment to http://localhost:4943?canisterId=<canister_id>.

Install the required npm package:

npm install @dfinity/auth-client

Make sure that all @dfinity/<package> dependencies in package.json are the same version.

Step 4: Deploy the application:

dfx deploy

Open the frontend URL that is returned in the deployment output. Your dapp's frontend will be displayed.

Click the "Login" button. You'll be redirected to the Internet Identity frontend. Since you're running this locally, you will be using a local, non-production Internet Identity. To create one, follow the on-screen steps.

Click the "Who am I" button and it should return your Internet Identity principal:

Your principal is: 5uylz-j7fcd-isj73-gp57f-xwwyy-po2ib-7iboa-fdkdv-nrsam-3bd3r-qqe

The above principal is an example. The principal returned will be different based on the account and the environment where the application and Internet Identity are running.

Local frontend development

When modifying this example's frontend, it is recommended to develop using a local development server instead of using the deployed frontend canister. This is because using a local development server will enable Hot Module Reloading, allowing you to see any modifications made to your frontend instantaneously, rather than having to redeploy the frontend canister to see the changes.

To start a local development server, run npm run start. The output will contain the local address the project is running at, such as 127.0.0.1:4943.

End-to-end testing

To run end-to-end testing for Internet Identity integrations, you can use the Internet Identity Playwright plugin.

To use this plugin, first install Playwright, then install the plugin itself with a package manager:

# Install with npm
npm install --save-dev @dfinity/internet-identity-playwright

# Install with pnpm
pnpm add --save-dev @dfinity/internet-identity-playwright

# Install with yarn
yarn add -D @dfinity/internet-identity-playwright

Import the plugin into your Playwright test file:

e2e/login.spec.ts
import {testWithII} from '@dfinity/internet-identity-playwright';

Then begin writing your tests, such as:

e2e/login.spec.ts
testWithII('should sign-in with a new user', async ({page, iiPage}) => {
await page.goto('/');

await iiPage.signInWithNewIdentity();
});

testWithII('should sign-in with an existing new user', async ({page, iiPage}) => {
await page.goto('/');

await iiPage.signInWithIdentity({identity: 10003});
});

In this test, iiPage represents your application's page that initiates the authentication flow with Internet Identity. By default, the test will look for a button identified by [data-tid=login-button]. This can be customized by configuring your own selector:

e2e/login.spec.ts
const loginSelector = '#login';

testWithII('should sign-in with a new user', async ({page, iiPage}) => {
await page.goto('/');

await iiPage.signInWithNewIdentity({selector: loginSelector});
});

testWithII('should sign-in with an existing new user', async ({page, iiPage}) => {
await page.goto('/');

await iiPage.signInWithIdentity({identity: 10003, selector: loginSelector});
});

If desired, you can have the test wait for Internet Identity to be ready by providing the local development environment URL and the canister ID of your local Internet Identity instance:

e2e/login.spec.ts
testWithII.beforeEach(async ({iiPage, browser}) => {
const url = 'http://127.0.0.1:4943';
const canisterId = 'rdmx6-jaaaa-aaaaa-aaadq-cai';

await iiPage.waitReady({url, canisterId});
});

You can also configure a timeout parameter that indicates how long the function should wait for Internet Identity before failing:

e2e/login.spec.ts
testWithII.beforeEach(async ({iiPage, browser}) => {
const url = 'http://127.0.0.1:4943';
const canisterId = 'rdmx6-jaaaa-aaaaa-aaadq-cai';
const timeout = 30000;

await iiPage.waitReady({url, canisterId, timeout});
});

Once your tests are ready, run them with the command:

npx playwright test

View more details in the plugin's repo.

Resources