Santhosh
Santhosh
Code with passion
Mar 12, 2022 4 min read

Login with crypto wallets - Metamask

Metamask is one of the most used cryptographic wallets. It’s used to store your cryptographic assets like NFT, bitcoin etc.

Today we’ll see how to build a secure “login with metamask” workflow. The benefit of keeping this option in your website is that users can easily login and create their account as just like any other login method but with the benefit of not sharing any personal information.

The flow will look like:

sequence-diagram-metamask-login

  1. User clicks on “Login With Metamask” button.
  2. Metamask initiates the connection request
  3. After connection, React App calls api with the wallet address and get back the nonce to sign.
  4. Using ethersjs library we can call Metamask and initiates a message signing with underlying web3 layer. Metamask prompts asking for sign.
  5. The signed message payload is shared to backend
  6. Backend API logic uses ethersjs verifySignature to verify signature and get the wallet address, compare it with the data and then generates JWT token.

End State:

Demo

Prerequisites:

  • Sample Web Client App with Web3 - React App with ethers.js package
  • Backend Server with web3 to verify login - Fastify in glitch for easy tryout.

Let’s Build this!

  1. Configure React app to sign ethersjs

Create react app

create-react-app metamask-login --template typescript

install ethers following their getting started docs.

npm install ethers
  1. Create the login button and signature code:

    • IF metamask is installed, then window.ethereum will be defined and can be used to interact with metamask. On click of login button, we’ll run the following code:
        const provider = new ethers.providers.Web3Provider(window.ethereum)
        await provider.send("eth_requestAccounts", []);
    

This will request for eth account details to metamask and initiate a connection request. Once user connects rest of the code will execute.

    const signer = provider.getSigner()
    const walletAddress = await signer.getAddress();

This will get the walletAddress and the signer instance, which we can use later to generate the signature.

  1. Call api to get user account nonce from backend
 const nonceResponse = await fetch('https://rapid-striped-advantage.glitch.me/api/metamask/login', {
      method: 'Post',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({walletAddress})
    });
    const {nonce} = await nonceResponse.json();

Hit the api endpoint and get the nonce which we’ll use to generate the signature. We’ll look at the processing of /api/metamask/login API endpoint side in next step.

  1. Process the sign-in request:

Meanwhile in the backend land,

const users = {};
fastify.post("/api/metamask/login", (req,rep) => {
    const walletAddress = req.body.walletAddress;
    if(!walletAddress) {
      rep.code(400);
      rep.send('Bad Request');
      return;
    }
    if(!!users[walletAddress]) {
      rep.send({nonce: users[walletAddress].nonce});
      return;
    }
    const nonce = generateNounce();
    users[walletAddress] = {walletAddress, nonce};
    rep.send({nonce});
});

We’re keeping a users object to represent database tables.

const users = {};

We’ll insert the user data into it keeping the wallet address as the key. On login request we check if there’s already user present in the object with the same wallet address if the user is present then give back the nonce in the record.

 if(!!users[walletAddress]) {
      rep.send({nonce: users[walletAddress].nonce});
      return;
 }

If the user/wallet address is not present we generate nonce and insert into the object, and give it back to the user.

code to generate random string nonce

const generateNounce = () => (Math.random() + 1).toString(36).substring(7);

create nonce and respond:

    const nonce = generateNounce();
    users[walletAddress] = {walletAddress, nonce};
    rep.send({nonce});
  1. In UI, receive the nonce and sign it using ethers and send it to backend:
const signature = await signer.signMessage(nonce);
    const loginResponse = await fetch('https://rapid-striped-advantage.glitch.me/api/metamask/verify', {
      method: 'Post',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({walletAddress, signature})
    });
  1. verify the signature and validate. Generate JWT Token.

Post endpoint:

fastify.post("/api/metamask/verify", (req,rep) => {
  const {signature, walletAddress} = req.body;
  const dbUser = users[walletAddress];
  const signerWalletAddress = ethers.utils.verifyMessage(dbUser.nonce, signature);
  
  if (signerWalletAddress !== dbUser.walletAddress) {
     rep.statusCode = 401;
     rep.send('Bad Creds');
     return;
  }
  
  const token = jwt.sign({walletAddress}, 'jwtSecretKey');
  rep.send({token});
//Regenerate nonce and update the old nonce after jwt token generation to 
// prevent replay attack:
 users[walletAddress].nonce = generateNounce();
});
  1. Recieve the token and store in localstorage for calling API in future
 const {token} = await loginResponse.json();
    localStorage.setItem('token', token);
    setToken(token);

You can find the whole backend code here in glitch: https://glitch.com/edit/#!/rapid-striped-advantage?path=server.js%3A1%3A0. you may see some other code as well in the same glitch server.js which was developed as part of this post just ignore

You can find the React App code in https://github.com/santhosh-ps/login-with-metamask

Tryout the live login here: https://metamask-login.netlify.app/ (install the metamask plugin first)