category: How toNov 24, 2022

Creating a registration and a login with two-factor authentication on React 🤩

If you have thought about building any dashboard, you probably realize you need to implement authentication. You are probably already familiar with terms like Login and Registration.

What is this article about?

In today’s world, more companies are pushing to secure your account and offer you to add a Two-Factor authentication.
Two-factor authentication is an extra layer of protection; it requires you to enter a code you can find in an external service, SMS, Email, or an Authentication App.

In this article, you’ll learn how to build an application that uses two-factor authentication with React, Novu, and Node.js.

Security

What is Two Factor Authentication (2FA)?

Two Factor Authentication – sometimes referred to as dual-factor authentication, is an additional security measure that allows users to confirm their identity before gaining access to an account.
It can be implemented through a hardware token, SMS Text Messages, Push notifications, and biometrics, required by the application before users can be authorized to perform various actions.

In this article, we’ll use the SMS text messaging 2FA by creating an application that accepts the users’ credentials and verifies their identity before granting them access to the application.

Novu – the first open-source notification infrastructure

Just a quick background about us. Novu is the first open-source notification infrastructure. We basically help to manage all the product notifications. It can be In-App (the bell icon like you have in the Dev Community – Websockets), Emails, SMSs and so on.

I would be super grateful if you can help us out by starring the library 🤩
https://github.com/novuhq/novu

GitHub

Setting up a Node.js server

Create the project folder containing two sub-folders named client and server.

1mkdir auth-system
2cd auth-system
3mkdir client server

Navigate into the server folder and create a package.json file.

1cd server & npm init -y

Install Express.js, CORS, and Nodemon.

1npm install express cors nodemon

Express.js is a fast, minimalist framework that provides several features for building web applications in Node.js. CORS is a Node.js package that allows communication between different domains. Nodemon is a Node.js tool that automatically restarts the server after detecting file changes.

Create an index.js file – the entry point to the web server.

1touch index.js

Set up a simple Node.js server as below:

1const express = require("express");
2const cors = require("cors");
3const app = express();
4const PORT = 4000;
5
6app.use(express.urlencoded({ extended: true }));
7app.use(express.json());
8app.use(cors());
9
10app.get("/api", (req, res) => {
11    res.json({ message: "Hello world" });
12});
13
14app.listen(PORT, () => {
15    console.log(`Server listening on ${PORT}`);
16});

Building the app user interface

In this section, we’ll build the user interface for the application allowing users to register and sign in to an application. Users can create an account, log in, and perform 2FA via SMS before they are authorised to view the dashboard.

Create a new React.js project within the client folder.

1cd client
2npx create-react-app ./

Delete the redundant files such as the logo and the test files from the React app, and update the App.js file to display Hello World as below.

1function App() {
2    return (
3        <div>
4            <p>Hello World!</p>
5        </div>
6    );
7}
8export default App;

Navigate into the src/index.css file and copy the code below. It contains all the CSS required for styling this project.

1@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap");
2* {
3    box-sizing: border-box;
4    margin: 0;
5    padding: 0;
6    font-family: "Space Grotesk", sans-serif;
7}
8input {
9    height: 45px;
10    padding: 10px 15px;
11    margin-bottom: 15px;
12}
13button {
14    width: 200px;
15    outline: none;
16    border: none;
17    padding: 15px;
18    cursor: pointer;
19    font-size: 16px;
20}
21.login__container,
22.signup__container,
23.verify,
24.dashboard {
25    width: 100%;
26    min-height: 100vh;
27    padding: 50px 70px;
28    display: flex;
29    flex-direction: column;
30    align-items: center;
31    justify-content: center;
32}
33.login__form,
34.verify__form,
35.signup__form {
36    width: 100%;
37    display: flex;
38    flex-direction: column;
39}
40.loginBtn,
41.signupBtn,
42.codeBtn {
43    background-color: green;
44    color: #fff;
45    margin-bottom: 15px;
46}
47.signOutBtn {
48    background-color: #c21010;
49    color: #fff;
50}
51.link {
52    cursor: pointer;
53    color: rgb(39, 147, 39);
54}
55.code {
56    width: 50%;
57    text-align: center;
58}
59.verify__form {
60    align-items: center;
61}
62
63@media screen and (max-width: 800px) {
64    .login__container,
65    .signup__container,
66    .verify {
67        padding: 30px;
68    }
69}

Install React Router – a JavaScript library that enables us to navigate between pages in a React application.

1npm install react-router-dom

Create a components folder within the React app containing the Signup.jsLogin.jsPhoneVerify.js and Dashboard.js files.

1mkdir components
2cd components
3touch Signup.js Login.js PhoneVerify.js Dashboard.js

Update the App.js file to render the newly created components on different routes via React Router.

1import { BrowserRouter, Route, Routes } from "react-router-dom";
2import Login from "./components/Login";
3import Signup from "./components/Signup";
4import Dashboard from "./components/Dashboard";
5import PhoneVerify from "./components/PhoneVerify";
6
7function App() {
8    return (
9        <BrowserRouter>
10            <Routes>
11                <Route path='/' element={<Login />} />
12                <Route path='/register' element={<Signup />} />
13                <Route path='/dashboard' element={<Dashboard />} />
14                <Route path='phone/verify' element={<PhoneVerify />} />
15            </Routes>
16        </BrowserRouter>
17    );
18}
19
20export default App;

The Login page

Copy the code below into the Login.js file. It accepts the email and password from the user.

1import React, { useState } from "react";
2import { useNavigate } from "react-router-dom";
3
4const Login = () => {
5    const [email, setEmail] = useState("");
6    const [password, setPassword] = useState("");
7    const navigate = useNavigate();
8
9    const handleSubmit = (e) => {
10        e.preventDefault();
11        console.log({ email, password });
12        setPassword("");
13        setEmail("");
14    };
15
16    const gotoSignUpPage = () => navigate("/register");
17
18    return (
19        <div className='login__container'>
20            <h2>Login </h2>
21            <form className='login__form' onSubmit={handleSubmit}>
22                <label htmlFor='email'>Email</label>
23                <input
24                    type='text'
25                    id='email'
26                    name='email'
27                    value={email}
28                    required
29                    onChange={(e) => setEmail(e.target.value)}
30                />
31                <label htmlFor='password'>Password</label>
32                <input
33                    type='password'
34                    name='password'
35                    id='password'
36                    minLength={8}
37                    required
38                    value={password}
39                    onChange={(e) => setPassword(e.target.value)}
40                />
41                <button className='loginBtn'>SIGN IN</button>
42                <p>
43                    Don't have an account?{" "}
44                    <span className='link' onClick={gotoSignUpPage}>
45                        Sign up
46                    </span>
47                </p>
48            </form>
49        </div>
50    );
51};
52
53export default Login;
Login

The Sign up page

Copy the code below into the Signup.js file. It accepts the email, username, telephone, and password from the user.

1import React, { useState } from "react";
2import { useNavigate } from "react-router-dom";
3
4const Signup = () => {
5    const [email, setEmail] = useState("");
6    const [username, setUsername] = useState("");
7    const [tel, setTel] = useState("");
8    const [password, setPassword] = useState("");
9    const navigate = useNavigate();
10
11    const handleSubmit = (e) => {
12        e.preventDefault();
13        console.log({ email, username, tel, password });
14        setEmail("");
15        setTel("");
16        setUsername("");
17        setPassword("");
18    };
19    const gotoLoginPage = () => navigate("/");
20
21    return (
22        <div className='signup__container'>
23            <h2>Sign up </h2>
24            <form className='signup__form' onSubmit={handleSubmit}>
25                <label htmlFor='email'>Email Address</label>
26                <input
27                    type='email'
28                    name='email'
29                    id='email'
30                    value={email}
31                    required
32                    onChange={(e) => setEmail(e.target.value)}
33                />
34                <label htmlFor='username'>Username</label>
35                <input
36                    type='text'
37                    id='username'
38                    name='username'
39                    value={username}
40                    required
41                    onChange={(e) => setUsername(e.target.value)}
42                />
43                <label htmlFor='tel'>Phone Number</label>
44                <input
45                    type='tel'
46                    name='tel'
47                    id='tel'
48                    value={tel}
49                    required
50                    onChange={(e) => setTel(e.target.value)}
51                />
52                <label htmlFor='tel'>Password</label>
53                <input
54                    type='password'
55                    name='password'
56                    id='password'
57                    minLength={8}
58                    required
59                    value={password}
60                    onChange={(e) => setPassword(e.target.value)}
61                />
62                <button className='signupBtn'>SIGN UP</button>
63                <p>
64                    Already have an account?{" "}
65                    <span className='link' onClick={gotoLoginPage}>
66                        Login
67                    </span>
68                </p>
69            </form>
70        </div>
71    );
72};
73
74export default Signup;
Singup

The PhoneVerify page

Update the PhoneVerify.js file to contain the code below. It accepts the verification code sent to the user’s phone number.

1import React, { useState } from "react";
2import { useNavigate } from "react-router-dom";
3
4const PhoneVerify = () => {
5    const [code, setCode] = useState("");
6    const navigate = useNavigate();
7
8    const handleSubmit = (e) => {
9        e.preventDefault();
10        console.log({ code });
11        setCode("");
12        navigate("/dashboard");
13    };
14    return (
15        <div className='verify'>
16            <h2 style={{ marginBottom: "30px" }}>Verify your Phone number</h2>
17            <form className='verify__form' onSubmit={handleSubmit}>
18                <label htmlFor='code' style={{ marginBottom: "10px" }}>
19                    A code has been sent your phone
20                </label>
21                <input
22                    type='text'
23                    name='code'
24                    id='code'
25                    className='code'
26                    value={code}
27                    onChange={(e) => setCode(e.target.value)}
28                    required
29                />
30                <button className='codeBtn'>AUTHENTICATE</button>
31            </form>
32        </div>
33    );
34};
35
36export default PhoneVerify;
Dashboard

The Dashboard page

Copy the code below into the Dashboard.js file. It is a protected page that is accessible to authenticated users only.

1import React, {useState} from "react";
2import { useNavigate } from "react-router-dom";
3
4const Dashboard = () => {
5    const navigate = useNavigate();
6
7useEffect(() => {
8        const checkUser = () => {
9            if (!localStorage.getItem("username")) {
10                navigate("/");
11            }
12        };
13        checkUser();
14    }, [navigate]);
15
16    const handleSignOut = () => {
17        localStorage.removeItem("username");
18        navigate("/");
19    };
20
21    return (
22        <div className='dashboard'>
23            <h2 style={{ marginBottom: "30px" }}>Howdy, David</h2>
24            <button className='signOutBtn' onClick={handleSignOut}>
25                SIGN OUT
26            </button>
27        </div>
28    );
29};
30
31export default Dashboard;

Creating the authentication workflow

Here, we’ll create the authentication workflow for the application.
When creating an account, the application accepts the user’s email, username, telephone number, and password. Then redirects the user to the sign-in page, where the email and password are required. The application sends a verification code to the user’s phone number to verify their identity before viewing the dashboard page.

The Sign up route

Create a function within the Signup component that sends the user’s credentials to the Node.js server.

1const postSignUpDetails = () => {
2    fetch("http://localhost:4000/api/register", {
3        method: "POST",
4        body: JSON.stringify({
5            email,
6            password,
7            tel,
8            username,
9        }),
10        headers: {
11            "Content-Type": "application/json",
12        },
13    })
14        .then((res) => res.json())
15        .then((data) => {
16            console.log(data);
17        })
18        .catch((err) => console.error(err));
19};
20
21const handleSubmit = (e) => {
22    e.preventDefault();
23    //👇🏻 Call it within the submit function
24    postSignUpDetails();
25    setEmail("");
26    setTel("");
27    setUsername("");
28    setPassword("");
29};

Create a POST route within the index.js file on the server that accepts the user’s credentials.

1app.post("/api/register", (req, res) => {
2    const { email, password, tel, username } = req.body;
3    //👇🏻 Logs the credentials to the console
4    console.log({ email, password, tel, username });
5})

Since we need to save the user’s credentials, update the POST route as below:

1//👇🏻 An array containing all the users
2const users = [];
3
4//👇🏻 Generates a random string as the ID
5const generateID = () => Math.random().toString(36).substring(2, 10);
6
7app.post("/api/register", (req, res) => {
8    //👇🏻 Get the user's credentials
9    const { email, password, tel, username } = req.body;
10
11    //👇🏻 Checks if there is an existing user with the same email or password
12    let result = users.filter((user) => user.email === email || user.tel === tel);
13
14    //👇🏻 if none
15    if (result.length === 0) {
16        //👇🏻 creates the structure for the user
17        const newUser = { id: generateID(), email, password, username, tel };
18        //👇🏻 Adds the user to the array of users
19        users.push(newUser);
20        //👇🏻 Returns a message
21        return res.json({
22            message: "Account created successfully!",
23        });
24    }
25    //👇🏻 Runs if a user exists
26    res.json({
27        error_message: "User already exists",
28    });
29});

Update the postSignUpDetails function within the Signup component to notify users that they have signed up successfully.

1const postSignUpDetails = () => {
2    fetch("http://localhost:4000/api/register", {
3        method: "POST",
4        body: JSON.stringify({
5            email,
6            password,
7            tel,
8            username,
9        }),
10        headers: {
11            "Content-Type": "application/json",
12        },
13    })
14        .then((res) => res.json())
15        .then((data) => {
16            if (data.error_message) {
17                alert(data.error_message);
18            } else {
19                alert(data.message);
20                navigate("/");
21            }
22        })
23        .catch((err) => console.error(err));
24};

The code snippet above checks if the data returned from the server contains an error message before navigating to the log-in route. If there is an error, it displays the error message.

The Login route

Create a function with the Login component that sends the user’s email and password to the server.

1const postLoginDetails = () => {
2    fetch("http://localhost:4000/api/login", {
3        method: "POST",
4        body: JSON.stringify({
5            email,
6            password,
7        }),
8        headers: {
9            "Content-Type": "application/json",
10        },
11    })
12        .then((res) => res.json())
13        .then((data) => {
14            console.log(data);
15        })
16        .catch((err) => console.error(err));
17};
18const handleSubmit = (e) => {
19    e.preventDefault();
20    //👇🏻 Calls the function
21    postLoginDetails();
22    setPassword("");
23    setEmail("");
24};

Create a POST route on the server that authenticates the user.

1app.post("/api/login", (req, res) => {
2    //👇🏻 Accepts the user's credentials
3    const { email, password } = req.body;
4    //👇🏻 Checks for user(s) with the same email and password
5    let result = users.filter(
6        (user) => user.email === email && user.password === password
7    );
8
9    //👇🏻 If no user exists, it returns an error message
10    if (result.length !== 1) {
11        return res.json({
12            error_message: "Incorrect credentials",
13        });
14    }
15    //👇🏻 Returns the username of the user after a successful login
16    res.json({
17        message: "Login successfully",
18        data: {
19            username: result[0].username,
20        },
21    });
22});

Update the postLoginDetails to display the response from the server.

1const postLoginDetails = () => {
2    fetch("http://localhost:4000/api/login", {
3        method: "POST",
4        body: JSON.stringify({
5            email,
6            password,
7        }),
8        headers: {
9            "Content-Type": "application/json",
10        },
11    })
12        .then((res) => res.json())
13        .then((data) => {
14            if (data.error_message) {
15                alert(data.error_message);
16            } else {
17                //👇🏻 Logs the username to the console
18                console.log(data.data);
19                //👇🏻 save the username to the local storage
20                localStorage.setItem("username", data.data.username);
21                //👇🏻 Navigates to the 2FA route
22                navigate("/phone/verify");
23            }
24        })
25        .catch((err) => console.error(err));
26};

The code snippet above displays the error message to the user if there is an error; otherwise, it saves the username gotten from the server to the local storage for easy identification.

In the upcoming sections, I’ll guide you through adding the SMS two-factor authentication using Novu.

How to add Novu to a Node.js application

Novu allows us to add various forms of notifications, such as SMS, email, chat, and push messages to your software applications.

To install the Novu Node.js SDK, run the code snippet below on your server.

1npm install @novu/node

Create a Novu project by running the code below. A personalised dashboard is available to you.

1npx novu init

You will need to sign in with Github before creating a Novu project. The code snippet below contains the steps you should follow after running npx novu init

1Now let's setup your account and send your first notification
2❓ What is your application name? Devto Clone
3❓ Now lets setup your environment. How would you like to proceed?
4   > Create a free cloud account (Recommended)
5❓ Create your account with:
6   > Sign-in with GitHub
7❓ I accept the Terms and Condidtions (https://novu.co/terms) and have read the Privacy Policy (https://novu.co/privacy)
8    > Yes
9✔️ Create your account successfully.
10
11We've created a demo web page for you to see novu notifications in action.
12Visit: http://localhost:57807/demo to continue

Visit the demo web page http://localhost:57807/demo, copy your subscriber ID from the page, and click the Skip Tutorial button. We’ll be using it later in this tutorial.

Novu1

Copy your API key available within the Settings section under API Keys on the Novu Manage Platform.

Novu2

Import Novu from the package and create an instance using your API Key on the server.

1//👇🏻 Within server/index.js
2
3const { Novu } = require("@novu/node");
4const novu = new Novu("<YOUR_API_KEY>");

Adding SMS two-factor authentication with Novu

Novu supports several SMS text messaging tools such as Twilio, Nexmo, Plivo, Amazon SNS, and many others. In this section, you’ll learn how to add Twilio SMS messaging to Novu.

Setting up a Twilio account

Go to the Twilio homepage and create an account. You will have to verify your email and phone number.

Head over to your Twilio Console once you’re signed in.

Generate a Twilio phone number on the dashboard. This phone number is a virtual number that allows you to communicate via Twilio.

Copy the Twilio phone number somewhere on your computer; to be used later in the tutorial.

Scroll down to the Account Info section, and copy and paste the Account SID and Auth Token somewhere on your computer. (to be used later in this tutorial).

Connecting Twilio SMS to Novu

Select Integrations Store from the sidebar of your Novu dashboard and scroll down to the SMS section.

Choose Twilio and enter the needed credentials provided by Twilio, then click Connect.

Novu2

Next, create a notification template by selecting Notifications on the sidebar.

Novu4

Select Workflow Editor from the side pane and create a workflow as below:

Novu5

Click the SMS from the workflow and add the text below to the message content field.

1Your verification code is {{code}}

Novu allows you to add dynamic content or data to the templates using the Handlebars templating engine. The data for the code variable will be inserted into the template as a payload from the request.

Go back to the index.js file on the server and create a function that sends an SMS to verify the users when they log in to the application. Add the code below into the index.js file:

1//👇🏻 Generates the code to be sent via SMS
2const generateCode = () => Math.random().toString(36).substring(2, 12);
3
4const sendNovuNotification = async (recipient, verificationCode) => {
5    try {
6        let response = await novu.trigger("<NOTIFICATION_TEMPLATE_ID>", {
7            to: {
8                subscriberId: recipient,
9                phone: recipient,
10            },
11            payload: {
12                code: verificationCode,
13            },
14        });
15        console.log(response);
16    } catch (err) {
17        console.error(err);
18    }
19};

The code snippet above accepts the recipient’s telephone number and the verification code as a parameter.

Update the login POST route to send the SMS via Novu after a user logs in to the application.

1//👇🏻 variable that holds the generated code
2let code;
3
4app.post("/api/login", (req, res) => {
5    const { email, password } = req.body;
6
7    let result = users.filter(
8        (user) => user.email === email && user.password === password
9    );
10
11    if (result.length !== 1) {
12        return res.json({
13            error_message: "Incorrect credentials",
14        });
15    }
16    code = generateCode();
17
18    //👇🏻 Send the SMS via Novu
19    sendNovuNotification(result[0].tel, code);
20
21    res.json({
22        message: "Login successfully",
23        data: {
24            username: result[0].username,
25        },
26    });
27});

To verify the code entered by the user, update the PhoneVerify component to send the code to the server.

1const postVerification = async () => {
2    fetch("http://localhost:4000/api/verification", {
3        method: "POST",
4        body: JSON.stringify({
5            code,
6        }),
7        headers: {
8            "Content-Type": "application/json",
9        },
10    })
11        .then((res) => res.json())
12        .then((data) => {
13            if (data.error_message) {
14                alert(data.error_message);
15            } else {
16                //👇🏻 Navigates to the dashboard page
17                navigate("/dashboard");
18            }
19        })
20        .catch((err) => console.error(err));
21};
22const handleSubmit = (e) => {
23    e.preventDefault();
24    //👇🏻 Calls the function
25    postVerification();
26    setCode("");
27};

Create a POST route on the server that accepts the code and checks if it is the same as the code on the backend.

1app.post("/api/verification", (req, res) => {
2    if (code === req.body.code) {
3        return res.json({ message: "You're verified successfully" });
4    }
5    res.json({
6        error_message: "Incorrect credentials",
7    });
8});

Congratulations!🎊 You’ve completed the project for this tutorial.

Conclusion

So far, you’ve learnt what two-factor authentication is, its various forms, and how to add it to a web application.

Two Factor Authentication is added to software applications to protect both the user credentials and the resources users can access. Depending on your application, you can add 2FA at specific parts where significant changes occur.

The source code for this application is available here: https://github.com/novuhq/blog/tree/main/2FA-with-react-nodejs-and-novu

Thank you for reading!

P.S I would be super grateful if you can help us out by starring the library 🤩
https://github.com/novuhq/novu

GitHub

Related Posts

category: How to

A Proper Guide to Web and Mobile Push Notification Service

Implement push notification services successfully by following this actionable guide on choosing platforms, setting up, and personalizing notifications for better results.

Emil Pearce
Emil Pearce
category: How to

How to Add Real-Time Notifications to a React App

Learn how to integrate real-time notifications into your React app using WebSockets, Server-Sent Events, Firebase Cloud Messaging (FCM), and Novu for improved user engagement and instant updates.

Emil Pearce
Emil Pearce
category: How to

A Developer’s Guide to Choosing the Best Notification Platform

A comprehensive guide for developers on selecting notification platforms, covering different types of notifications (push, in-app, email, SMS, and chat), key features to consider, popular providers, and best practices for implementation. Learn how to evaluate notification platforms based on integration ease, cost-effectiveness, scalability, and security, with practical insights on platforms like FCM, Twilio, SendGrid, and Novu.

Emil Pearce
Emil Pearce