## 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.
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
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
Now, on your frontend run the following in terminal:
npm i @metamask/detect-provider web3
Your folders should look like this:
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);
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;
We’ll use the nonce parameter for cryptographic communication. We store the address of user for verification.
- Now, go to your graphql/schema folder
- Create a new file. call it graphql.schema.js
- 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
}
`);
- Go to graphql/resolvers folder
- Create a new file. call it graphql.resolvers.js
- 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);
})
},
}
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>
)
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);
}
}
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();
}
}
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'
}
}
},
}
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)