Non fungible tokens(NFT) for user auth in web 2 apps

Non fungible tokens(NFT) for user auth in web 2 apps

Hi there, awesome human on the internet! GM✨! In this blog, we are going to be coding an authentication system that gives users access if they happen to hold an NFT in the wallet they sign in with. This is somewhat an intersection of web2 and web3. In this tutorial, we will be leveraging EVM-compatible blockchains. Below are the pre-requisites to follow along.

  • Basic Solidity.
  • express.js, JWT, and javascript.
  • Familiarity with an EVM-compatible crypto wallet like metamask.

In this article, I'll only be explaining the key aspects of the implementation rather than going deep into each and every line of the code. If you want the complete implementation, please click here

Signed messages

This is the key piece of the puzzle in our implementation. To check if the user who has made a request to the API holds at least one of our NFTs, we need to know their public address but since the blockchain is a public ledger anyone can fraudulently claim that they hold the keys to a public address. We need a way to be able to verify the claim.

That's where the magical cryptographic signature comes into play! A person can sign a message with their private key (without revealing it!) and give the signed message along with the original message to a verifier. Verifier can verify a signature against a message, arrive at the public key, and then derive the address from the public key and be sure of it that signer actually holds private keys to the address they claim to be the owner of.

Signing a message on frontend

The signing step will happen on the client-side with the help of web3 provider injected by any wallet extensions like metamask. To get a user to sign a message, you can use the following snippet

// this function generates the message the frontend needs to sign
const constructMessage = (timestamp: number, address: string) => `
This request will not trigger a blockchain transaction or cost any gas fees.

Your authentication status will reset after 24 hours.

Address:${address}

Timestamp:${timestamp}
`;

// this function returns signature and the original message
const signMessage = async () => {
    const { ethereum } = window; 
    const provider = new ethers.providers.Web3Provider(ethereum);
    const signer = provider.getSigner();

    const timestamp = Math.floor(Date.now() / 1000);
    const address = await signer.getAddress();
    const message = constructMessage(timestamp, address);

    const signature = await signer.signMessage(message);

    return [signature, message]
  };

Notice that the message includes a timestamp. This is used to avoid using the same signature repeatedly. With the signature and message generated in the above function make an api call to the backend.

Verifying the signature on the backend

Once the message is signed and both message and signature passed to the backend, you can verify the signature to arrive at the public address. Below is the code snippet of implementing the verification for auth as an express.js controller function.

// this function verifies signature and returns a JWT for further auth
const login = async (req, res) => {
  const { message, signature } = req.body;

  // extract address timestamp from message
  const [addressElement, timestampElement] = message.split("\n\n").slice("-2");
  const timestamp = timestampElement.split(":")[1];
  const address = addressElement.split(":")[1];

  // verify timestamp not older than 5 minutes
  const now = Math.floor(Date.now() / 1000);
  if (now - timestamp > 300) {
    return res.status(401).json({
      message: "Bro... this time stamp old af",
    });
  }

  const hashMessage = ethers.utils.hashMessage(message);
  const pk = ethers.utils.recoverPublicKey(hashMessage, signature);
  const recoveredAddress = ethers.utils.computeAddress(pk);

  if (recoveredAddress !== address) {
    return res.status(401).json({
      message: "Bro... this signature is not for you",
    });
  }

  // if you plan on just implementing login with ethereum
  // issue your JWT here

  // making a smart contract call to check if the address holds your NFTs
  const nfts = await nfKeyContract.getNftsByAddress(recoveredAddress);
  if (nfts.length === 0) {
    res.status(401).json({
      message: "Buy NFKey to access this resource",
    });
  }

  // construct jwt token
  const accessToken = jwt.sign(
    { address, nftCount: nfts.length },
    process.env.JWT_SECRET,
    {
      expiresIn: "24h",
    }
  );

  res.json({
    jwt: accessToken,
  });
};

NFT smart contract

You can find numerous tutorials on how to create an NFT smart contract with solidity. If you're not familiar with the standard NFT smart contract, I'd suggest you go through the NFT track on buildspace ✨. Apart from the standard contract, I've added a function that returns an array of NFT ids an address holds for our convenience at the later stage.

    function getNftsByAddress(address walletAddress)
        external
        view
        returns (uint256[] memory)
    {
        uint256 nfKeysCount = balanceOf(walletAddress);

        uint256[] memory nfKeys = new uint256[](nfKeysCount);
        uint256 currentIndex = 0;

        for (uint256 i = 0; i < _tokenIds.current(); i++) {
            if (ownerOf(i) == walletAddress) {
                nfKeys[currentIndex] = i;
                currentIndex++;
            }
        }
        return nfKeys;
    }

Of course, this function is not required and you can do the processing on the backend as well but I prefer to do it in the smart contract.


That's all for the article folks. Thanks for reading. I hope this helped you. If I've made any mistakes please let me know in the comments, I'll make sure to fix them. And if you have any questions, make sure to leave them in the comments as well, I'll try to answer them to the best of my abilities. Have a great day!