Integrating with II
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.
- Prerequisites
Creating a simple app with II authentication
Step 1: Create or open a project.
First, use dfx start
to start a local instance of the replica (if necessary) and create a new project or open an existing project.
Creating a new project
When creating a new project with the command dfx new
, you will have the option to Add extra features. From this prompt, select Internet Identity.
This extra feature 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": {
"internet_identity": {
"candid": "https://github.com/dfinity/internet-identity/releases/latest/download/internet_identity.did",
"frontend": {},
"remote": {
"id": {
"ic": "rdmx6-jaaaa-aaaaa-aaadq-cai"
}
},
"type": "custom",
"wasm": "https://github.com/dfinity/internet-identity/releases/latest/download/internet_identity_dev.wasm.gz"
}
}
}
Then deploy the canister locally:
dfx deploy
This example's frontend was created based on the Vanilla JS framework option that can be selected when creating a project with dfx new
. If you used another framework or template for your project, you'll need to adapt the code.
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:
- Motoko
- Rust
actor {
public shared query (msg) func whoami() : async Principal {
msg.caller
};
};
#[query]
async fn whoami() -> Principal {
caller()
}
Then, build the project to make the new backend declarations available to the frontend:
dfx build
In the frontend canister code, add a method to the App
class to call the backend function:
/**
* Call the new backend function and render the returned principal to the screen
*/
#handleWhoAmI = async () => {
const principal = await ii_integration_backend.whoami();
this.greeting = `Your principal is: ${principal.toText()}`;
this.#render();
};
Lastly, update the #render
method to add a new "Who am I?" button and a click listener:
#render() {
let body = html`
<main>
<img src="${logo}" alt="DFINITY logo" />
<br />
<br />
<form action="#">
<label for="name">Enter your name: </label>
<input id="name" alt="Name" type="text" />
<button type="submit">Click Me!</button>
</form>
<center>
<button id="whoami">Who am I?</button>
</center>
<section id="greeting">${this.greeting}</section>
</main>
`;
render(body, document.getElementById('root'));
document
.querySelector('form')
.addEventListener('submit', this.#handleSubmit);
document
.querySelector('#whoami')
.addEventListener('click', this.#handleWhoAmI);
}
Deploy the project:
dfx deploy
View the deployed application's frontend. Click the "Who am I?" button, it should return the anonymous principal:
Your principal is: 2vxsx-fae
Step 3: Implement authentication.
Install the required npm
package:
npm install @dfinity/auth-client
Make sure that all @dfinity/<package> dependencies in package.json are the same version.
Add a method to the App
class to get the correct Internet Identity URL:
/**
* Get the correct Internet Identity URL based on the current environment
*/
#identityProvider = () => {
if (process.env.DFX_NETWORK === "local") {
return `http://${process.env.CANISTER_ID_INTERNET_IDENTITY}.localhost:4943`;
} else if (process.env.DFX_NETWORK === "ic") {
return `https://${process.env.CANISTER_ID_INTERNET_IDENTITY}.ic0.app`;
} else {
return `https://${process.env.CANISTER_ID_INTERNET_IDENTITY}.dfinity.network`;
}
};
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>
.
Add a method to create an AuthClient
instance, then call it in the App
class constructor:
constructor() {
this.#render();
await this.#createAuthClient();
}
/**
* Replace backend actor identity with the identity from AuthClient,
* additionally re-render to show the updated authentication status
*/
#onIdentityUpdate = async () => {
Actor.agentOf(ii_integration_backend).replaceIdentity(this.authClient.getIdentity());
this.isAuthenticated = await this.authClient.isAuthenticated();
this.#render();
}
/**
* Create AuthClient, this loads an existing session if available
*/
#createAuthClient = async () => {
this.authClient = await AuthClient.create();
await this.#onIdentityUpdate();
}
Add methods to the App
class to facilitate logging in and logging out:
/**
* Login with AuthClient, this should be called without any delay in a click handler
*/
#handleLogin = async () => {
await new Promise((resolve, reject) => this.authClient.login({
identityProvider: this.#identityProvider(),
onSuccess: resolve,
onError: reject
}));
await this.#onIdentityUpdate();
}
/**
* Logout with AuthClient
*/
#handleLogout = async () => {
await this.authClient.logout();
await this.#onIdentityUpdate();
}
Make sure to always create a single AuthClient
instance on page load and reuse it within your click handlers as shown in the previous and current steps.
Lastly, update the #render
method to add the new "Login" and "Logout" buttons and click listeners:
#render() {
let body = html`
<main>
<img src="${logo}" alt="DFINITY logo" />
<br />
<br />
<form action="#">
<label for="name">Enter your name: </label>
<input id="name" alt="Name" type="text" />
<button type="submit">Click Me!</button>
</form>
<center>
${this.isAuthenticated
? html`<button id="logout">Logout</button>`
: html`<button id="login">Login</button>`}
<button id="whoami">Who am I?</button>
</center>
<section id="greeting">${this.greeting}</section>
</main>
`;
render(body, document.getElementById('root'));
document
.querySelector('form')
.addEventListener('submit', this.#handleSubmit);
document
.querySelector('#whoami')
.addEventListener('click', this.#handleWhoAmI);
document
.querySelector('#login')
?.addEventListener('click', this.#handleLogin);
document
.querySelector('#logout')
?.addEventListener('click', this.#handleLogout);
}
Deploy the updated application:
dfx deploy
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:
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.