How to implement a sign in with Ethereum wallet flow Web3 JS

In this guide, we are going to learn how to implement an authentication system that allows users to log in using an Ethereum wallet with Web3 JS.

In this guide, we are going to learn how to implement an authentication system without using passwords that allows users to log in using an Ethereum wallet with Web3 JS.

Sign in with Ethereum or signing in with a wallet on another blockchain is getting more and more popularity in Web3 and the reason is that you don't need to manage passwords and it improves the user experience because you can create password-less one-click authentication flows!

In short, here's how it works:

  • allow users to connect a wallet
  • make users sign a piece of data that changes every time with their wallet to prove they have access to it
  • verify that the signature was generated with the correct address, the address of the user

And here is how it's going to work technically:

  1. The user signs up and connects his wallet. We save his wallet address in the User model along with other information like the name and email.
  2. On the log in screen, the user connects his wallet
  3. After the user connected his wallet on the log in screen, we ask him to sign a random string with his wallet. That string was generated on the back-end specifically for the connected wallet.
  4. After the user signs the data we sent, we verify on the back-end that the signature was generated with an address that corresponds with a User
  5. If the address that generated the signature is linked with a User that means the user was able to authenticate with his wallet so we allow the user to log in
  6. If it does not correspond with any User then we deny access

To allow users to log in with a wallet, you'll need to make some adjustments on the back-end first and then implement the feature on the front-end. We're going to see both here.

Configuring your model

You'll need to add 2 things to your user model:

  • The nonce, which is a random string that changes every time the user logs in. That's the data the user is going to sign with his wallet to prove that he owns the wallet.
  • The address of the user's wallet that you will save at the first log in. This field should be unique, you can't have 2 users with the same address.

Creating the sign up endpoint

That endpoint will be like a normal sign up endpoint, you'll need to create a new user and make sure the address that the user sends is unique.

Creating the log in endpoint

You need 2 endpoints to log in:

  1. A GET endpoint to get the nonce that the user needs to sign with their wallet: /api/auth/get-nonce/:address
  2. A GET endpoint to verify the signature that the user generated and return a JWT: /api/auth/verify/:address/:signature

The get-nonce endpoint:

The first endpoint needs to generate a random string that will be unique for the address that was passed in the URL. That's the data the user will sign with his wallet.

If there is no User with the address that was passed, don't even bother to generate the string, just return that the user cannot authenticate with that address.

Otherwise, generate the random string, save it to the User model and return it.

The verify signature endpoint

The second endpoint will verify the signature that is passed in the URL parameters was signed with the address that is passed and that the address corresponds to a User in the database.

First, if the address doesn't correspond to any User in the database, don't even bother to verify and just return a 400.

To verify the signature, you can use Web3 JS:

import Web3 from 'web3';

// Replace URL below with a URL from a blockchain API like Infura or Alchemy
const web3 = new Web3(new Web3.providers.HttpProvider('URL'));

// Verify the signature:
web3.eth.accounts.recover(nonce, signature)
.then((addressThatSignedData) => {
    // verify that addressThatSignedData === the address in the URL
})

In the code above, the recover function takes in parameter the data that was signed and the signature and returns the public address that corresponds to the private key that generated the signature with the data passed.

Also, in that code, nonce comes from the User model that has the address passed in the parameters. It is the string you generated and saved to the model using the get-nonce endpoint we described above.

And signature comes from the URL parameters.

Using the code above, you can check that addressThatSignedData equals the address that was passed in the URL parameters. If yes, then the user is authenticated, otherwise, he is unauthorised.

If the user is authenticated, you'll need to return a JWT containing the user ID.

Implementing the front-end

The first step here is to connect the user wallet. To do that, you can use the method you want. We have a few tutorials on this:

Immediately after the user connected his wallet, you can get the address of the connected account, then query the first endpoint you created, the get-nonce and request a signature.

To get the connected address:

const address = (await web3.eth.getAccounts())[0]

Then, you can send it to your back-end using the fetch function.

Once you received the data to sign, you can request a the user to sign it with his wallet:

web3.eth.personal.sign(nonce, address)
.then((signature) => {
    // send the signature to the back-end
});

When the user accepts to sign, you can immediately send the signature and the address to the back-end, to the second endpoint you created, the verify endpoint.

That endpoint will verify the signature and will authenticate the user or not. Depending on the response from the back-end, you either log the user in or not.

Sending API calls with an authenticated user

Now that your user is authenticated, you need a way to send API calls to your back-end with the authenticated user.

For endpoints that require an authenticated user, your back-end will need to know which user is authenticated, if there is one, to allow or not that user to do a specific action or change data.

For that, you'll have to save the JWT that was returned when logging in, in the local storage on the front-end.

Every time you want to send an API call to the back-end, you send that JWT token in the headers. In the back-end, you will simply decode that JWT and get the user from the ID that is in the token.

Verifying the signature on a smart contract

If you want to verify the signature and authenticate users in a smart contract, follow this tutorial:

How to verify a signature in a smart contract in Solidity
We are going to learn how to verify signatures in a Solidity smart contract and learn how to get the address that generated the signature.

And that's it 🎉

Thank you for reading this article, if you want to go further and get better at blockchain development, leave your email below and you'll get:

  • access to the private Discord of a community of Web3 builders
  • access to free guides that will teach you a subject from scratch like the Web3 JS Cheat Sheet