DEV Community

Oliver Kem
Oliver Kem

Posted on

Implementing Login with Metamask, send Ether, user registration using React, NodeJS, Sequelize and GraphQL

## In this Project we are going to learn how you can implement a login functionality using React on frontend NodeJS backend with GraphQL and stores data on an SQL database. When creating DAPPs there’s always a need to implement functionality where users can login there metamask account to your applications once they are registered.

In this article, we will be generating JWT tokens to allow users to access protected routes in NodeJS.

Let’s look at the authentication flow in the image below.
Auth flow

Prerequisites

  • NodeJS
  • React
  • Metamask

So, let’s get started. Go to your terminal, create a new folder name it any name you like. I’ll call it web3-login.

pnpm create vite@latest 
code web3-login
Enter fullscreen mode Exit fullscreen mode

Now in this folder, let’s create another directory here, call it backend, and install a couple of dependencies:

mkdir backend
cd backend
npm init -y
npm i bcryptjs web3 ethereumjs-util uuid express sequelize jsonwebtoken
npm i express-graphql graphql sqlite3
npm i -D nodemon
mkdir graphql
mkdir database models 
cd graphql && mkdir resolvers && mkdir schema
Enter fullscreen mode Exit fullscreen mode

Now, on your frontend run the following in terminal:

npm i @metamask/detect-provider web3
Enter fullscreen mode Exit fullscreen mode

Your folders should look like this:
folder_vieq

Now, in your backend directory, make a new file. call it index.js. type in the following lines of code:

const express = require('express');
const {graphqlHTTP} = require('express-graphql');
// we will make this folder. Keep following
const graphqlSchema = require('./graphql/schema/schema.graphql');
const graphqlResolver = require('./graphql/resolvers/resolvers.graphql.js');
const sequelize = require('./database/db');

const app = express();
app.use(express.json());
// you can also use the CORS library of nodejs
app.use((req, res, next)=>{
    res.setHeader("Access-Control-Allow-Origin","*");
    res.setHeader("Access-Control-Allow-Methods","POST,GET,OPTIONS");
    res.setHeader("Access-Control-Allow-Headers", 'Content-Type, Authorization');
    if(req.method ==="OPTIONS"){
        return res.sendStatus(200);
    }
    next();
});
// creates a graphql server
app.use('/users',graphqlHTTP({
    schema: graphqlSchema,
    rootValue: graphqlResolver,
    graphiql: true
}))
sequelize
.sync()
.then(res=>{
    // only connects to app when
    app.listen(4000,()=>{
        console.log("Backend Server is running!!");
    })
})
.catch(err=>console.error);
Enter fullscreen mode Exit fullscreen mode

Now, go to your database folder. create a new file. call it db.js. copy and paste this line of code. Also create a new file with the .sqlite extension. e.g. db.sqlite

const sequelize = require('../database/db');

const { DataTypes } = require('sequelize');
const User = sequelize.define('Users', {
    id: {
        type: DataTypes.UUIDV1,
        allowNull: false,
        primaryKey: true
    },
    email: {
        type: DataTypes.STRING,
        allowNull: false,
        unique: true
    },
    password: {
        type: DataTypes.STRING(64),
        allowNull: false
    },
    address: {
        type: DataTypes.STRING,
        allowNull: false,
        unique: true
    },
    nonce: {
        type: DataTypes.INTEGER,
        allowNull: false,
        unique: true
    },
},{
    timestamps: true
}
);
module.exports = User;
Enter fullscreen mode Exit fullscreen mode

We’ll use the nonce parameter for cryptographic communication. We store the address of user for verification.

  1. Now, go to your graphql/schema folder
  2. Create a new file. call it graphql.schema.js
  3. Paste in the following lines of code
const {buildSchema} = require('graphql');
//we build the schema here.
module.exports = buildSchema(`
    type User{
        id: ID!
        email: String!
        password: String
        address: String!
        nonce: Int!
    }
    input UserInput{
        email: String!
        password: String!
        address: String!
    }
    type AuthData{
        userId: ID!
        token: String!
        tokenExpiration: Int!
    }
    type Token{
        nonce: Int
    }
    type jwtToken{
        token: String,
        message: String
    }
    type RootQuery {
        loginMetamask(address: String!): Token!
        login(email: String!, password: String!): AuthData!
        signatureVerify(address: String!,signature: String!): jwtToken!
        users: [User!]
    }
    type RootMutation {
        register(userInput: UserInput): User!
    }
    schema {
        query: RootQuery
        mutation: RootMutation
    }
`);
Enter fullscreen mode Exit fullscreen mode
  1. Go to graphql/resolvers folder
  2. Create a new file. call it graphql.resolvers.js
  3. Let's start with user registration
const User = require('../../models/user.model.js');
module.exports = {
  //from our schema, we pass in the user email, password and address,
  // a nonce is generated
  register: (args) => {
      return User.create({
          id: uuid.v1(),
          ...args.userInput,
          nonce: Math.floor(Math.random() * 1000000)
      }).then(res=>{
          console.log(res);
          return res
      }).catch(err=>{
          console.log(err);
      })  
    },
}
Enter fullscreen mode Exit fullscreen mode

Now, we go on the frontend:

return(
        <div className="login_metamask">
            <h1>Metamask</h1>
            <div className="success">
                <h3>Succcessfull Transaction</h3>
                <div className="animation-slider"></div>
            </div>
            <section className="metamask__action">
                <button onClick={Enable_metamask} className="enable_metamask">
                    <img src={require("../images/metamask.png")} alt="metamaskimage" />
                    <h2>Enable Metamask</h2>
                </button>
                <button onClick={send_metamask} className="enable_metamask">
                    <img src={require("../images/metamask.png")} alt="metamaskimage" />
                    <h2>Send Ether</h2>
                </button>
                <button onClick={Login_metamask} className="enable_metamask">
                    <img src={require("../images/metamask.png")} alt="metamaskimage" />
                    <h2>Login with Metamask</h2>
                </button>
                <button onClick={Logout_metamask} className="enable_metamask">
                    <img src={require("../images/metamask.png")} alt="metamaskimage" />
                    <h2>LOGOUT</h2>
                </button>
            </section>
        </div>
    )
Enter fullscreen mode Exit fullscreen mode

Image of metamsk

Now, we need to enable meta mask. When you click the “ENABLE METAMASK” button, the code will run and pop up window will open. Key in your password.

import Web3 from "web3";
import detectEthereumProvider from "@metamask/detect-provider";
async function Enable_metamask(){
    const provider = await detectEthereumProvider();
    if (provider) {
        window.web3 = new Web3(provider);
        web3 = window.web3;
        ethereum = window.ethereum;
        const chainId = await ethereum.request({ method: 'eth_chainId' });
        const accounts  = await ethereum.request({ method: 'eth_requestAccounts' });
        account = accounts[0];
        console.log(chainId, accounts);
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, let’s implement the login with Metamask functionality:

async function Login_metamask(){
        //1. First we query for the user nonce in the backend passing in the user account address
        let requestBody={
            query: `
                query {
                    loginMetamask(address: "${account}"){
                        nonce
                    }
                }
            `
        }
        const handleSignMessage = (nonce, publicAddress) => {
            return new Promise((resolve, reject) =>
                web3.eth.personal.sign(
                    web3.utils.fromUtf8(`Nonce: ${nonce}`),
                    publicAddress,
                    (err, signature) => {
                        if (err) return reject(err);
                        return resolve({ publicAddress, signature });
                    }
                )
            );
        }
        //2. if metamask is enabled we send in the request
        if(web3 && ethereum && account){
            fetch('http://localhost:4000/users', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify(requestBody),
            })
            .then(res=>{
                if(res.status !== 200 && res.status !==201){
                    //3. if we get an error, respond error
                    throw new Error("Failed");
                }
                return res.json();
            })
            .then(data => {
                console.log(data);
                  //4.  we log retrieve the nonce
                const nonce = data.data.loginMetamask.nonce;
                console.log(nonce);
                if(nonce != null){
                    //5. we then generate a signed message. and send it to the backend
                    return handleSignMessage(nonce,account)
                    .then((signedMessage)=>{
                        console.log(signedMessage.signature)
                        requestBody = {
                            query: `
                                query {
                                    signatureVerify(address: "${account}",signature: "${signedMessage.signature}"){
                                        token
                                        message
                                    }
                                }
                            `
                        }
                        fetch('http://localhost:4000/users', {
                            method: 'POST',
                            headers: {
                                'Content-Type': 'application/json',
                            },
                            body: JSON.stringify(requestBody),
                        })
                        .then(response =>{
                            if(response.status !== 200 && response.status !==201){
                                throw new Error('Failed');
                            }
                            return response.json();
                        })
                        .then(data => {
                            console.log(data);
                        })
                        .catch(err=>console.error); 
                    })
                }else{
                    //Redirect the user to registration site.
                    console.log('Please Register at our site. ')
                }
            })
            .catch((error) => {
                console.error('Error encountered:', error);
            });
        }else{
            await Enable_metamask();
        }
    }
Enter fullscreen mode Exit fullscreen mode

Now, on our backend we receive this request and process it:

const User = require('../../models/user.model.js');
function VerifySignature(signature,nonce) {
    const msg = `Nonce: ${nonce}`;
    //convert the message to hex
    const msgHex = ethUtil.bufferToHex(Buffer.from(msg));
    //if signature is valid
    const msgBuffer = ethUtil.toBuffer(msgHex);
    const msgHash = ethUtil.hashPersonalMessage(msgBuffer);
    const signatureBuffer = ethUtil.toBuffer(signature);
    const signatureParams = ethUtil.fromRpcSig(signatureBuffer);
    const publicKey = ethUtil.ecrecover(
        msgHash,
        signatureParams.v,
        signatureParams.r,
        signatureParams.s
    );
    const addressBuffer = ethUtil.publicToAddress(publicKey);
    const secondaddress = ethUtil.bufferToHex(addressBuffer);
    return secondaddress;
}
module.exports = {
  //from our schema, we pass in the user email, password and address,
  // a nonce is generated
    register: (args) => {
        return User.create({
            id: uuid.v1(),
            ...args.userInput,
            nonce: Math.floor(Math.random() * 1000000)
        }).then(res=>{
            console.log(res);
            return res
        }).catch(err=>{
            console.log(err);
        })  
    },
    loginMetamask: function({address}){
          return User.findOne({address})
          .then(user=>{
              if(!user){
                  // if user doesn't exit return null
                  return{
                      nonce: null
                  }
              }
              return {
                //else return the nonce of user
                  nonce: user.dataValues.nonce
              }
          })
          .catch(err=>{
              throw err;
          })
    },
    signatureVerify:async function({address, signature}){
            const user =await User.findOne({address});
            if(!user){
                return{
                    token: null,
                    message: 'User not Found. Sign In again'
                }
            }
            // then verify the signature sent by the user i
            let secondaddress = VerifySignature(signature,user.nonce)
            if(address.toLowerCase() === secondaddress.toLowerCase()){
                //change the user nonce
                user.nonce = Math.floor(Math.random() * 1000000);
                await user.save()
                const token = await jwt.sign({address: user.address,email: user.email},'HelloMySecretKey',{expiresIn: '1h'});
                return{
                    token: token,
                    message: 'User not Found. Sign In again'
                }
            }else {
                return{
                    token: null,
                    message: 'User not Found. Sign In again'
                }
            }
    },
}
Enter fullscreen mode Exit fullscreen mode

From this, a json web token will be returned to the client. We can use this to authenticate to protected routes on our server. Now, for user experience:

Wrapping Up

** This is just a simple authentication mechanism with Metamask yet effective.

Find thesource code on GitHub link:

https://github.com/OliverMengich/web3LoginSendEtherMetamask.git

Thank you.**

Top comments (0)