Cover image
13 minute read

One-click Login with Blockchain: A MetaMask Tutorial

Online users are becoming increasingly resistant to traditional email/password registration processes. One-click social logins via Facebook, Google, or GitHub are better, but they come with data privacy trade-offs. This article introduces a one-click, cryptographically-secure login flow using MetaMask, with all data stored on the app’s own back-end.

Online users are becoming increasingly resistant to traditional email/password registration processes. One-click social login functionality via Facebook, Google, or GitHub turns out to be a much more desirable alternative. However, it comes with a trade-off.

Pros of social media login integration:

  • No more cumbersome form-filling.
  • No need to remember yet another username/password pair.
  • The whole process takes seconds instead of minutes.

Cons of social media login integration:

  • Since the user’s information is loaded from external providers, this raises a huge privacy concern on how providers use all this personal data. For example, at the time of writing, Facebook is facing data privacy issues.

This article introduces a new login method to blockchain development: A one-click, cryptographically-secure login flow using the MetaMask extension, with all data stored on our own back end. We call it: “Login with MetaMask”.

A picture being worth a thousand words, here is a demo of the login flow we are going to build:

An animation showing the MetaMask Login Demo.

Looks good? Let’s get started!

How to Use Metamask for a One-Click Login Flow

The basic idea is that it’s cryptographically easy to prove the ownership of an account by signing a piece of data using a private key. If you manage to sign a precise piece of data generated by our back end, then the back end will consider you the owner of that public address. Therefore, we can build a message-signing-based authentication mechanism with a user’s public address as their identifier.

If it doesn’t seem clear, that’s all right, because we’ll explain it bit-by-bit:

Please note that while we will be using tools connected to the Ethereum blockchain (MetaMask, Ethereum public addresses), this login process does not actually need the blockchain: It only needs its cryptography functions. That being said, with MetaMask becoming such a popular extension, now seems a good time to introduce this login flow.

The MetaMask Browser Extension

If you already know what MetaMask is, feel free to skip this section.

MetaMask is a browser plugin, available as the MetaMask Chrome extension or Firefox Add-on. At its core, it serves as an Ethereum wallet: By installing it, you will get access to a unique Ethereum public address, with which you can start sending and receiving ether or tokens.

But MetaMask does something more than an Ethereum wallet. As a browser extension, it can interact with the current webpage you’re browsing. It does so by injecting a JavaScript library called web3.js in every webpage you visit. Once injected, a web3 object will be available via window.web3 in the JavaScript code of this website. To have a look at what this object looks like, just type window.web3 in the Chrome or Firefox DevTools console, if you have MetaMask installed.

Web3.js is a JavaScript interface to the Ethereum blockchain. There are functions to:

  • Get the latest block of the chain (web3.eth.getBlockNumber)
  • Check the current active account on MetaMask (web3.eth.coinbase)
  • Get the balance of any account (web3.eth.getBalance)
  • Send transactions (web3.eth.sendTransaction)
  • Sign messages with the private key of the current account (web3.personal.sign)
  • …and much more

When MetaMask is installed, any front-end code can get access to all these functions, and interact with the blockchain. They are called dapps or DApps (for decentralized apps–sometimes even styled “ĐApps”).

Most functions in web3.js are read functions (get block, get balance, etc.), and web3 will give the response immediately. However, some functions (like web3.eth.sendTransaction and web3.personal.sign) need the current account to sign some data with its private key. These functions trigger MetaMask to show a confirmation screen, to double-check that the user knows what she or he is signing.

Let’s see how to use MetaMask for this. To make a simple test, paste the following line in the DevTools console:

web3.personal.sign(web3.fromUtf8("Hello from Toptal!"), web3.eth.coinbase, console.log);

This command means: Sign my message, converted from utf8 to hex, with the coinbase account (i.e. current account), and as a callback, print the signature. A MetaMask popup will appear, and if you sign it, the signed message will be printed.

MetaMask confirmation popup

We will be using web3.personal.sign in our login flow.

A final note about this section: MetaMask injects web3.js into your current browser, but there are actually other standalone browsers which also inject web3.js, like Mist, for example. However, in my opinion, MetaMask offers today the best UX and simplest transition for regular users to explore dapps.

How the Login Flow Works

Let’s start with the how. The how will hopefully convince you that it’s secure, so I’ll keep the why part short.

As stated in the overview, we will forget about the blockchain. We have a traditional Web 2.0 client-server RESTful architecture. We will make one assumption: That all users visiting our front-end web page have MetaMask installed. With this assumption, we will show how a passwordless cryptographically-secure login flow works.

Step 1: Modify the User Model (Back-end)

First of all, our User model needs to have two new required fields: publicAddress and nonce. Additionally, publicAddress needs to be unique. You can keep the usual username, email, and password fields—especially if you want to implement your MetaMask login parallely to an email/password login—but they are optional.

The signup process will also slightly differ, as publicAddress will be a required field on signup, if the user wishes to use a MetaMask login. Rest assured, the user will never need to type their publicAddress manually, since it can be fetched via web3.eth.coinbase.

Step 2: Generate Nonces (Back-end)

For each user in the database, generate a random string in the nonce field. For example, nonce can be a big random integer.

Step 3: User Fetches Their Nonce (Front-end)

In our front-end JavaScript code, assuming MetaMask is present, we have access to window.web3. We can therefore call web3.eth.coinbase to get the current MetaMask account’s public address.

When the user clicks on the login button, we fire an API call to the back end to retrieve the nonce associated with their public address. Something like a route with a filter parameter GET /api/users?publicAddress=${publicAddress} should do. Of course, since this is an unauthenticated API call, the back end should be configured to only show public information (including nonce) on this route.

If the previous request doesn’t return any result, it means that the current public address hasn’t signed up yet. We need to first create a new account via POST /users, passing publicAddress in the request body. On the other hand, if there’s a result, then we store its nonce.

Step 4: User Signs the Nonce (Front-end)

Once the front end receives nonce in the response of the previous API call, it runs the following code:

web3.personal.sign(nonce, web3.eth.coinbase, callback);

This will prompt MetaMask to show a confirmation popup for signing the message. The nonce will be displayed in this popup, so that the user knows she or he isn’t signing some malicious data.

When she or he accepts it, the callback function will be called with the signed message (called signature) as an argument. The front end then makes another API call to POST /api/authentication, passing a body with both signature and publicAddress.

Step 5: Signature Verification (Back-end)

When the back end receives a POST /api/authentication request, it first fetches the user in the database corresponding to the publicAddress given in the request body. In particular it fetches the associated nonce.

Having the nonce, the public address, and the signature, the back end can then cryptographically verify that the nonce has been correctly signed by the user. If this is the case, then the user has proven ownership of the public address, and we can consider her or him authenticated. A JWT or session identifier can then be returned to the front end.

Step 6: Change the Nonce (Back-end)

To prevent the user from logging in again with the same signature (in case it gets compromised), we make sure that the next time the same user wants to log in, she or he needs to sign a new nonce. This is achieved by generating another random nonce for this user and persisting it to the database.

Et voilà! This is how we manage a nonce-signing passwordless login flow.

Why the Login Flow Works

Authentication, by definition, is really only the proof of ownership of an account. If you uniquely identify your account using a public address, then it’s cryptographically trivial to prove you own it.

To prevent the case where a hacker gets hold of one particular message and your signature of it (but not your actual private key), we enforce the message to sign to be:

  1. Provided by the back end, and
  2. Regularly changing

We changed it after each successful login in our explanation, but a timestamp-based mechanism could also be imagined.

Overview of the six steps of the MetaMask login flow.

Let’s Build It Together

In this section, I’ll go through the six steps above, one by one. I’ll show some snippets of code for how we can build this login flow from scratch, or integrate it in an existing back end, without too much effort.

I created a small demo app for the purpose of this article. The stack I’m using is the following:

  • Node.js, Express, and SQLite (via the Sequelize ORM) to implement a RESTful API on the back end. It returns a JWT on successful authentication.
  • React single-page application on the front-end.

I try to use as few libraries as I can. I hope the code is simple enough so that you can easily port it to other tech stacks.

The whole project can be seen in this GitHub repository. A demo is hosted here.

Step 1: Modify the User Model (Back-end)

Two fields are required: publicAddress and nonce. We initialize nonce as a random big number. This number should be changed after each successful login. I also added an optional username field here that the user would be able to change.

const User = sequelize.define('User', {
  nonce: {
    allowNull: false,
    type: Sequelize.INTEGER.UNSIGNED,
    defaultValue: () => Math.floor(Math.random() * 1000000) // Initialize with a random nonce
  publicAddress: {
    allowNull: false,
    type: Sequelize.STRING,
    unique: true,
    validate: { isLowercase: true }
  username: {
    type: Sequelize.STRING,
    unique: true

To make it simple, I set the publicAddress field as lowercase. A more rigorous implementation would add a validation function to check that all addresses here are valid Ethereum addresses.

Step 2: Generate Nonces (Back-end)

This is done in the defaultValue() function in the model definition above.

Step 3: User Fetches Their Nonce (Front-end)

The next step is to add some boilerplate code on the back end to handle CRUD methods on the User model, which we won’t do here.

Switching to the front-end code, when the user clicks on the login button, our handleClick handler does the following:

class Login extends Component {
  handleClick = () => {
    // --snip--
    const publicAddress = web3.eth.coinbase.toLowerCase();

    // Check if user with current publicAddress is already present on back end
      .then(response => response.json())
      // If yes, retrieve it. If no, create it.
        users => (users.length ? users[0] : this.handleSignup(publicAddress))
      // --snip--

  handleSignup = publicAddress =>
    fetch(`${process.env.REACT_APP_BACKEND_URL}/users`, {
      body: JSON.stringify({ publicAddress }),
      headers: {
        'Content-Type': 'application/json'
      method: 'POST'
    }).then(response => response.json());

Here, we are retrieving the MetaMask active account with web3.eth.coinbase. Then we check whether this publicAddress is already present or not on the back end. We either retrieve it, if the user already exists, or if not, we create a new account in the handleSignup method.

Step 4: User Signs the Nonce (Front-end)

Let’s move forward in our handleClick method. We now have in our possession a user given by the back end (be it retrieved or newly created). In particular, we have their nonce and publicAddress. So we’re ready to sign the nonce with the private key associated with this publicAddress using web3.personal.sign. This is done in the handleSignMessage function.

Do note that web3.personal.sign takes a hexadecimal representation of the string as its first argument. We need to convert our UTF-8-encoded string to hex format using web3.fromUtf8. Also, instead of signing the nonce only, I decided to sign a more user-friendly sentence, since it will be displayed in the MetaMask confirmation popup: I am signing my once-time nonce: ${nonce}.

class Login extends Component {
  handleClick = () => {
    // --snip--
      .then(response => response.json())
      // If yes, retrieve it. If no, create it.
        users => (users.length ? users[0] : this.handleSignup(publicAddress))
      // Popup MetaMask confirmation modal to sign message
      // Send signature to back end on the /auth route
      // --snip--

  handleSignMessage = ({ publicAddress, nonce }) => {
    return new Promise((resolve, reject) =>
        web3.fromUtf8(`I am signing my one-time nonce: ${nonce}`),
        (err, signature) => {
          if (err) return reject(err);
          return resolve({ publicAddress, signature });

  handleAuthenticate = ({ publicAddress, signature }) =>
    fetch(`${process.env.REACT_APP_BACKEND_URL}/auth`, {
      body: JSON.stringify({ publicAddress, signature }),
      headers: {
        'Content-Type': 'application/json'
      method: 'POST'
    }).then(response => response.json());

When the user has successfully signed the message, we move onto the handleAuthenticate method. We simply send a request to the /auth route on the back end, sending our publicAddress as well as the signature of the message the user just signed.

Step 5: Signature Verification (Back-end)

This is the slightly more complicated part. The back end receives a request on the /auth route containing a publicAddress and a signature, and needs to verify if this publicAddress has signed the correct nonce.

The first step is to retrieve from the database the user with said publicAddress; there is only one because we defined publicAddress as a unique field in the database. We then set the message msg as “I am signing my…”, exactly like in the front end in Step 4, with this user’s nonce.

The next block is the verification itself. There is some cryptography involved. If you feel adventurous I recommend you reading more about elliptic curve signatures.

To summarize this block, what it does is, given our msg (containing the nonce) and our signature, the ecrecover function outputs the public address used to sign the msg. If it matches our publicAddress from the request body, then the user who made the request successfully proved their ownership of publicAddress. We consider them authenticated.

User.findOne({ where: { publicAddress } })
  // --snip--
  .then(user => {
    const msg = `I am signing my one-time nonce: ${user.nonce}`;

    // We now are in possession of msg, publicAddress and signature. We
    // can perform an elliptic curve signature verification with ecrecover
    const msgBuffer = ethUtil.toBuffer(msg);
    const msgHash = ethUtil.hashPersonalMessage(msgBuffer);
    const signatureBuffer = ethUtil.toBuffer(signature);
    const signatureParams = ethUtil.fromRpcSig(signatureBuffer);
    const publicKey = ethUtil.ecrecover(
    const addressBuffer = ethUtil.publicToAddress(publicKey);
    const address = ethUtil.bufferToHex(addressBuffer);

    // The signature verification is successful if the address found with
    // ecrecover matches the initial publicAddress
    if (address.toLowerCase() === publicAddress.toLowerCase()) {
      return user;
    } else {
      return res
        .send({ error: 'Signature verification failed' });

On successful authentication, the back end generates a JWT and sends it back to the client. This is a classic authentication scheme, and the code for integrating JWT with your back end you can find in the repo.

Step 6: Change the Nonce (Back-end)

The final step is to change the nonce, for security reasons. Somewhere after the successful authentication, add this code:

// --snip--
.then(user => {
  user.nonce = Math.floor(Math.random() * 1000000);
// --snip--

It wasn’t so hard, was it? Again, if you want to see how the whole app is wired up (JWT generation, CRUD routes, localStorage, etc.), feel free to have a look at the GitHub repo.

It’s Production-ready Today

While the blockchain may have its flaws and is still in an infant stage, I can’t emphasize enough how this login flow could be implemented on any existing website today. Here’s a list of arguments why this login flow is preferable over both email/password and social logins:

  • Increased security: Proof of ownership by public-key encryption is arguably more secure than proof of ownership by email/password or by a third party—all the more so because MetaMask stores credentials locally on your computer, and not on online servers, which makes the attack surface smaller.
  • Simplified UX: This is a one-click (okay, maybe two-click) login flow, done in a handful of seconds, without the need to type or remember any password.
  • Increased privacy: No email needed, and no third party involved.

Of course, a MetaMask login flow can perfectly well be used in parallel with other traditional login methods. A mapping needs to be done between each account and the public address(es) it holds.

But this login flow is not suited for everyone:

  • Users need to have MetaMask installed: This login flow obviously doesn’t work without MetaMask or a web3-enabled browser. If your audience is not interested in cryptocurrencies, there’s a small chance they would even consider installing MetaMask. With the recent crypto-boom, let’s hope we’re heading towards a Web 3.0 internet.
  • Some work needs to be done on the back end: As we’ve seen, it’s quite straightforward to implement a simple version of this login flow. However, to integrate it into an existing complex system, it requires some changes in all areas that touch authentication: Signup, database, authentication routes, etc. This is especially true because each account will be associated with one or more public addresses.
  • It doesn’t work on mobile: This deserves its own section—read on.

Shortcomings on Mobile

As we have seen, web3 is a prerequisite for this login flow. On desktop browsers, MetaMask injects it. However, there are no extensions on mobile browsers, so this login flow won’t work out-of-the-box on mobile Safari, Chrome, or Firefox. There are some standalone mobile browsers which inject web3—basically MetaMask wrapped up in a browser. They are pretty early-stage as of this writing, but if you are interested, have a look at Cipher, Status, and Toshi. “Login with MetaMask” works with these mobile browsers.

Concerning mobile apps, the answer is yes, the login flow works, but there’s a lot of groundwork to prepare. Basically, you would need to rebuild a simple Ethereum wallet yourself. This includes public address generation, seed word recovery, and secure private key storage, as well as web3.personal.sign and the confirmation popup. Fortunately, there are libraries to help you. The crucial area to focus on is naturally security, as the app itself holds the private key. On desktop browsers, we delegated this task to MetaMask.

So I would argue that the short answer is no, this login flow does not work on mobile today. Effort is being put in this direction, but the easy solution today remains a parallel traditional login method for mobile users.

Let Your Users Login with MetaMask

We introduced in this article a one-click, cryptographically-secure login flow, with no third party involved, called “Login with MetaMask”. We explained how a digital signature of a back end-generated random nonce can prove ownership of an account, and therefore provide authentication. We also explored the trade-offs of this login mechanism compared to traditional email/password or social logins, both on desktop and on mobile.

Even though the target audience of such a login flow is still small today, I sincerely hope that some of you feel inspired to offer Login with MetaMask in your own web app, in parallel to traditional login flows—and I would love to hear about it if you do. If you have any questions, feel free to get in touch in the comments below.

Further Reading on the Toptal Engineering Blog: