Integrating with II
Overview
This guide shows an example of how to integrate Internet Identity into an application by using a simple 'Who am I?' backend canister and a frontend UI that returns the Internet Identity principal of the user who calls the backend's whoami
method.
This project uses the pullable version of the Internet identity canister. A pullable canister is a canister that provides a public service at a static canister ID. To learn more about pullable canisters, please see the documentation.
Prerequisites
Before you start, verify that you have:
- Downloaded and installed
dfx
version 0.14.1 or later. - Node.js v16+.
Step 1: To get started, open a terminal window and create a new project:
- dfx v0.17.0 or newer
- dfx v0.16.1 or older
Start dfx
:
dfx start --clean --background
Use dfx new <project_name>
to create a new project:
dfx new ii_integration
You will be prompted to select the language that your backend canister will use. Select 'Motoko':
? Select a backend language: ›
❯ Motoko
Rust
TypeScript (Azle)
Python (Kybra)
Then, select a frontend framework for your frontend canister. Select 'Vanilla JS':
? Select a frontend framework: ›
SvelteKit
React
Vue
❯ Vanilla JS
No JS template
No frontend canister
Lastly, you can include extra features to be added to your project. Select 'Internet Identity'.
? Add extra features (space to select, enter to confirm) ›
x Internet Identity
⬚ Bitcoin (Regtest)
⬚ Frontend tests
Start dfx
:
dfx start --clean --background
Use dfx new <project_name>
to create a new project:
dfx new ii_integration
As mentioned in the introduction, you'll be using the pullable version of the Internet Identity canister, which uses the dfx deps
workflow. The project's dfx.json
file defines the Internet Identity canister as "type": "pull"
.
Open the dfx.json
file and replace the existing content with the following:
{
"canisters": {
"ii_integration_backend": {
"main": "src/ii_integration_backend/main.mo",
"type": "motoko"
},
"internet_identity" : {
"type": "pull",
"id": "rdmx6-jaaaa-aaaaa-aaadq-cai"
},
"ii_integration_frontend": {
"dependencies": [
"ii_integration_backend"
],
"frontend": {
"entrypoint": "src/ii_integration_frontend/src/index.html"
},
"source": [
"src/ii_integration_frontend/assets",
"dist/ii_integration_frontend/"
],
"type": "assets"
}
},
"defaults": {
"build": {
"args": "",
"packtool": ""
}
},
"output_env_file": ".env",
"version": 1
}
Pull the II canister using dfx deps
:
dfx deps pull
Initialize the canister. You can use the '(null)'
value passed to the init command to use the default values. To do so, run the command:
dfx deps init internet_identity --argument '(null)'
Step 2: For this project, you'll use a simple 'Who am I?' function for the backend canister. Open the src/ii_integration_backend/main.mo
file and replace the existing content with the following:
actor {
public shared (msg) func whoami() : async Principal {
msg.caller
};
};
In this actor, there is a single method that responds with the caller's principal. This will show if you make a request from the application's frontend using an authenticated Internet Identity or an AnonymousIdentity.
Step 3: Install the @dfinity/auth-client package:
npm install @dfinity/auth-client
Step 4: Insert the following code into the src/ii_integration_frontend/src/index.js
file:
/* A simple webapp that authenticates the user with Internet Identity and that
* then calls the whoami canister to check the user's principal.
*/
import { Actor, HttpAgent } from "@dfinity/agent";
import { AuthClient } from "@dfinity/auth-client";
const webapp_id = process.env.WHOAMI_CANISTER_ID;
// The interface of the whoami canister
const webapp_idl = ({ IDL }) => {
return IDL.Service({ whoami: IDL.Func([], [IDL.Principal], ["query"]) });
};
export const init = ({ IDL }) => {
return [];
};
// Autofills the <input> for the II Url to point to the correct canister.
document.body.onload = () => {
let iiUrl;
if (process.env.DFX_NETWORK === "local") {
iiUrl = `http://localhost:4943/?canisterId=${process.env.II_CANISTER_ID}`;
} else if (process.env.DFX_NETWORK === "ic") {
iiUrl = `https://${process.env.II_CANISTER_ID}.ic0.app`;
} else {
iiUrl = `https://${process.env.II_CANISTER_ID}.dfinity.network`;
}
document.getElementById("iiUrl").value = iiUrl;
};
document.getElementById("loginBtn").addEventListener("click", async () => {
// When the user clicks, we start the login process.
// First we have to create and AuthClient.
const authClient = await AuthClient.create();
// Find out which URL should be used for login.
const iiUrl = document.getElementById("iiUrl").value;
// Call authClient.login(...) to login with Internet Identity. This will open a new tab
// with the login prompt. The code has to wait for the login process to complete.
// We can either use the callback functions directly or wrap them in a promise.
await new Promise((resolve, reject) => {
authClient.login({
identityProvider: iiUrl,
onSuccess: resolve,
onError: reject,
});
});
// At this point we're authenticated, and we can get the identity from the auth client:
const identity = authClient.getIdentity();
// Using the identity obtained from the auth client, we can create an agent to interact with the IC.
const agent = new HttpAgent({ identity });
// Using the interface description of our webapp, we create an actor that we use to call the service methods.
const webapp = Actor.createActor(webapp_idl, {
agent,
canisterId: webapp_id,
});
// Call whoami which returns the principal (user id) of the current user.
const principal = await webapp.whoami();
// show the principal on the page
document.getElementById("loginStatus").innerText = principal.toText();
});
Depending on your web browser, you may need to change the http://rdmx6-jaaaa-aaaaa-aaadq-cai.localhost:4943
value:
Chrome, Firefox:
http://<canister_id>.localhost:4943
Safari:
http://localhost:4943?canisterId=<canister_id>
This code does the following:
- Interacts with the backend actor to call the
whoami
method. - Creates an
AuthClient
using the auth-client library that is used to authenticate with Internet Identity. - Retrieves the identity from the
AuthClient
. - Uses the identity to create an agent that interacts with ICP.
- Then, uses the interface description of the app to create an actor that's used to call the app's service methods.
If you used a project name other than ii_integration
, you will need to rename the imports and environment variables in the code.
Step 5: Insert the following code into the src/ii_integration_frontend/src/index.html
file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>greet</title>
<base href="/" />
<link rel="icon" href="favicon.ico" />
<link type="text/css" rel="stylesheet" href="main.css" />
</head>
<body>
<main>
<img src="logo2.svg" alt="DFINITY logo" />
<br />
<br />
<form>
<button id="login">Login!</button>
</form>
<br />
<form>
<button id="whoAmI">Who Am I</button>
</form>
<section id="principal"></section>
</main>
</body>
</html>
This code provides a simple UI for us to interact with our application.
View the repo for this code if you would rather clone the example.
Step 6: Deploy the project:
dfx deps deploy
dfx deploy
Step 8: Navigate to the frontend canister's URL in your web browser. You will see the frontend of the app:
Step 7: Then, select 'Log in'.
You'll be redirected to the II 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.
Step 8: Create a local Internet Identity
- Select 'Create New' from the UI.
- Next, select 'Create Passkey'.
- When prompted, choose how to create your passkey, either on your current device or on another device.
- Then, enter the CAPTCHA to continue.
Your Internet Identity has been created! It'll be shown on the screen, and it is recommended that you write it down in a safe location to save it.
This number is your Internet Identity. With this number and your passkey, you will be able to create and securely connect to Internet Computer dapps. If you lose this number, you will lose any accounts that were created with it. This number is not secret, but is unique to you.
Once you save it, select the 'I saved it, continue' button.
Step 9: Once you are redirected back to the frontend of the app, click the 'Click me!' button.
Step 10: Your Internet Identity's principal ID will be returned:
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:
import {testWithII} from '@dfinity/internet-identity-playwright';
Then begin writing your tests, such as:
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:
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 replica URL and the canister ID of your local Internet Identity instance:
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:
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.