Authenticating using NFTs

A common use case for NFTs is to act as a login to websites, discord channels or other content. In order to do this, we have to prove 2 things:

  1. A wallet owns an NFT from a given collection

  2. The NFT record hasn't been spent yet.

To authenticate ownership of an unspent NFT, we recommend the following pattern. 1. Request Records from the wallet

const allRecords = await (wallet?.adapter as LeoWalletAdapter).requestRecords(NFTProgramId);
const nftRecords = allRecords.filter((record: any) => && record.spent === false);

We request all of the records for the program and then filter the spent records to get the records representing the NFTs. 2. Request an Execution of the authorize Function

const aleoTransaction = Transaction.createTransaction(
    [nftRecords[0], '101u64'], // This should be a random number in production, generated on the back end of a DApp

const txId = await (wallet?.adapter as LeoWalletAdapter).requestExecution(aleoTransaction);

The authorize function takes two parameters: the NFT record and a nonce . The nonce should be randomly generated on the backend of the application to prevent the user from choosing a specific random number. This prevents this authorization being used in replay attacks. 3. Get the execution

const newStatus = await (wallet?.adapter as LeoWalletAdapter).transactionStatus(txId);
if (newStatus === 'Finalized') {
    const execution = await (window as any).leoWallet.getExecution(

The execution takes some time to generate so on some interval, check the status of the transaction. When it's Finalized, you can get the execution.

In general, it should be faster than a real transaction as no fee proof is generated.

  1. Verify the execution

const pm = new Aleo.ProgramManager();
const aleoVerifyingKey = Aleo.VerifyingKey.fromString(verifyingKey!);
try {
    await pm.verify_execution(execution.execution, NFTProgram, AUTHORIZE_FUNCTION, true, undefined, aleoVerifyingKey);
    alert('Authorization successful!');
} catch (e) {
    alert('Authorization failed :(');

Additionally, you want to ensure the execution.global_state_root exists on chain and that the serial number execution.transitions[0].inputs[0].id does not exist on chain. It's extremely important to verify these two pieces of data. Otherwise, malicious users will be able to authorize using either a fraudulent NFT or an already spent NFT.

Together, these checks ensure that a valid record exists on-chain and it has not been spent all without paying any fees.

Additionally, by saving the serial number of the record, DApps can automatically invalidate the user session if the serial number appears on chain.

Some tips:

  1. To get the verifying key for a given function, you can use the following helper methods:

const ALEO_URL = '';

async function getDeploymentTransaction(programId: string): Promise<any> {
  const response = await fetch(`${ALEO_URL}find/transactionID/deployment/${programId}`);
  const deployTxId = await response.json();
  const txResponse = await fetch(`${ALEO_URL}transaction/${deployTxId}`);
  const tx = await txResponse.json();
  return tx;

export async function getVerifyingKey(programId: string, functionName: string): Promise<string> {
  const deploymentTx = await getDeploymentTransaction(programId);

  const allVerifyingKeys = deploymentTx.deployment.verifying_keys;
  const verifyingKey = allVerifyingKeys.filter((vk: any) => vk[0] === functionName)[0][1][0];
  return verifyingKey;
  1. To check if the serial number has been spent, you can use the following method from the Demox RPC:

var myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");

var raw = JSON.stringify({
  "jsonrpc": "2.0",
  "id": 1,
  "method": "serialNumbers",
  "params": {
    "serialNumbers": [

var requestOptions = {
  method: 'POST',
  headers: myHeaders,
  body: raw,
  redirect: 'follow'

fetch("http://localhost:3000", requestOptions)
  .then(response => response.text())
  .then(result => console.log(result))
  .catch(error => console.log('error', error));
  1. The execution from the authorize function cannot be broadcast successfully. It will fail every time. To safely prevent all broadcasting attempts by potentially malicious DApps, ensure the finalize statement contains an always fail assertion. For example:

finalize authorize(
        // fails on purpose, so that the nft is not burned.
        assert_eq(0u8, 1u8);

Last updated