Login with MetaMask Under the Hood
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:
- Detect the Ethereum provider
- Detect the active network
- Get the user's account ID
- Prove account ownership
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 ID | Network |
0x1 | Ethereum Main Network (Mainnet) |
0x3 | Ropsten Test Network |
0x4 | Rinkeby Test Network |
0x5 | Goerli Test Network |
0x2a | Kovan 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.
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.
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.
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 (https://docs.metamask.io/guide/).
References
- MetaMask Login Flow Demo App (https://github.com/m57c/metamask-connect-demo)
- MetaMask Documentation (https://docs.metamask.io/guide/)
- EIP-1193 (https://eips.ethereum.org/EIPS/eip-1193)
- MetaMask Detect Provider Utility (https://github.com/MetaMask/detect-provider)
- MetaMask Tutorial: One-click Login With Blockchain Made Easy (https://www.toptal.com/ethereum/one-click-login-flows-a-metamask-tutorial)