Login with MetaMask Under the Hood

Login with MetaMask Under the Hood

·

5 min read

This article will break down what a typical Web3 login with MetaMask flow is and how it works under the hood. I tried to keep the examples as simple as possible without using third-party libraries. If you plan to implement a login flow in a real app, check out the resources at the end of the article for where to start—this article to meant to give you a general understanding and not a complete implementation.

I also created a small app to demonstrate these concepts. You can see the code on GitHub or try it out here.

The typical sign-in flow with MetaMask has four steps:

  1. Detect the Ethereum provider
  2. Detect the active network
  3. Get the user's account ID
  4. Prove account ownership

D_D Newsletter CTA

1. Detect the Ethereum Provider

The Ethereum provider is a JavaScript object injected into a website. EIP-1193 defines its API. MetaMask's provider is available via window.ethereum. Note that MetaMask provides a way to check if the provider is injected by MetaMask. A full implementation of detecting the provider is here.

Let’s check out a basic implementation:

if (window.ethereum) {
  sendToLog("Provider Detected");

  const { ethereum } = window;

  if (ethereum.isMetaMask) {
    sendToLog("Provider is MetaMask");
  } else {
    sendToLog("Provider is Not MetaMask", "error");
  }
}

2. Detect the Active Network

The app can now send requests to the wallet via the provider. The first request an app will typically send is to ask what network the wallet is connected to. It does so via the eth_chainId method. This method will return the chain id of the network that the user is currently on. Here's a list of standard chain IDs:

Chain IDNetwork
0x1Ethereum Main Network (Mainnet)
0x3Ropsten Test Network
0x4Rinkeby Test Network
0x5Goerli Test Network
0x2aKovan Test Network

Detecting the chain:

ethereum
  .request({ method: "eth_chainId" })
  .then((_chainId) => {
    chainId = _chainId; 
    sendToLog(`Chain ID: ${chainId}`);
  });

The app can also detect when the user changes chains via the chainChanged event like this:

ethereum.on("chainChanged", (_chainId) => {
  window.location.reload();
  });

3. Get the Account ID

The app will then request the current account id via the eth_accounts method. This is the first time you'll see MetaMask ask for permission from the user. MetaMask will ask the user which accounts they want to connect to the site and then ask for permission to view the account, the balance of the account, account activity, and to request transactions to approve.

connect-metamask-1.png

connect-metamask-2.png

You will now see the green circle next to the account in your wallet, indicating that you connected to the website. Being connected to a website means that MetaMask won't ask for this permission again unless you disconnect or the site asks MetaMask to prompt for approval again. Note that this is a security feature of MetaMask and not, as of today, part of the provider specification. Again, other wallets might handle this differently.

connect-metamask-3.png

Getting the account id:

function handleAccountsChnaged(accounts) {
  if (accounts.length === 0) {
    // MetaMask is locked or not connected
    sendToLog("Please connect to MetaMask", "info");
  } else if (accounts[0] !== currentAccount) {
    currentAccount = accounts[0];
    sendToLog(`Connected to account: ${currentAccount}`);
  }
}

ethereum
  .request({ method: "eth_accounts" })
  .then(handleAccountsChnaged)
  .catch((err) => {
    console.error(err);
  });

The app can also detect when the user changes accounts via the accountsChanged event:

//handle user switching accounts
ethereum.on("accountsChanged", (_accounts) => {
  handleAccountsChnaged(_accounts);
});

4. Prove Account Ownership

The app can now see the account id provided, but it still doesn't know if you own that account. This is where singing comes in. Every Ethereum account has a public and private key. Your public key is available to anyone who knows your account id. Your private key is never shared. However, if you sign a message with your private key, anyone with your public key can verify that the message was signed using your private key. This is how the app can prove that you own the account id provided.

In addition to verifying account ownership, an app might include a nonce (number used once) in the message to sign. This is used to protect against replay attacks. If an attacker got hold of a signature without a nonce, they could reuse it to impersonate you.

connect-metamask-4.png

Signing a message with a nonce:

signButton.addEventListener('click', () => {
  if (currentAccount !== null) {

    let nonce = Math.random().toString(20).toString('hex').substring(2);

    let msg = "Sign this message to prove\n"
      + "you have access to this wallet.\n"
      + "This wont cost any Ether.\n\n"
      + "The app will verify your wallet using\n"
      + `this random ID: ${nonce}`;

    ethereum
      .request({
        method: 'personal_sign',
        params: [
          msg,
          currentAccount,
        ],
      })
      .then((response) => {
        sendToLog(`Signature: ${response}`);
        verifySignature(msg, response);
      })
      .catch((err) => {
        if (err.code === 4001) {
          // User rejected the request
          sendToLog("User rejected request to sign", "error");
        } else {
          console.error(err);
        }
      });
  }
});

It is up to the app to decide how it wants to persist the login. Typically, it will set a cookie in your browser with a token validated by the server. This process is beyond the scope of this article but is a critical point in understanding how signing in with MetaMask works. Once the site has verified, you are who you say you are; it most likely uses standard Web2 mechanisms to log you in and keep you logged in. Every app may handle this flow differently.

If you connected to a website on MetaMask, it doesn't necessarily mean you logged into it, and being disconnected doesn't always mean you logged out. However, disconnecting from a site does mean that the site cannot initiate transactions or ask you to sign any messages, which is why it’s a good practice to disconnect from any websites you don't trust or are no longer using.

I hope you've learned more than you ever wanted to know about logging into a site with MetaMask. Please let me know if you have any questions or corrections in the comments.

The best place to start learning more about MetaMask and the sign-in flow is the MetaMask documentation (docs.metamask.io/guide).

D_D Newsletter CTA

References