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:
- 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. - On the log in screen, the user connects his wallet
- 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.
- 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
- 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 - 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:
- A
GET
endpoint to get thenonce
that the user needs to sign with their wallet:/api/auth/get-nonce/:address
- 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:
- How to connect a MetaMask wallet to your React app
- How to connect a wallet to your website using Web3Modal (recommended)
- How to connect a MetaMask wallet to your website using vanilla JS
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:
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