Fredrick Emmanuel
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Versions and GitHub Sync Note Insights Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       owned this note    owned this note      
    Published Linked with GitHub
    Subscribed
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    Subscribe
    # Strengthen App Security with Multi-Factor Authentication. Passwords are very important in applications as they create a barrier between unauthorized users and a resource in the application. These passwords alone can’t prevent hackers from brute-forcing their way into your application, hence the need for Multi-Factor authentication. Multi-Factor authentication creates a two-layered barrier between an unauthorized user and resources in your web applications. To get into your application, one of the common ways is to ask the user to provide their username and password. Once this is provided, the user is required to retrieve the One Time Password (OTP) sent to their email. This strengthens the security of your various applications to a large extent. Follow this tutorial to learn how to integrate Multi-factor authentication into your applications. ## Prerequisites Following effectively with this tutorial requires: - NodeJS 18.0+ installed. > To know the current version of your NodeJS, run `node --version` command in your terminal. - Knowledge of NextJS and NodeJS. - Code editor, preferably Visual Studio Code. Once you've got all these, Let's jump right into the article. ## What is MFA? Multi-Factor Authentication (MFA) is an authentication mechanism that requires two or more distinct method of verification which grants the user access to a resource in the application. Research shows that cracking an eight-character password containing symbols, and lower and uppercase letters, using a supercomputer, can be done in a couple of hours. This makes it essential to add layers of authentication to your application. MFA makes use of verification processes like passwords, biometrics, One Time Passwords(OTP), etc Now that we have basic knowledge of MFA, let's get started with the application. ## Getting Started Create a new folder in your chosen directory that will contain all the source code for this application. Name this folder as you desire, but its name in this tutorial is **mfa**. Open the folder in VS Code and run the command below in the integrated terminal to create a NextJS application. ```bash yarn create next-app ``` Next, we would install the following dependencies: ```bash npm install mongoose nodemailer nanoid jose cookie ``` - [`mongoose`](https://www.npmjs.com/package/mongoose) will be used to link our application to MongoDB and create a Login schema. - [`nodemailer`](https://www.npmjs.com/package/nodemailer) will be used to send the generated OTP to the user's provided email address. - [`nanoid`](https://www.npmjs.com/package/nanoid) will be used to generate the random OTP. - [`jose`](https://www.npmjs.com/package/jose) will be used to generate the `jwt` and store the OTP. - [`cookie`](https://www.npmjs.com/package/cookie) will be used to store the token. Once we've installed the dependencies successfully, the next step is to create different API routes for this application. In this application, we would create 2 routes. - Auth Route. - Email Route. - Verify Route. Let's create the following files in the **api** folder. ``` ┣ pages ┃ ┣ api ┃ ┃ ┣ auth.js ┃ ┃ ┣ email.js ┃ ┃ ┗ verify.js ┃ ┣ _app.js ┃ ┗ _document.js ``` Next, we will create 4 files in the pages folder; **login.js**, **signup.js**, **verify.js**, and **protected.js**. ``` ┣ pages ┃ ┣ index.js ┃ ┣ protected.js ┃ ┣ login.js ┃ ┣ signup.js ┃ ┣ verify.js ┃ ┣ _app.js ┃ ┗ _document.js ``` When we've created the **api** and the **pages** files, we can start building the first stage of authentication. ## Creating the first layer of authentication In this section, we will set up the database and a **Log in/Sign up** authentication mechanism, where users will log in/sign up with their username and password. ### Setting up the database This tutorial chooses [**Mongodb**](https://www.mongodb.com/) as its chosen database. Feel free to use any database you desire. Navigate to [Mongodb's official website](https://www.mongodb.com/atlas/database) to [login](https://account.mongodb.com/account/login?nds=true) or [signup](https://www.mongodb.com/cloud/atlas/register) for an account, if you don't have one. Once you're done, head on to your [dashboard](https://cloud.mongodb.com/) and **Build a database**. ![Create Database](https://i.imgur.com/Aw0ykqO.jpg) You can choose to use any tier of your choice but this tutorial uses the free tier. ![Free tier](https://i.imgur.com/3bhWTrO.png) Next, scroll to the bottom, give the database a name and click **Create Cluster**. The name of the database in this tutorial is **MFA**. ![Name of databse](https://i.imgur.com/emetZ4z.png) Next, you'll be asked to add a username and a password else, click on **Database Access** by the sidebar and select **ADD NEW DATABASE USER**. Enter your desired details and click **Create User** ![Create User](https://i.imgur.com/KYaDcHc.png) Select **Network Access** by the side nav bar and click on **Add IP Address** to configure the database's IP address. ![IP address not added](https://i.imgur.com/J4FylnA.png). You can decide to either add only your current IP address or make the database accessible to all. Once the decision is made, click on the **Confirm** button. ![IP address added](https://i.imgur.com/axW8uUK.jpg) ### Adding MongoDB to the application Select **Database** on the sidebar and click the **Connect** button. ![Connect Button](https://i.imgur.com/f7nRm0d.jpg) Click on **Connect your application** ![Connect your application](https://i.imgur.com/sshiv39.jpg) Now, copy the **MongoDB** URI. ![MongoDB URI](https://i.imgur.com/WfD7e71.jpg) Open the **next.config.js** file and paste the URI as shown in the code below. ```js /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, env: { MONGO_URI: 'mongodb+srv://admin:<password>@mfa.example.mongodb.net/?retryWrites=true&w=majority', }, }; module.exports = nextConfig; ``` > Replace `<password>` with the password you specified. Next, create a folder in the project's directory named **database** and create a file named **index.js** in it. This file will connect our application to **MongoDB**. Add the following lines of code to the **index.js** file. ```js //Path: datbase/index.js import mongoose from 'mongoose'; const ConnectDB = () => { mongoose.set('strictQuery', false); mongoose .connect(process.env.MONGO_URI) .then(() => { console.log('Connected successfully'); }) .catch(err => { console.log(err.message); }); }; export default ConnectDB; ``` > Ensure you added the `MONGO_URI` to the **next.config.js** file Open your **auth.js** file and add the lines of code below. ```js //Path: pages/api/auth.js import ConnectDB from '@/database';//Importing the Function for connecting MongoDB. ConnectDB();// Running the function const Auth = (req, res) => {}; export default Auth; ``` ### Setting up the server In this section, we'll handle the different CRUD operations that will be sent to the `api/auth` route. Still in the **auth.js** file, add the following. ```js //Path: pages/api/auth.js import ConnectDB from '@/database'; //Importing the Function for connecting MongoDB. ConnectDB(); // Running the function const Auth = async (req, res) => { const { method } = req; //Getting the type of request made switch (method) { case 'POST': //handler for POST requests try { console.log(method + ' REQUEST'); res.end(); } catch (error) { console.log(error); res.status(400).json({ success: false, message: 'POST request Error' }); } break; case 'PUT': //handler for PUT requests try { console.log(method + ' REQUEST'); res.end(); } catch (error) { console.log(error); res.status(400).json({ success: false, message: 'PUT request Error' }); } break; default: res .status(400) .json({ success: false, message: 'Unsupported CRUD operation' }); break; } }; export default Auth; ``` Above, we created a handler for the `PUT` and `POST` requests and returned an error, a status of `400`, when a different CRUD operation is sent. ### Creating the user model Now, we will create the schema for the first layer of authentication. This schema will contain a `name`, `email`, and a `password`. Create a file named **model.js** in the **database** folder and add the following to it. ```js //Path: database/model.js const mongoose = require('mongoose'); const { Schema } = mongoose; const UserSchema = new Schema({//Creating the schema name: { type: String, }, email: { type: String, required: [true, 'Provide an email address'], }, password: { type: String, required: [true, 'Please add a password'], minlength: 6, }, }); //Checking if the model has been created module.exports = mongoose.models.User || mongoose.model('User', UserSchema); ``` In the above code, we created the user model containing the name, email, and password fields. We set the email as required, sending an error message if the email address is not provided. We did the same for the password field but specified a minimum length the password should have. ### Storing the user's data Now, import the **model.js** file into the **auth.js** file. ```js //Path: pages/api/auth.js import ConnectDB from '@/database'; //Importing the Function for connecting MongoDB. import User from '@/database/model'; // Importing the User Model //The rest of the code ``` Next, we will handle the `PUT` operation that will be used to register new users. ```js //Path: pages/api/auth.js import ConnectDB from '@/database'; //Importing the Function for connecting MongoDB. import User from '@/database/model'; // Importing the User Model ConnectDB(); // Running the function const Auth = async (req, res) => { const { method } = req; //Getting the type of request made switch (method) { case 'PUT': //handler for PUT requests try { const check = await User.findOne({ email: req.body.email });//Checking if the email exist if (check) { res .status(400) .json({ success: false, message: 'Email already exists!' }); } else { await User.create(req.body);//Adding the user to the database res .status(201) .json({ success: true, message: 'Account created successfully' }); } } catch (error) { console.log(error.message); res.status(400).json({ success: false, message: 'PUT request Error' }); } break; //The rest of the code } }; export default Auth; ``` Above, we checked if the email provided by the user is already in the database else, we will add the user's detail to the database. > You can choose to encrypt the password before storing it in the database using [bcrypt](https://www.npmjs.com/package/bcrypt). ### Signing users Here, we will handle the `POST` request that will be used to sign in users. ```js //Path: pages/api/auth.js import ConnectDB from '@/database'; //Importing the Function for connecting MongoDB. import User from '@/database/model'; // Importing the User Model ConnectDB(); // Running the function const Auth = async (req, res) => { const { method } = req; //Getting the type of request made switch (method) { //Missing lines of code case 'POST': //handler for POST requests try { const user = await User.findOne({ email: req.body.email }); //Checking if the email exist with a password if (user) { if (user.password === req.body.password) { //Checking the password provided is the same as the one in the database res .status(200) .json({ success: true, message: 'Login successful' }); } else { res .status(401) .json({ success: false, message: 'Invalid Email or Password' }); } } else { res .status(401) .json({ success: false, message: 'Invalid Email or Password' }); } } catch (error) { console.log(error); res.status(400).json({ success: false, message: 'POST request Error' }); } break; default: res .status(400) .json({ success: false, message: 'Unsupported CRUD operation' }); break; } }; export default Auth; ``` The above code handles the Login function. We checked if the user's email exist then we checked if the password provided tallies with the one in the database. ### Setting up the Frontend With this, we are done with the server-side part of the first layer of authentication. The next step is to work on the client side. Open the **signup.js** file and let's handle the signup page. ```js //Path: pages/signup.js import { useState } from 'react'; export default function Signup() { const [name, setName] = useState(''); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const handleChange = (e, func) => { func(e.target.value); }; return ( <> <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh', fontSize: '30px', }} > <form onSubmit={onSubmit}> <u> <h1>Register</h1> </u> <label htmlFor="name">Name: </label> <input type="text" value={name} onChange={e => handleChange(e, setName)} style={{ marginBottom: '10px', height: '20px', fontSize: '20px', outline: 'none', }} /> <br /> <label htmlFor="email">Email: </label> <input type="email" name="email" value={email} style={{ marginBottom: '10px', height: '20px', fontSize: '20px', outline: 'none', }} id="email" required onChange={e => handleChange(e, setEmail)} /> <br /> <label htmlFor="password">Password: </label> <input type="password" name="password" value={password} style={{ marginBottom: '10px', height: '20px', fontSize: '20px', outline: 'none', }} id="password" minLength="6" required onChange={e => handleChange(e, setPassword)} /> <br /> <button type="submit" style={{ width: '100px', height: '30px' }}> Submit </button> </form> </div> </> ); } ``` > This tutorial would focus more on the authentication part rather than on beautification. So, feel free to design the authentication to your taste. Now, we will create an `onSubmit` function that will send a `PUT` request to the `/api/auth` route and redirect the user to the `protected` when the request is successful. ```js //Path: pages/signup.js import { useEffect, useState } from 'react'; export default function Signup() { const [name, setName] = useState(''); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const handleChange = (e, func) => { func(e.target.value); }; const onSubmit = async e => { //Handling the registration e.preventDefault(); const response = await fetch('api/auth', { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ name, email, password, }), }); const data = await response.json(); if (data.success === true) return router.push('/protected'); alert(data.message); }; return ( //The rest of the code ); } ``` Let's Handle the login page next. ```js import { useState } from 'react'; import { useRouter } from 'next/router'; export default function Login() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const router = useRouter(); const handleChange = (e, func) => { func(e.target.value); }; const onSubmit = async e => { //Handling the login e.preventDefault(); const response = await fetch('api/auth', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ email, password, }), }); const data = await response.json(); if (data.success === true) return router.push('/protected'); alert(data.message); }; return ( <> <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh', fontSize: '30px', }} > <form onSubmit={onSubmit}> <u> <h1>Login</h1> </u> <label htmlFor="email">Email: </label> <input type="email" name="email" value={email} style={{ marginBottom: '10px', height: '20px', fontSize: '20px', outline: 'none', }} id="email" required onChange={e => handleChange(e, setEmail)} /> <br /> <label htmlFor="password">Password: </label> <input type="password" name="password" value={password} style={{ marginBottom: '10px', height: '20px', fontSize: '20px', outline: 'none', }} id="password" minLength="6" required onChange={e => handleChange(e, setPassword)} /> <br /> <button type="submit" style={{ width: '100px', height: '30px' }}> Submit </button> </form> </div> </> ); } ``` Now, let's add the following to the route we are protecting, the **protected.js** file. ```js //Path: pages/protected.js export default function App() { return ( <h2>This is a Protected route. Unauthorized users can't see this</h2> ); } ``` Lastly, the **index.js** file. ```js //Path: pages/index.js ``` ### Sessions in JWT Here, we'll handle sessions using `JWT` and `cookie`. Once users log in or sign up, we will generate a token using JWT and store this token as a cookie in the application. `jose` needs a set of strings, mostly random strings, to create the token. For security reasons, open the **next.config.js** file and store your **secret** in an environmental variable as shown below: ```js //Path: next.config.js /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: false, env: { MONGO_URI: 'mongodb+srv://admin:<password>@mfa.example.mongodb.net/', SECRET: 'OpenReplay', }, }; module.exports = nextConfig; ``` Above, we made use of `OpenReplay` as our set of random strings. Feel free to use something different. Let's generate and store the token for users that just signed up. Open the **auth.js** file and add the following. ```js //Path: pages/api/auth.js //The rest of the code const secret = process.env.SECRET; //Getting the secret //The rest of the code case 'PUT': //handler for PUT requests try { const check = await User.findOne({ email: req.body.email }); //Checking if the email exist if (check) { res .status(400) .json({ success: false, message: 'Email already exists!' }); } else { await User.create(req.body); //Adding the user to the database const iat = Math.floor(Date.now() / 1000); const exp = iat + 60 * 60 * 24; // 1day const token = await new SignJWT({ key: 'test' }) .setProtectedHeader({ alg: 'HS256', typ: 'JWT' }) .setExpirationTime(exp) .setIssuedAt(iat) .setNotBefore(iat) .sign(new TextEncoder().encode(secret)); //Generating the token const serialized = cookie.serialize('token', token, { httpOnly: true, maxAge: 60 * 60 * 24, path: '/', }); //Serializing the token res.setHeader('Set-Cookie', serialized); //Setting the cookie res .status(201) .json({ success: true, message: 'Account created successfully' }); } } catch (error) { console.log(error.message); res.status(400).json({ success: false, message: 'PUT request Error' }); } break; //The rest of the code ``` Next, we generate the token for Logged in Users ```js //Path: pages/api/auth.js //The rest of the code const secret = process.env.SECRET; //Getting the secret //The rest of the code case 'POST': //handler for POST requests try { const user = await User.findOne({ email: req.body.email }); //Checking if the email exist with a password if (user) { if (user.password === req.body.password) { //Checking the password provided is the same as the one in the database const iat = Math.floor(Date.now() / 1000); const exp = iat + 60 * 60 * 24; // 1day const token = await new SignJWT({ key: 'test' }) .setProtectedHeader({ alg: 'HS256', typ: 'JWT' }) .setExpirationTime(exp) .setIssuedAt(iat) .setNotBefore(iat) .sign(new TextEncoder().encode(secret)); //Generating the token const serialized = cookie.serialize('token', token, { httpOnly: true, maxAge: 60 * 60 * 24, path: '/', }); //Serializing the token res.setHeader('Set-Cookie', serialized); //Setting the cookie res .status(200) .json({ success: true, message: 'Login successful' }); } else { res .status(401) .json({ success: false, message: 'Invalid Email or Password' }); } } else { res .status(401) .json({ success: false, message: 'Invalid Email or Password' }); } } catch (error) { console.log(error); res.status(400).json({ success: false, message: 'POST request Error' }); } break; //The rest of the code ``` From the codes above, we created the token by passing the following basic parameter: - Type of token - `JWT` - Token expiration time - `1day` - JWT Secret - `OpenReplay`(`env` gotten from the **next.config.js** file) Next, we created the cookie using the token and set its time limit (one day) Testing out the application would store the cookie as shown in the output below. ![Token](https://i.imgur.com/rSWA6ZY.jpg) ### Protecting routes Any user can access the `protected` page with or without the token. We need to stop this and create a mechanism that allow access when the token is present. We will make use of the [NextJS Middleware](https://nextjs.org/docs/advanced-features/middleware) to do this. To do this, we will create a file in the project's directory named `middleware.js`. > This name is not optional. NextJS supports either middleware.js or middleware.ts ```js //Path: middleware.js import { NextResponse } from 'next/server'; import { jwtVerify } from 'jose'; const secret = process.env.SECRET; //Importing the secret export const config = { //Setting the supported route matcher: ['/login', '/signup', '/protected'], }; export default async function Middleware(req, res) { const token = req.cookies.get('token')?.value; //Getting the token from the cookie const url = new URL(req.url); const tokenChecker = async () => { if (token) { try { const verified = await jwtVerify( //Verifying the token token, new TextEncoder().encode(secret) ); return verified; } catch (error) { return null; } } else { return null; } }; if (url.pathname === '/protected') { //Checking the present url const checker = await tokenChecker(); if (checker) { return null; } else { return NextResponse.redirect(new URL('/', req.url)); } } NextResponse.next(); } ``` We specified the routes that this middleware applied to using the `matcher` option. Then we got the token from the `cookie` and verified the token using `jwtVerify`, a module in `jose`. Next, we redirected users trying to access the `/protected` route without a token or an invalid token to the `/` page. After protecting the `/protected` route, we need redirected users with valid tokens trying to access the `login` or `signup` page to the `/protected` page. ```js //Path: middleware.js //The rest of the code if (url.pathname === '/login' || url.pathname === '/signup') { const checker = await tokenChecker(); if (checker) { return NextResponse.redirect(new URL('/protected', req.url)); } } //The rest of the code. ``` ### Logout Here, we will handle the `logout` function in the application. Once the user clicks on the logout button, we will send a `GET` request to the `api/auth` route. Immediately we get this request, we will set the time limit of the cookie to *1sec* and redirect the user to the login Page. Now open your **protected.js** file and create the button. ```js //Path: pages/protected.js import { useRouter } from 'next/router'; export default function App() { const router = useRouter(); const onLogout = async () => { const response = await fetch('api/auth'); const data = await response.json(); if (data.success === true) return router.push('/login'); alert(data.message); }; return ( <> <h2>This is a Protected route. Unauthorized users can't see this</h2> <button onClick={onLogout}>Logout</button> </> ); } //The rest of the code ``` Next, open the **auth.js** file and handle the logout request. ```js //Path: pages/api/auth.js case 'GET': try { const serialized = cookie.serialize('token', null, { httpOnly: true, maxAge: 1, //Deleting the cookie after 1 second path: '/', }); //Serializing the token res.setHeader('Set-Cookie', serialized); //Setting the cookie res.redirect('/login'); } catch (error) {} break; //The rest of the code ``` ## Creating the second layer of authentication In the previous part of this tutorial, we authenticated users using `JWT` and their credentials. Now, we will authenticate users using OTP. ### Setting up Nodemailer [Nodemailer](https://nodemailer.com/) is a NodeJS module that sends emails to users. Setting up nodemailer requires an email service or an SMTP service and this tutorial makes use of the Google mailing service. > Go [Gmail's](https://accounts.google.com/signup) signup page to create an account if you don't have one. Now that your Gmail account is set, you need to enable [2-Step Verification](https://myaccount.google.com/u/2/security). ![2-Step Verification](https://i.imgur.com/z9A0QLL.jpg) Next, click on **GET STARTED** ![GET STARTED](https://i.imgur.com/ZRh2qkD.jpg) You can choose to use the following option to set up your 2-step verification process: - Getting a notification in your already connected devices - Using a security key - Getting a verification code. ![2-Step Verification Option](https://i.imgur.com/QOYRPto.jpg) Once you are done, you should get a similar output as shown below. ![2-Step Verification success](https://i.imgur.com/LWnfYR0.png) Next, you need to set an **App password** in the [Signing in to Google]((https://myaccount.google.com/u/2/security) option ![Signing in to Google](https://i.imgur.com/m4BRbC5.jpg) Click on the **Select app** option and choose **Other (Custom name)** ![Custom name](https://i.imgur.com/FHPqYvP.jpg) Enter **Nodemailer** or any desired name, click **GENERATE** and copy the generated **16 digits password**. ![GENERATE App password](https://i.imgur.com/hWZbZdr.png) > Ensure you copy the password because you can't retrieve it. You can store this password in the **next.config.js** file With this, we can create the mechanism for sending emails. Open your **email.js** file and let's configure nodemailer ```js //Path: pages/api/email.js import nodemailer from 'nodemailer'; export default function Verify(req, res) { const transporter = nodemailer.createTransport({ service: 'gmail', //Specifying the service auth: { user: 'yourgmail@gmail.com', //Your Gmail pass: process.env.PASSWORD, //Your app password }, }); const mailOptions = { from: 'yourgmail@gmail.com', to: req.body.email,//Getting the user's email subject: 'Subject', text: 'OTP',//Passing the OTP }; transporter.sendMail(mailOptions, function (error, info) { if (error) { console.log(error); res.status(400).json({ success: false, message: error }); } else { console.log('Email sent: ' + info.response); res.status(201).json({ success: true, message: info.response }); } }); } ``` > Source: [Use Gmail with Nodemailer](https://miracleio.me/snippets/use-gmail-with-nodemailer/) ### Generating the OTP We are done setting up `nodemodule`, now we need to generate the OTP that will be sent to users' email once they log in or sign up. Before we do that, let's refactor our application. - We need to redirect users from the `login` or `signup` route to the `verify` page for the 2nd layer of authentication. ```js //Path: pages/login.js //The rest of the code const onSubmit = async e => { //Handling the login e.preventDefault(); const response = await fetch('api/auth', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ email, password, }), }); const data = await response.json(); if (data.success === true) { try { const res = await fetch('api/email', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ email, }), }); const sent = await res.json(); sent.success === true ? router.push('/verify') : alert('an error occurred'); } catch (error) { console.log(error); } } else { alert(data.message); } }; //The rest of the code ``` Same goes for the **signup** page ```js //Path: pages/signup.js //The rest of the code const onSubmit = async e => { //Handling the registration e.preventDefault(); const response = await fetch('api/auth', { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ name, email, password, }), }); const data = await response.json(); if (data.success === true) { try { const res = await fetch('api/email', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ email, }), }); const sent = await res.json(); sent.success === true ? router.push('/verify') : alert('an error occured'); } catch (error) { console.log(error); } } else { alert(data.message); } }; //The rest of the code ``` - Next, we will take out the token and cookie lines of code from the auth.js file. We need the creation of tokens and cookies to be done after they've entered a valid OTP. ```js //Path: pages/api/auth.js //The rest of the code case 'PUT': //handler for PUT requests try { const check = await User.findOne({ email: req.body.email }); //Checking if the email exist if (check) { res .status(400) .json({ success: false, message: 'Email already exists!' }); } else { await User.create(req.body); //Adding the user to the database res .status(201) .json({ success: true, message: 'Account created successfully' }); } } catch (error) { console.log(error.message); res.status(400).json({ success: false, message: 'PUT request Error' }); } break; case 'POST': //handler for POST requests try { const user = await User.findOne({ email: req.body.email }); //Checking if the email exist with a password if (user) { if (user.password === req.body.password) { //Checking the password provided is the same with the one in the databse res .status(200) .json({ success: true, message: 'Login successful' }); } else { res .status(401) .json({ success: false, message: 'Invalid Email or Password' }); } } else { res .status(401) .json({ success: false, message: 'Invalid Email or Password' }); } } catch (error) { console.log(error); res.status(400).json({ success: false, message: 'POST request Error' }); } break; //The rest of the code ``` Let's add the following code to the **verify.js** file ```js //Path: pages/verify.js import { useRouter } from 'next/router'; import { useState } from 'react'; export default function Verify() { const [text, setText] = useState(''); const router = useRouter(); const handleChange = (e, func) => { func(e.target.value); }; const onSubmit = async e => { //Handling Verification e.preventDefault(); const response = await fetch('api/verify', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ OTP: text, }), }); const data = await response.json(); data.success === true ? router.push('/protected') : alert('Invalid OTP'); }; return ( <> <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh', fontSize: '30px', }} > <form onSubmit={onSubmit}> <u> <h1>Verify</h1> </u> <label htmlFor="email">OTP: </label> <input type="text" name="text" value={text} style={{ marginBottom: '10px', height: '20px', fontSize: '20px', outline: 'none', }} id="text" required onChange={e => handleChange(e, setText)} /> <br /> <button type="submit" style={{ width: '100px', height: '30px' }}> Submit </button> </form> </div> </> ); } ``` From the above code in the **verify.js** file, we sent a `POST` request to `api/verify` route, passing the OTP the user provided as the body params. Once we are done refactoring, we can generate the OTP and send it to the user's email. ```js //Path: pages/api/email.js import nodemailer from 'nodemailer'; import { customAlphabet } from 'nanoid'; const nanoid = customAlphabet('1234567890', 8); export default function Verify(req, res) { const OTP = nanoid(); const transporter = nodemailer.createTransport({ service: 'gmail', //Specifying the service auth: { user: 'yourgmail@gmail.com', //Your Gmail pass: process.env.PASSWORD, //Your app password }, }); const mailOptions = { from: 'yourgmail@gmail.com', to: req.body.email, //Getting the user's email subject: 'One Time Password', text: `Here is your OTP: ${OTP}`, //Sending the OTP }; transporter.sendMail(mailOptions, function (error, info) { if (error) { console.log(error); res.status(400).json({ success: false, message: error }); } else { console.log('Email sent: ' + info.response); res.status(201).json({ success: true, message: info.response }); } }); } ``` Once we send the OTP to the user's email, let's encrypt the OTP and store it as cookie. ```js //Path: pages/api/email.js import nodemailer from 'nodemailer'; import { customAlphabet } from 'nanoid'; import { SignJWT } from 'jose'; import cookie from 'cookie'; const secret = process.env.SECRET; const nanoid = customAlphabet('1234567890', 8); export default async function Verify(req, res) { const OTP = nanoid(); const iat = Math.floor(Date.now() / 1000); const exp = iat + 60 * 60 * 24; // 1day const OTPToken = await new SignJWT({ OTP }) .setProtectedHeader({ alg: 'HS256', typ: 'JWT' }) .setExpirationTime(exp) .setIssuedAt(iat) .setNotBefore(iat) .sign(new TextEncoder().encode(secret)); //Generating the token const serialized = cookie.serialize('OTPToken', OTPToken, { httpOnly: true, maxAge: 60 * 60 * 24, path: '/', }); //Serializing the token res.setHeader('Set-Cookie', serialized); //Setting the cookie //The rest of the code ``` After we've encrypted the OTP, let's compare the OTP provided by the user to the OTP stored as cookie in the **verify.js** file. ```js //Path: pages/api/verfiy.js import { SignJWT, jwtVerify } from 'jose'; import cookie from 'cookie'; const secret = process.env.SECRET; export default async function Verify(req, res) { const OTP = req.cookies.OTPToken; //Getting the token form the cookie console.log(OTP); if (OTP) { try { const OTPVerify = await jwtVerify( //Verifying the token OTP, new TextEncoder().encode(secret) ); console.log(OTPVerify.payload.OTP, req.body.OTP); if (OTPVerify.payload.OTP === req.body.OTP) { return res .status(200) .json({ success: true, message: 'OTP is correct' }); } res.status(400).json({ success: false, message: 'OTP is not correct' }); } catch (error) { console.log(error); res.status(401).json({ success: false, message: 'Invalid OTP', error }); } } else { res.status(403).json({ success: false, message: 'OTP does not exist' }); } } ``` Lastly, we will create the token and delete the `OTPToken` when the provided `OTP` is valid. ```js //Path: //Path: pages/api/verfiy.js //The rest of the code if (OTP) { try { const OTPVerify = await jwtVerify( //Verifying the token OTP, new TextEncoder().encode(secret) ); console.log(OTPVerify.payload.OTP, req.body.OTP); if (OTPVerify.payload.OTP === req.body.OTP) { const iat = Math.floor(Date.now() / 1000); const exp = iat + 60 * 60 * 24; // 1day const token = await new SignJWT({ key: 'test' }) .setProtectedHeader({ alg: 'HS256', typ: 'JWT' }) .setExpirationTime(exp) .setIssuedAt(iat) .setNotBefore(iat) .sign(new TextEncoder().encode(secret)); //Generating the token const serialized = cookie.serialize('token', token, { httpOnly: true, maxAge: 60 * 60 * 24, path: '/', }); //Serializing the token const OTPserialized = cookie.serialize('OTPToken', null, { httpOnly: true, maxAge: 1, path: '/', }); //Serializing the token res.setHeader('Set-Cookie', [serialized, OTPserialized]); //Setting the cookie return res .status(200) .json({ success: true, message: 'OTP is correct' }); } res.status(400).json({ success: false, message: 'OTP is not correct' }); } catch (error) { console.log(error); res.status(401).json({ success: false, message: 'Invalid OTP', error }); } } else { res.status(403).json({ success: false, message: 'OTP does not exist' }); } ``` ## Conclusion This tutorial was aimed at teaching you how to set up multi-factor authentication in your application. In this tutorial, you learned how to set up MongoDB, connect it to the application, handle user registration/login/logout, handle sessions using token & cookie and One-Time-Password using nodemailer. ## Resources - [GitHub Repo](https://github.com/divofred/multi-factor-auth) - [MongoDB with NextJS](https://blog.openreplay.com/a-complete-guide-to-nextjs-plus-mongodb/)

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully