Multi Factor authentication using MEAN
Multi-factor authentication is important and common in every website or app to securely login the user. In this article, we will see how we can implement Multi-factor authentication using MEAN Stack. MEAN Stack includes the use of Angular for frontend, Node JS and Express for backend, and MongoDB as the database.
Project Preview:
Prerequisites:
Approach to Create Address Book using MEAN Stack
Backend:
- Set up a new node js project
- Set up the server using express with CORS as the middleware in file server.js
- Create the app instance using const app = express()
- Twilio a cloud communication platform used to send or receive text message which can help developers to implement the multi-factor authentication functionality.
- Set up a twilio account and get the configuration details
- Create controllers folder which will define the API methods
- Create models folder to create the database schema for user
- Create router folder and mention all the routes related to user login, register and verify for checking the code entered by user
- Set up local Mongo DB database.
- Set up the connection to local Mongo DB in server.js file
- Create collections within the database to store and retrieve the data from database
- One collection is created – user
- Create .env files for storing the environment variables for config details related to twilio account, database and port
- Implement the core logic of register, login and generation and verification of the code
- Test the API endpoints using postman
Frontend:
- Create a new angular project
- Create components folder and seperate components for seperate routes – user component for login and register functionality and verify component for verification code feature
- Create HTML, CSS and ts files for all the components
- Create service to establish communication between frontend and backend routes
- Create various routes in app.routes.ts folder for login, register and verify
- Test the frontend application in browser at https://localhost:3000
Steps to Create the Application
Step 1: Create the main folder for complete project
mkdir multi-factor-auth
cd multi-factor-auth
Step 2: Initialize the node.js project
npm init -y
Step 3: Install the required dependencies
npm install express mongoose jsonwebtoken bcryptjs nodemon twilio cors
Project Structure (Backend):
The updated dependencies in package.json file of backend will look like:
"dependencies": {
"bcryptjs": "^2.4.3",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.3.1",
"twilio": "^5.0.3"
}
Example: Create the required files as seen on the project structure and add the following codes.
// authController.js
const User = require("../model/authModel");
const bcrypt = require("bcryptjs");
const jwt = require("jsonwebtoken");
const twilio = require("twilio");
require("dotenv").config({ path: __dirname + "/config/.env" });
const twilioClient = twilio(
process.env.TWILIO_ACCOUNT_SID,
process.env.TWILIO_AUTH_TOKEN
);
exports.register = async (req, res) => {
try {
const { username, email, password, phone } = req.body;
let user = await User.findOne({ email });
if (user) {
return res.status(400).json({
message: "User Already Exist",
success: false,
});
}
user = new User({
username: username,
email: email,
password: password,
phone: phone,
});
const salt = await bcrypt.genSalt(10);
user.password = await bcrypt.hash(password, salt);
await user.save();
const token = generateJwtToken(user._id);
res.status(201).json({
success: true,
token: token,
message: "User registered successfully",
});
} catch (error) {
res.status(500).json({
message: "Server error! New user registration failed",
success: false,
});
}
};
exports.login = async (req, res) => {
try {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user) {
return res.status(400).json({
message: "Invalid credentials",
success: false,
});
}
const isMatched = await bcrypt.compare(password, user.password);
if (!isMatched) {
return res.status(400).json({
message: "Invalid credentials",
success: false,
});
}
const verificationCode = Math.floor(100000 + Math.random() * 900000);
const codeExpires = new Date();
codeExpires.setMinutes(codeExpires.getMinutes() + 10);
user.loginCode = verificationCode.toString();
user.codeExpires = codeExpires;
await user.save();
await twilioClient.messages.create({
body: `Your login verification code is ${verificationCode}`,
from: process.env.TWILIO_PHONE_NUMBER,
to: `+91` + user.phone,
});
return res.status(200).json({
message: "Verification code sent to your phone.",
userId: user._id,
});
} catch (error) {
return res.status(500).json({
success: false,
message: "Internal Server Error, Login unsuccessful",
});
}
};
function generateJwtToken(userID) {
const payload = {
user: {
id: userID,
},
};
return jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: 3600 });
}
exports.verifyUser = async (req, res) => {
const { userId, code } = req.body;
console.log(userId);
try {
const user = await User.findById(userId);
if (!user) {
return res.status(404).send("User not found.");
}
const currentTime = new Date();
if (currentTime > user.codeExpires) {
return res
.status(400)
.send(
"Verification code has expired. Please login again to receive a new code."
);
}
if (user.loginCode !== code) {
return res.status(400).send("Invalid verification code.");
}
user.verified = true;
user.loginCode = null;
user.codeExpires = null;
await user.save();
const token = generateJwtToken(user._id);
res.status(200).json({
message: "User successfully verified.",
token: token,
});
} catch (error) {
res.status(500).json("Verification error: " + error.message);
}
};
// authModel.js
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
username: {
type: String,
required: true
},
email: {
type: String,
required: true,
uniques: true
},
password: {
type: String,
required: true
},
phone: {
type: Number,
required: true
},
verified: {
type: Boolean,
default: false,
required: false
},
loginCode: {
type: String,
default: "",
required: false
},
codeExpires: {
type: Date,
required: false
}
});
module.exports = mongoose.model('User', userSchema);
// authRoutes.js
const express = require('express');
const router = express.Router();
const authController = require('../controller/authController');
router.post('/register', authController.register);
router.post('/login', authController.login);
router.post('/verify', authController.verifyUser);
module.exports = router;
// server.js
const express = require("express");
const cors = require("cors");
const mongoose = require("mongoose");
const authRoutes = require("../backend/route/authRoute");
const dotenv = require("dotenv");
dotenv.config({ path: "../backend/config/.env" });
const app = express();
app.use(cors());
app.use(express.json());
mongoose
.connect(process.env.DB_URI, {
family: 4,
})
.then(() => console.log("Mongo DB connected"))
.catch((err) => console.log(err));
app.use("/api/auth", authRoutes);
const PORT = process.env.PORT;
app.listen(PORT, () => console.log(`Server started on port ${PORT}`));
// .env file
PORT=5000
DB_URI=mongodb://localhost:27017/multi-factor-auth
JWT_SECRET=jwtSecret
TWILIO_ACCOUNT_SID=YOUR_TWILIO_ACCOUNT_SID
TWILIO_AUTH_TOKEN=YOUR_TWILIO_AUTH_TOKEN
TWILIO_PHONE_NUMBER=YOUR_TRIAL_TWILIO_PHONE_NUMBER
To start the backend run the following command.
nodemon server.js
Step 5: Install the angular CLI
npm install -g @angular/cli
Step 6: Create a new angular project
ng new frontend
Step 7: Create components for different functionalities in angular
Syntax - ng generate component <component-name>
ng generate component user
ng generate component verify
Step 8: Create services for communication between frontend and backend
Syntax - ng generate service <service-name>
ng generate service user
ng generate service shared
Project Structure (Frontend):
The updated dependencies in package.json file of frontend will look like:
"dependencies": {
"@angular/animations": "^17.2.0",
"@angular/common": "^17.2.0",
"@angular/compiler": "^17.2.0",
"@angular/core": "^17.2.0",
"@angular/forms": "^17.2.0",
"@angular/platform-browser": "^17.2.0",
"@angular/platform-browser-dynamic": "^17.2.0",
"@angular/platform-server": "^17.2.0",
"@angular/router": "^17.2.0",
"@angular/ssr": "^17.2.3",
"@auth0/angular-jwt": "^5.2.0",
"express": "^4.18.2",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.3"
}
Example: Create the required files as seen in project structure and add the following codes.
<!-- user.component.html -->
<div class="error-message" *ngIf="errorMessage">{{ errorMessage }}</div>
<div class="success-message" *ngIf="successMessage">{{ successMessage }}</div>
<div class="container" *ngIf="loginActive">
<h2>Login</h2>
<form (ngSubmit)="login()">
<div class="form-group">
<label for="email">Email:</label>
<input
type="email"
class="form-control"
id="email"
name="email"
[(ngModel)]="email"
required
/>
</div>
<div class="form-group">
<label for="password">Password:</label>
<input
type="password"
class="form-control"
id="password"
name="password"
[(ngModel)]="password"
required
/>
</div>
<button type="submit" class="btn btn-primary">Login</button>
</form>
</div>
<div class="container" *ngIf="registerActive">
<h2>Register</h2>
<form (submit)="register()">
<div class="form-group">
<label for="username">Username</label>
<input
type="text"
id="username"
class="form-control"
[(ngModel)]="username"
name="username"
required
/>
</div>
<div class="form-group">
<label for="email">Email</label>
<input
type="email"
id="email"
class="form-control"
[(ngModel)]="email"
name="email"
required
/>
</div>
<div class="form-group">
<label for="password">Password</label>
<input
type="password"
id="password"
class="form-control"
[(ngModel)]="password"
name="password"
required
/>
</div>
<div class="form-group">
<label for="phone">Phone Number</label>
<input
type="number"
id="phone"
class="form-control"
[(ngModel)]="phone"
name="phone"
required
/>
</div>
<button type="submit" class="btn btn-primary">Register</button>
</form>
</div>
<!-- verify.component.html -->
<div class="verify-container">
<h3>Please Enter your Verification Code</h3>
<form (ngSubmit)="onVerify()">
<div>
<label for="code">Verification Code: </label>
<input type="text" id="code" [(ngModel)]="code" name="code" required>
</div>
<button type="submit">Verify</button>
<p *ngIf="errorMessage">{{ errorMessage }}</p>
</form>
</div>
<!-- dashboard.component.html -->
<div class="verify-container">
<h3>Please Enter your Verification Code</h3>
<form (ngSubmit)="onVerify()">
<div>
<label for="code">Verification Code: </label>
<input type="text" id="code" [(ngModel)]="code" name="code" required>
</div>
<button type="submit">Verify</button>
<p *ngIf="errorMessage">{{ errorMessage }}</p>
</form>
</div>
<!-- app.component.html -->
<main class="main">
<div class="content">
<div class="left-side">
<h1>{{ title }}</h1>
<div>
<ul>
<li><a (click)="login()" *ngIf="!isLoggedIn">Login</a></li>
<li><a (click)="register()" *ngIf="!isLoggedIn">Register</a></li>
<li><a (click)="logout()" *ngIf="isLoggedIn">Logout</a></li>
</ul>
</div>
</div>
</div>
</main>
<router-outlet>
</router-outlet>
/* user.component.css */
.container {
width: 50%;
margin: 2rem auto;
padding: 1.5vmax;
padding-right: 2.5vmax;
border: 1px solid #ccc;
border-radius: 5px;
}
h2 {
text-align: center;
margin-bottom: 20px;
font-size: 2rem;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
}
input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"] {
width: 97%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
button[type="submit"] {
width: 20%;
padding: 1.1vmax;
background-color: #0056b3;
color: #fff;
border: none;
border-radius: 5px;
cursor: pointer;
font-weight: bold;
font-size: 1rem;
align-self: center;
margin-top: 1vmax;
}
.container {
width: 50%;
margin: 2rem auto;
padding: 1.5vmax;
padding-right: 3.5vmax;
border: 1px solid #ccc;
border-radius: 5px;
}
h2 {
text-align: center;
margin-bottom: 20px;
font-size: 2rem;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
}
input[type="email"],
input[type="password"] {
width: 99%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
button[type="submit"] {
width: 20%;
padding: 1.1vmax;
background-color: #0056b3;
color: #fff;
border: none;
border-radius: 5px;
cursor: pointer;
font-weight: bold;
font-size: 1rem;
align-self: center;
margin-top: 1vmax;
}
.error-message {
color: #FF0000;
background-color: #FFEFEF;
padding: 10px;
border: 1px solid #FF0000;
border-radius: 5px;
margin-bottom: 10px;
margin-top: 10px;
}
.success-message {
color: green;
background-color: rgb(186, 218, 186);
padding: 10px;
border: 1px solid green;
border-radius: 5px;
margin-bottom: 10px;
margin-top: 10px;
}
/* verify.component.css */
.verify-container {
width: 100%;
max-width: 400px;
margin: 50px auto;
padding: 20px;
background-color: #f8f9fa;
border: 1px solid #ced4da;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.verify-container h3 {
text-align: center;
color: #333;
margin-bottom: 20px;
}
.verify-container form {
display: flex;
flex-direction: column;
}
.verify-container label {
font-size: 16px;
color: #495057;
margin-bottom: 5px;
}
.verify-container input[type="text"] {
height: 38px;
width: 63%;
padding: 6px 12px;
font-size: 16px;
color: #495057;
background-color: #fff;
border: 1px solid #ced4da;
border-radius: 4px;
margin-bottom: 20px;
}
.verify-container button {
background-color: #3b5775;
color: white;
border: none;
padding: 10px 15px;
font-size: 16px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}
.verify-container button:hover {
background-color: #0056b3;
}
.verify-container p {
color: #dc3545;
font-size: 14px;
margin-top: 0;
}
/* dashboard.component.css */
.dashboard-container {
margin: 20px;
padding: 20px;
border: 1px solid green;
border-radius: 5px;
text-align: center;
color: green;
background-color: rgb(164, 220, 164);
}
/* app.component.css */
.main {
display: flex;
flex-direction: column;
align-items: center;
background-color: darkslategrey;
color: white;
}
.content {
width: 100%;
max-width: 1200px;
padding: 20px;
}
.left-side {
display: flex;
justify-content: space-between;
align-items: center;
}
.left-side h1 {
margin: 0;
margin-left: 3%;
}
.left-side ul {
list-style-type: none;
padding: 0;
margin: 0;
}
.left-side li {
display: inline;
margin-right: 20px;
}
.left-side li a {
text-decoration: none;
color: white;
font-weight: bold;
font-size: 1.5rem;
}
.left-side li a:hover {
color: lightgray;
}
a {
cursor: pointer;
}
// user.component.ts
import { Component, OnInit } from "@angular/core";
import { UserService } from "../../services/user.service";
import { Router } from "@angular/router";
import { SharedService } from "../../services/shared.service";
import { FormsModule } from "@angular/forms";
import { CommonModule } from "@angular/common";
@Component({
selector: "app-user",
standalone: true,
imports: [FormsModule, CommonModule],
templateUrl: "./user.component.html",
styleUrl: "./user.component.css",
})
export class UserComponent implements OnInit {
username!: string;
email!: string;
password!: string;
phone: number = 0;
credentials: any = {};
successMessage: string = "";
errorMessage: string = "";
loginActive: boolean = true;
registerActive: boolean = false;
constructor(
private userService: UserService,
private router: Router,
private sharedService: SharedService
) { }
ngOnInit(): void {
this.sharedService.loginEvent.subscribe(() => {
this.loginActive = true;
this.registerActive = false;
this.username = "";
this.email = "";
this.password = "";
this.successMessage = "";
this.errorMessage = "";
});
this.sharedService.registerEvent.subscribe(() => {
this.registerActive = true;
this.loginActive = false;
this.username = "";
this.email = "";
this.password = "";
this.successMessage = "";
this.errorMessage = "";
});
}
login(): void {
const credentials = {
email: this.email,
password: this.password,
};
this.userService.login(credentials).subscribe(
(response: any) => {
this.loginActive = false;
this.registerActive = false;
this.router.navigate([`/verify/${response.userId}`]);
this.successMessage = response.message;
this.errorMessage = "";
},
(error: any) => {
console.error("Error logging in:", error);
this.errorMessage =
"Login unsuccessfull ! Please reload or try in incognito tab";
this.successMessage = "";
}
);
}
register(): void {
const userData = {
username: this.username,
email: this.email,
password: this.password,
phone: this.phone,
};
this.userService.register(userData).subscribe(
(response: any) => {
this.successMessage = response.message;
this.errorMessage = "";
this.loginActive = true;
this.registerActive = false;
},
(error: any) => {
this.errorMessage = "User not registered successfully";
this.successMessage = "";
}
);
}
}
// verify.component.ts
import { Component, OnInit } from "@angular/core";
import { UserService } from "../../services/user.service";
import { ActivatedRoute, Router } from "@angular/router";
import { FormsModule } from "@angular/forms";
import { CommonModule } from "@angular/common";
@Component({
selector: "app-verify",
standalone: true,
imports: [FormsModule, CommonModule],
templateUrl: "./verify.component.html",
styleUrls: ["./verify.component.css"],
})
export class VerifyComponent implements OnInit {
userId: string = "";
code: string = "";
errorMessage: string = "";
successMessage: string = "";
constructor(
private authService: UserService,
private router: Router,
private route: ActivatedRoute
) { }
ngOnInit() {
this.route.params.subscribe((params: any) => {
this.userId = params["id"];
});
}
onVerify(): void {
this.authService.verifyCode(this.userId, this.code).subscribe({
next: (response: any) => {
console.log("Verification successful", response);
this.errorMessage = "";
const token = response.token;
localStorage.setItem("token", token);
this.authService.setAuthenticationStatus(true);
this.authService.emitLoggedInEvent();
this.successMessage = response.message;
this.router.navigate(["/dashboard"]);
},
error: (error: any) => {
this.errorMessage = "Verification failed";
console.error("Error during verification", error);
},
});
}
}
// dashboard.component.ts
import { Component, OnInit } from "@angular/core";
@Component({
selector: "app-dashboard",
standalone: true,
imports: [],
templateUrl: "./dashboard.component.html",
styleUrl: "./dashboard.component.css",
})
export class DashboardComponent implements OnInit {
message: string;
constructor() {
this.message = "You are logged in successfully!";
}
ngOnInit(): void { }
}
// user.service.ts
import { HttpClient } from "@angular/common/http";
import { EventEmitter, Injectable } from "@angular/core";
import { BehaviorSubject, Observable } from "rxjs";
@Injectable({
providedIn: "root",
})
export class UserService {
private baseUrl = "http://localhost:5000/api/auth";
constructor(private httpClient: HttpClient) { }
register(userData: any): Observable<any> {
return this.httpClient.post(`${this.baseUrl}/register`, userData);
}
login(credentials: any): Observable<any> {
return this.httpClient.post(`${this.baseUrl}/login`, credentials);
}
private isAuthenticatedSubject = new BehaviorSubject<boolean>(false);
isAuthenticated(): Observable<boolean> {
return this.isAuthenticatedSubject.asObservable();
}
setAuthenticationStatus(isAuthenticated: boolean): void {
this.isAuthenticatedSubject.next(isAuthenticated);
}
loggedInEvent: EventEmitter<any> = new EventEmitter();
emitLoggedInEvent() {
this.loggedInEvent.emit();
}
verifyCode(userId: string, code: string): Observable<any> {
return this.httpClient.post(`${this.baseUrl}/verify`, { userId, code });
}
}
// shared.service.ts
import { EventEmitter, Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class SharedService {
loginEvent: EventEmitter<void> = new EventEmitter<void>();
registerEvent: EventEmitter<void> = new EventEmitter<void>();
constructor() { }
triggerLoginEvent(): void {
this.loginEvent.emit();
}
triggerRegisterEvent(): void {
this.registerEvent.emit();
}
}
// app.routes.ts
import { Routes } from "@angular/router";
import { UserComponent } from "./components/user/user.component";
import { VerifyComponent } from "./components/verify/verify.component";
import { DashboardComponent } from "./components/dashboard/dashboard.component";
export const routes: Routes = [
{ path: "", component: UserComponent },
{ path: "verify/:id", component: VerifyComponent },
{ path: "dashboard", component: DashboardComponent },
{ path: "**", redirectTo: "/" },
];
// app.module.ts
import { InjectionToken, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { JwtHelperService, JWT_OPTIONS } from '@auth0/angular-jwt';
import { RouterModule } from '@angular/router';
import { routes } from './app.routes';
import { UserComponent } from './components/user/user.component';
import { VerifyComponent } from './components/verify/verify.component';
import { DashboardComponent } from './components/dashboard/dashboard.component';
@NgModule({
declarations: [
AppComponent,
UserComponent,
VerifyComponent,
DashboardComponent
],
imports: [
BrowserModule,
FormsModule,
RouterModule.forRoot(routes),
],
exports: [RouterModule],
providers: [{ provide: JWT_OPTIONS, useValue: JWT_OPTIONS }, JwtHelperService],
bootstrap: [AppComponent]
})
export class AppModule { }
// app.component.ts
import { Component } from "@angular/core";
import { Router, RouterOutlet } from "@angular/router";
import { SharedService } from "./services/shared.service";
import { UserService } from "./services/user.service";
import { FormsModule } from "@angular/forms";
import { CommonModule } from "@angular/common";
@Component({
selector: "app-root",
standalone: true,
imports: [RouterOutlet, FormsModule, CommonModule],
templateUrl: "./app.component.html",
styleUrl: "./app.component.css",
})
export class AppComponent {
title = "Multi Factor Authentication";
isLoggedIn: boolean = false;
constructor(
private router: Router,
private userService: UserService,
private sharedService: SharedService
) { }
ngOnInit(): void {
this.userService.loggedInEvent.subscribe((data: any) => {
this.isLoggedIn = true;
});
if (typeof localStorage !== "undefined" && localStorage.getItem("token")) {
this.isLoggedIn = true;
}
}
login(): void {
this.sharedService.triggerLoginEvent();
this.router.navigate(["/"]);
}
register(): void {
this.sharedService.triggerRegisterEvent();
this.router.navigate(["/"]);
}
logout(): void {
this.userService.setAuthenticationStatus(false);
this.isLoggedIn = false;
localStorage.removeItem("token");
this.router.navigate(["/"]);
}
}
To start the application run the following command.
ng serve
Output: