Enhancing User Authentication: A Progressive Journey from Basic to Robust Security in a Course Selling Application

Enhancing Security and Exploring different Storage Options for a Node.js Application

ยท

8 min read

Enhancing User Authentication: A Progressive Journey from Basic to Robust Security in a Course Selling Application

So, you're developing a Node.js application? That's fantastic! When it comes to building web apps, authentication is a crucial aspect that cannot be overlooked. It ensures the security of user accounts and sensitive information.

In this blog, we'll explore the evolution of authentication in your Node.js application. We'll compare different storage options, including memory-based, file-based, and MongoDB-based solutions. By examining their strengths and weaknesses, we'll find the optimal and secure approach for your authentication needs.

Join us on this exciting journey as we navigate the intricacies of authentication in Node.js. Let's enhance your app's security and build a robust authentication system. Get ready to make your Node.js app shine! ๐Ÿš€

Setting the Stage

Node.js provides a powerful and versatile platform for building server-side applications. With its event-driven, non-blocking I/O model, Node.js enables developers to create scalable and high-performance applications. However, without a robust authentication system, your application could be vulnerable to security breaches and unauthorized access. In this article, we'll explore the journey we took to enhance the security of our Node.js application's authentication process.

The Naive Approach: Basic Authentication with In-Memory Array Storage

When we first embarked on our authentication journey, we took a simplistic approach. We relied on basic authentication, where users would send their credentials, such as usernames and passwords, in the headers of each request. While this method was quick to implement, it left our authentication system vulnerable to attacks. Malicious actors could easily intercept and misuse the transmitted credentials, compromising the security of our application.

Additionally, we stored user data in an in-memory array, which posed challenges in terms of data persistence and scalability.

let ADMINS = [];
let USERS = [];
let COURSES = [];

const adminAuth = (req,res,next) =>{
   const {username, password} = req.headers;
   const admin = ADMINS.find((adm)=>{
    return adm.username === username && adm.password === password;
   })
   if(admin){
    next();
   }else{
    res.status(403).json({message: `Admin Authentication Failed`});
   }
};

const userAuth = (req,res, next) =>{
   const {username, password} = req.headers;
   const user = USERS.find((usr)=>{
    return usr.username === username && usr.password === password;
   })
   if(user){
    req.user = user;
    next();
   }else{
    res.status(403).json({message: `User Authentication Failed`});
   }
};
app.get('/users/courses',userAuth, (req, res) => {
  // logic to list all courses
});

Enter JWT: Enhanced Security with Tokens

To address the vulnerabilities of basic authentication, we turned to JSON Web Tokens (JWT). JWT is a widely adopted standard that allows us to securely transmit information between parties as a compact and self-contained token. Instead of relying solely on the in-memory array, the server generates a JWT upon successful login and sends it to the client. The client includes this token in subsequent requests, allowing the server to verify the authenticity and authorization of the user or admin.

However, a persistent issue persisted despite our efforts โ€“ the challenge of data persistence. Whenever we restarted the server, the arrays storing user data would reset, resulting in the loss of valuable information. This limitation hindered the reliability and continuity of our authentication system.

const jwt = require("jsonwebtoken");

let ADMINS = [];
let USERS = [];
let COURSES = [];

const secretKey = "mysecretkey";
const generateJwt = (user) => {
  const payload = { username: user.username };
  return jwt.sign(payload, secretKey, {
    expiresIn: '1h',
  });
};

const authenticateJwt = (req, res, next) => {
  const token =
    req.cookies.token ||
    req.body.token ||
    req.headers.authorization.replace('Bearer ', '');

  if (!token) {
    return res.status(403).send('Token is missing');
  }
  jwt.verify(token, secretKey, (err, user) => {
    if (err) {
      return res.sendStatus(403);
    }
    req.user = user;
    next();
  });
};

app.post('/users/login', (req, res) => {
  const { username, password } = req.body;
  const user = USERS.find(u => u.username === username && u.password === password);

  if (user) {
    const token = generateJwt(user);
    res.json({ message: 'Logged in successfully', token });
  } else {
    res.status(403).json({ message: 'Invalid username or password' });
  }
});

app.get('/users/courses',authenticateJwt, (req, res) => {
  // logic to list all courses
});

Slightly Better Approach: Tokens with File-Based Storage

In this approach, we address the limitations of array storage by incorporating JSON files for improved data persistence and security. JSON files provide a more reliable and scalable solution for storing user, admin, and course data. This approach ensures that the data remains intact even if the server restarts and enables the application to handle larger datasets efficiently.

We modify the code to utilize JSON files for storing user, admin, and course data. The data is read from and written to these files, providing a persistent storage solution. By integrating the JWT authentication mechanism and middleware from the previous stage, we achieve enhanced security and data integrity in our course app.

const jwt = require('jsonwebtoken');
const fs = require('fs');
let ADMINS = [];
let USERS = [];
let COURSES = [];

// Read data from file, or initialize to empty array if file does not exist
try {
    ADMINS = JSON.parse(fs.readFileSync('admins.json', 'utf8'));
//utf8 -> content will be interpreted as unicode string, but we need to have object in array, so that is why we use json.parse
    USERS = JSON.parse(fs.readFileSync('users.json', 'utf8'));
    COURSES = JSON.parse(fs.readFileSync('courses.json', 'utf8'));
} catch {
    ADMINS = [];
    USERS = [];
    COURSES = []
}
const SECRET = 'my-secret-key';
const generateJwt = (user) => {
  // Logic to generate JWT token using the provided secret key and user data
};

const authenticateJwt = (req, res, next) => {
  // Middleware function to authenticate JWT token in requests
};

app.post('/users/signup', (req, res) => {
  const { username, password } = req.body;
  const user = USERS.find(u => u.username === username);
  if (user) {
    res.status(403).json({ message: 'User already exists' });
  } else {
    const newUser = { username, password };
    USERS.push(newUser);
    fs.writeFileSync('users.json', JSON.stringify(USERS));
    const token = generateJwt(user)
    res.json({ message: 'User created successfully', token });
  }
});

app.get('/users/courses', authenticateJwt, (req, res) => {
  res.json({ courses: COURSES });
});

While the file-based storage approach provided persistence, we encountered limitations as our application grew. Every server restart or refresh resulted in reading and writing data from files, leading to performance bottlenecks.

Robust Security and Performance: MongoDB and Securing password ๐Ÿง‚

To overcome the challenges in our previous approaches, we integrated MongoDB, a robust NoSQL database, into our authentication system. MongoDB's flexible document model allowed us to efficiently manage user accounts, passwords, and other relevant data. With MongoDB as our database, we achieved scalability and improved performance, ensuring seamless operations even with a growing number of users.

And, to ensure that even if the database were compromised, our password would remain protected. We used bcrypt, a powerful hashing algorithm specifically designed for password hashing.

๐Ÿ’ก
We can use MVC architecture in the below code as a good practice, but I have included all components in one file for demonstration purpose.
const express = require('express');
const jwt = require('jsonwebtoken');
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const app = express();
app.use(express.json());

const SECRET = 'SECr3t';  

// Define mongoose schemas
const userSchema = new mongoose.Schema({
  username: {type: String},
  password: String,
  purchasedCourses: [{
     type: mongoose.Schema.Types.ObjectId,
      ref: 'Course'
     }]
});

const adminSchema = new mongoose.Schema({
  username: String,
  password: String
});

const courseSchema = new mongoose.Schema({
  title: String,
  description: String,
  price: Number,
  imageLink: String,
  published: Boolean
});

// Define mongoose models
const User = mongoose.model('User', userSchema);
const Admin = mongoose.model('Admin', adminSchema);
const Course = mongoose.model('Course', courseSchema);

const authenticateJwt = (req, res, next) => {
//logic for auth
};
//Connecting to our db
mongoose.connect('mongodb+srv://asrajay968:<YOUR PASSWORD>@cluster0.7poatss.mongodb.net/', { 
  useNewUrlParser: true,
  useUnifiedTopology: true,
  dbName: "courses" 
}).then(console.log(`Db connected successfully`))
  .catch(error => {
    console.log(`Db connection failed`);
    console.log(error);
    process.exit(1);
});

// Admin routes
app.post('/admin/signup', (req, res) => {
  const { username, password } = req.body;
  Admin.findOne({ username }).then(admin => {
    if (admin) {
      res.status(403).json({ message: 'Admin already exists' });
    } else {
//Using bcrypt to hash the password
      bcrypt.hash(password, 10, (err, hashedPassword) => {
        if (err) {
          res.status(500).json({ message: 'Error hashing password' });
        } else {
          const obj = { username: username, password: hashedPassword };
          const newAdmin = new Admin(obj);
          newAdmin.save();
          const token = jwt.sign({ username, role: 'admin' }, SECRET, { expiresIn: '1h' });
          res.json({ message: 'Admin created successfully', token });
        }
      });
    }
  }).catch(err => {
    res.status(500).json({ message: 'Error creating admin' });
  });
});


app.post('/admin/login', async (req, res) => {
  const { username, password } = req.body;
  Admin.findOne({ username }).then(admin => {
    if (admin) {
//Comparing the our pass to the hashed pass for login
      bcrypt.compare(password, admin.password, (err, result) => {
        if (err) {
          res.status(500).json({ message: 'Error comparing passwords' });
        } else if (result) {
          const token = jwt.sign({ username, role: 'admin' }, SECRET, { expiresIn: '1h' });
          res.json({ message: 'Logged in successfully', token });
        } else {
          res.status(403).json({ message: 'Invalid username or password' });
        }
      });
    } else {
      res.status(403).json({ message: 'Invalid username or password' });
    }
  }).catch(err => {
    res.status(500).json({ message: 'Error logging in' });
 });
});



app.listen(3000, () => console.log('Server running on port 3000'));

You made it to the end!

Great job learning about the evolution of authentication in our course selling Node.js applications! By implementing robust authentication mechanisms and addressing data persistence challenges, we've made significant strides in ensuring the security and reliability of our course app. ๐Ÿ˜Š

Stay tuned for future blog posts where we'll explore more exciting topics related to authentication in Node.js. We'll dive deep into industry best practices, discuss different storage methods, and share valuable insights on enhancing security in web applications. So, keep up the enthusiasm and happy coding as we continue to build secure and user-friendly experiences in our course-selling web app! ๐Ÿš€


I'm eager to hear your thoughts and gather any questions or topics you'd like me to cover in future articles on MERN stack, Creative web development, and QA. Your feedback is essential in ensuring that I provide valuable insights that align with your specific interests.

Additionally, I regularly share my journey through posts on Twitter, so be sure to follow me for the latest updates on all things web development. Together, we can stay informed and explore the exciting world of web development!

ย