Whenever we look up any service over the internet, it requires us to authenticate ourselves as valid users. To prove one is a valid user, he or she needs to sign up for that service using email/password or other social login strategies, which then stores the user’s Web3 login details at that company’s respective server. It means a centralized location on web2.0 that hosts such servers contains our personal information, thus, making user data privacy a primary concern.    

On the contrary, Blockchain or Web3.0 provides us with decentralized access, which means nobody has access to any personal information of the user, therefore, mitigating the risk of user privacy and making users the owner of their own information. To authenticate users over Blockchain wallets like Metamask, methods like public key encryption are used, thus, making it a safe & secure platform for accessing services over the internet. 

So, let’s begin and know how to execute Web3 login with MetaMask using NodeJS & ReactJS.

How Does User Authentication System Work In Web3 While Using MetaMask? 

The concept of a user authentication system with web3 using MetaMask is as follows:

    1. The backend provides a random message for the user to sign in with his or her private key in the MetaMask wallet. The signed message is then returned to the backend, together with the user’s public Ethereum address.
    2. The backend uses its inbuilt cryptographic methods to check if the message was signed with the same private key to which the public address belongs. If the signed message and public address belong to the same private key, it means that the user who is trying to log in is the owner of the account.

After the successful validation, the backend creates a JSON Web Token (JWT) and sends it to the frontend to authenticate further user requests.

Now we are going to form an authentication solution that enables a user to login with MetaMask. Their public Ethereum address will be utilized as a unique identifier, and we will employ the private key management tools that MetaMask discloses to provide a mechanism for a user to verify they own that particular address and have permission to login as that user. 

Let’s Build It Together 

The technology stack we are using is:

  1. Frontend – React.js
  2. Backend – Node.js, Express.js, PostgreSQL
  3. MetaMask Wallet 

Frontend Flow

In our frontend of ReactJS, if MetaMask is installed as a browser extension, then we can access it using window.web3. Moreover, we can get the current MetaMask account’s public address using web3.eth.coinbase.

When a user clicks the login button, frontend calls the backend API

GET /api/users?publicAddress=${publicAddress}  

We can get the public address by using the following code:   

<code>  

const coinbase = await web3.eth.getCoinbase();  

const publicAddress = coinbase.toLowerCase();  

 </code>  

If the request of the above API call does not return any result, it means the user did not sign up, so we will call POST/users route with publicAdress params in the body, which will create the user as a new user in the database, else API will return nonce with user publicAddress which means user already exists in the database. 

Once the frontend receives nonce in response to the previous API call, it executes the following code:  

<code>  

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

</code>  

It opens MetaMask to show a confirmation popup for signing the message. This popup will display the nonce so that the user knows he or she isn’t signing something malicious data.

Web3 login with MetaMask

When the user signs the message, the callback function is called with the signed message (signature) as an argument

The frontend then makes another API call to POST /api/authentication, passing a body with both signature and publicAddress.  

POST /api/authentication request will first check the user using publicAddress given in the request body in the database to fetch the associated nonce.  

Using the nonce publicAddress and signature, we use a helper from eth-sig-util to extract the address from the signature once signature verification is successful.  

If the address is found with sigUtil.recoverPersonalSignature matches with the initial publicAddress that is saved in the database; this API returns a JWT token that may be saved into the localStorage or cookies that can be used by frontend for further secure communication with the backend, and the user will be logged in successfully.

logged in publicAddress

To prevent the user from logging in again with the same signature, update the nonce with a new random value. 

Backend Flow 

On the backend side, we are using ExpressJS with Sequalize for the rest endpoints. 

The database layer creates a user table in PostgreSQL, and the User model defines its configuration.

<code>  

import {Model } from 'sequelize';  

 export class User extends Model {  

public id!: number;  

public nonce!: number;  

public publicAddress!: string;  

public username?: string;   

 

</code>  

Sequalize will create a user’s table as below in the database using this model configuration.

Sequalize

To the complete code for the backend, click here 

We have created two routes, one for auth and the second for users. 

User routes have all routes for the crud operation of users, and auth routes have routes for authentication. 

<code>  

import express from 'express';  

  

import { authRouter } from './auth/authRoutes';  

import { userRouter } from './users/usersRoutes';  

  

export const services = express.Router();  

  

services.use('/auth', authRouter);  

services.use('/users', userRouter);  

</code>  

The important login code is inside the authcontroller & that we are explaining here. The rest crud operation for the user model inside the code is pretty explanatory. Moreover, the authentication logic we describe here is in the code base.   

<code>  

export const login = async (req: Request, res: Response, next: NextFunction) => {  

const {signature, publicAddress } = req.body;  

if (!signature || !publicAddress) {  

return res  

.status(400)  

.send ({message: 'signature and publicAddress is required'});  

  

 

  

// Get the user with the given publicAddres  

const user = new User();  

const userExists = await User.findOne({ where: { publicAddress }});  

if (!userExists) {  

user.nonce = Math.floor(Math.random() * 10000);  

return user.save();  

 

else {  

// if user exists then verify user signature  

const msg = `nonce: ${userExists.nonce}`;  

const msgBufferHex = bufferToHex(Buffer.from(msg, 'utf8'));  

const address = recoverPersonalSignature({  

data: msgBufferHex,  

sig: signature,  

});  

  

//match stored address with address found after verifying signature  

  

if (address.toLowerCase() === publicAddress.toLowerCase()) {  

//create jwt token  

const token = jwt.sign({  

payload: {  

id: user.id,  

publicAddress,  

},  

},  

config.secret,  

 

algorithm: config.algorithms[0],  

})  

return res.json({ accessToken : token });  

} else {  

res.status(401).send({  

error: 'Signature verification failed',  

});  

  

 

 

 

  

</code>  

The frontend sends a request on the POST /auth route with parameters publicAddress and a signature in the body; it can fetch using req.body. If both parameters exist, then we move on. If it shows an error, it means a signature and publicAddress are required.  

In the next step, we find the user using publicAddress; if a user does not exist in the database, we create a nonce and save the user using the user.save();   

If the user exists, then set the message msg as “nonce” exactly like in the frontend with the user’s nonce.next, we pass this message and signature to the recoverPersonalSignature() method of eth-sig-util. This method will return the public address after verifying the signature.   

In the next step, match the stored address with the address found. If the signature is verified, return a JWT token using the jwt sign method. Else return message signature verification failed.  

Give our msg (containing the nonce) and our signature; the recoverPersonalSignature() method returns the public address used to sign the msg. If it matches the publicAddress we fetched in earlier steps from the request body, then the user who made the request successfully verified their ownership of publicAddress. We consider them authenticated users.  

Ending Words

This article was a short and practical guide to quickly implement a Web3 authentication flow using MetaMask, NodeJS, ExpressJS, and ReactJS with PostgreSQL in your application. If you are facing issues related to Web3 login or its authentication or want to create a Blockchain-based project, get in touch with one of the best Blockchain development companies (Infrablok).   

Moreover, if you want to see the complete code, visit at GitHub repo.

Leave a Reply

Your email address will not be published. Required fields are marked *