Keyless Integration Guide
At a high level, there are three steps to follow in order to integrate Keyless Accounts.
- Configure your OpenID integration with your IdP. In this step, the dApp will register with the IdP of choice (e.g. Google) and receive a
client_id - Install the Aptos TypeScript SDK.
- Integrate Keyless Account support in your application client
- Set up the
"Sign In with [Idp]"flow for your user. - Instantiate the user’s
KeylessAccount - Sign and submit transactions via the
KeylessAccount.
- Set up the
Example Implementation
Section titled “Example Implementation”You can find an example app demonstrating basic Keyless integration with Google in the aptos-keyless-example repository. Follow the directions in the README to start with the example. For more detailed instructions on keyless, please read the rest of this integration guide.
-
Step 1. Configure your OpenID integration with your IdP
The first step is to setup the configuration with your IdP(s).
-
Step 2. Install the Aptos TypeScript SDK
Terminal window # Keyless is supported in version 1.18.1 and abovepnpm install @aptos-labs/ts-sdk -
Step 3. Client Integration Steps
Below are the default steps for a client to integrate Keyless Accounts
1. Present the user with a “Sign In with [IdP]” button on the UI
Section titled “1. Present the user with a “Sign In with [IdP]” button on the UI”-
In the background, we create an ephemeral key pair. Store this in local storage.
import {EphemeralKeyPair} from '@aptos-labs/ts-sdk';const ephemeralKeyPair = EphemeralKeyPair.generate(); -
Save the
EphemeralKeyPairin local storage, keyed by itsnonce.// This saves the EphemeralKeyPair in local storagestoreEphemeralKeyPair(ephemeralKeyPair);
Example implementation for
storeEphemeralKeyPair/*** Store the ephemeral key pair in localStorage.*/export const storeEphemeralKeyPair = (ekp: EphemeralKeyPair): void =>localStorage.setItem("@aptos/ekp", encodeEphemeralKeyPair(ekp));/*** Retrieve the ephemeral key pair from localStorage if it exists.*/export const getLocalEphemeralKeyPair = (): EphemeralKeyPair | undefined => {try {const encodedEkp = localStorage.getItem("@aptos/ekp");return encodedEkp ? decodeEphemeralKeyPair(encodedEkp) : undefined;} catch (error) {console.warn("Failed to decode ephemeral key pair from localStorage",error);return undefined;}};/*** Stringify the ephemeral key pairs to be stored in localStorage*/export const encodeEphemeralKeyPair = (ekp: EphemeralKeyPair): string =>JSON.stringify(ekp, (_, e) => {if (typeof e === "bigint") return { __type: "bigint", value: e.toString() };if (e instanceof Uint8Array)return { __type: "Uint8Array", value: Array.from(e) };if (e instanceof EphemeralKeyPair)return { __type: "EphemeralKeyPair", data: e.bcsToBytes() };return e;});/*** Parse the ephemeral key pairs from a string*/export const decodeEphemeralKeyPair = (encodedEkp: string): EphemeralKeyPair =>JSON.parse(encodedEkp, (_, e) => {if (e && e.__type === "bigint") return BigInt(e.value);if (e && e.__type === "Uint8Array") return new Uint8Array(e.value);if (e && e.__type === "EphemeralKeyPair")return EphemeralKeyPair.fromBytes(e.data);return e;});-
Prepare the URL params of the login URL. Set the
redirect_uriandclient_idto your configured values with the IdP. Set thenonceto the nonce of theEphemeralKeyPairfrom step 1.1.const redirectUri = 'https://.../login/callback'const clientId = env.IDP_CLIENT_ID// Get the nonce associated with ephemeralKeyPairconst nonce = ephemeralKeyPair.nonce -
Construct the login URL for the user to authenticate with the IdP. Make sure the
openidscope is set. Other scopes such asemailandprofilecan be set based on your app’s needs.const loginUrl = `https://accounts.google.com/o/oauth2/v2/auth?response_type=id_token&scope=openid+email+profile&nonce=${nonce}&redirect_uri=${redirectUri}&client_id=${clientId}` -
When the user clicks the login button, redirect the user to the
loginUrlthat was created in step 1.4.
2. Handle the callback by parsing the token and create a Keyless account for the user
Section titled “2. Handle the callback by parsing the token and create a Keyless account for the user”-
Once the user completes the login flow, they will be redirected to the
redirect_uriset in step 1. The JWT will be set in the URL as a search parameter in a URL fragment, keyed byid_token. Extract the JWT from thewindowby doing the following:const parseJWTFromURL = (url: string): string | null => {const urlObject = new URL(url);const fragment = urlObject.hash.substring(1);const params = new URLSearchParams(fragment);return params.get('id_token');};// window.location.href = https://.../login/google/callback#id_token=...const jwt = parseJWTFromURL(window.location.href) -
Decode the JWT and get the extract the nonce value from the payload.
import { jwtDecode } from 'jwt-decode';const payload = jwtDecode<{ nonce: string }>(jwt);const jwtNonce = payload.nonce -
Fetch the
EphemeralKeyPairstored in step 1.2. Make sure to validate the nonce matches the decoded nonce and that theEphemeralKeyPairis not expired.const ekp = getLocalEphemeralKeyPair();// Validate the EphemeralKeyPairif (!ekp || ekp.nonce !== jwtNonce || ekp.isExpired() ) {throw new Error("Ephemeral key pair not found or expired");} -
Instantiate the user’s
KeylessAccountDepending on the type of Keyless you are using, follow the instructions below:
- Normal Keyless
import {Aptos, AptosConfig, Network} from '@aptos-labs/ts-sdk';const aptos = new Aptos(new AptosConfig({ network: Network.DEVNET })); // Configure your network hereconst keylessAccount = await aptos.deriveKeylessAccount({jwt,ephemeralKeyPair,});- Federated Keyless
import {Aptos, AptosConfig, Network} from '@aptos-labs/ts-sdk';const aptos = new Aptos(new AptosConfig({ network: Network.DEVNET })); // Configure your network hereconst keylessAccount = await aptos.deriveKeylessAccount({jwt,ephemeralKeyPair,jwkAddress: jwkOwner.accountAddress});
3. Store the KeylessAccount in local storage (Optional)
Section titled “3. Store the KeylessAccount in local storage (Optional)”-
After the account has been derived, store the
KeylessAccountin local storage. This allows the user to return to the application without having to re-authenticate.export const storeKeylessAccount = (account: KeylessAccount): void =>localStorage.setItem("@aptos/account", encodeKeylessAccount(account));export const encodeKeylessAccount = (account: KeylessAccount): string =>JSON.stringify(account, (_, e) => {if (typeof e === "bigint") return { __type: "bigint", value: e.toString() };if (e instanceof Uint8Array)return { __type: "Uint8Array", value: Array.from(e) };if (e instanceof KeylessAccount)return { __type: "KeylessAccount", data: e.bcsToBytes() };return e;}); -
Whenever the user returns back to the application, retrieve the
KeylessAccountfrom local storage and use it to sign transactions.export const getLocalKeylessAccount = (): KeylessAccount | undefined => {try {const encodedAccount = localStorage.getItem("@aptos/account");return encodedAccount ? decodeKeylessAccount(encodedAccount) : undefined;} catch (error) {console.warn("Failed to decode account from localStorage",error);return undefined;}};export const decodeKeylessAccount = (encodedAccount: string): KeylessAccount =>JSON.parse(encodedAccount, (_, e) => {if (e && e.__type === "bigint") return BigInt(e.value);if (e && e.__type === "Uint8Array") return new Uint8Array(e.value);if (e && e.__type === "KeylessAccount")return KeylessAccount.fromBytes(e.data);return e;});
4. Submit transactions to the Aptos blockchain
Section titled “4. Submit transactions to the Aptos blockchain”-
Create the transaction you want to submit. Below is a simple coin transfer transaction for example:
import {Account} from '@aptos-labs/ts-sdk';const bob = Account.generate();const transaction = await aptos.transferCoinTransaction({sender: keylessAccount.accountAddress,recipient: bob.accountAddress,amount: 100,}); -
Sign and submit the transaction to the chain.
const committedTxn = await aptos.signAndSubmitTransaction({ signer: keylessAccount, transaction }); -
Wait for the transaction to be processed on-chain
const committedTransactionResponse = await aptos.waitForTransaction({ transactionHash: committedTxn.hash });
-