Securing Your Node.js App: A Deep Dive into Effective Authentication Techniques

Securing Your Node.js App: A Deep Dive into Effective Authentication Techniques

Authentication plays a pivotal role in the security of modern web applications, ensuring a safe and personalized user experience. It involves the verification of user identities and permissions, safeguarding sensitive data and resources from unauthorized access. In this post, we will explore the significance of authentication, and its benefits to users, and discuss various authentication techniques. We will then delve into setting up different types of authentication using Node.js, my favorite framework known for its performance and flexibility.

Prerequisites

To follow through in this article, a basic understanding of the following is required:

  • NodeJS

  • ExpressJS

    Command Line

  • Postman

What is Authentication?

Authentication is the process of verifying the identity of a user to determine if they are authorized to access specific resources. It involves asking the user "Who are you" and validating their credentials. For instance, during a login attempt, the user's input on the client side is compared with the stored data on the server side. If the information matches, the user is successfully authenticated and allowed access.

Analogous to the Cinderella story, where the prince used a shoe fitting test to authenticate the rightful owner, authentication serves to confirm and validate the user's identity.

Authentication Techniques

  1. Local or Basic Authentication: The simplest form, where users provide a username and password, and access is granted if the credentials match those stored in the database.

  2. Authentication by OTP: Also known as password-free authentication, this method sends a one-time password (OTP) to the user's device. The user is authenticated if the entered OTP matches the one on the server side.

  3. Single Sign-On (SSO): This technique allows users to access multiple applications with just one sign-in. An example is using one account to access both Gmail and Gdrive.

  4. Open Authorization (OAuth): Involves using existing login credentials from various platforms like Google, Twitter, Facebook, or Github for authentication, without requiring additional security measures.

  5. Two-Factor/Multi-Factor Authentication (2FA/MFA): An extra layer of security is added, requiring additional checks before successful authentication, such as sending an OTP to the user's device or a token to their email address.

Now you’ve understood the different authentication techniques, let’s discuss how to implement the various types.

Session or Cookie-based Authentication:

How it works:

  1. Upon receiving a login request from a client, the server verifies the provided credentials and proceeds to generate a session.

  2. The session data is temporarily held in the server's memory and assigned a unique session ID.

  3. The client stores the received session ID within a cookie for future reference.

  4. Subsequent client requests include the current session ID from the cookie to maintain the session's continuity.

  5. When the user initiates a logout, the server clears both the session ID stored in the cookie and removes the associated session data from the server. As a result, the same session ID cannot be reused.

Run the command below in your terminal to set up a session or cookie-based authentication:

npm install express express-session

By running the aforementioned command, the necessary modules and dependencies will be installed, enabling the implementation of session or cookie-based authentication. Below is a code snippet to assist you in this process:

Create an app.js file inside the project directory and paste the code below:

app.js:


const express = require('express');
const session = require('express-session');

const app = express();
app.use(session({
  secret: 'your-secret-key',
  resave: false,
  saveUninitialized: false
}));

// Function to generate a session id
function generateSessionId() {
  return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
}

// Middleware to check if the user is logged in
function checkLoggedIn(req, res, next) {
  if (req.session.userId) {
    next();
  } else {
    res.status(401).send('Unauthorized: Please log in.');
  }
}
// Login route
app.post('/login', (req, res) => 
  req.session.userId = generateSessionId();
  res.send('Login successful');
});

app.get('/authenticatedRoute', checkLoggedIn, (req, res) => {
    res.send('Authenticated Route!');
});

// Logout route
app.post('/logout', (req, res) => {
  req.session.destroy(err => {
    if (err) {
      res.status(500).send('Error logging out.');
    } else {
      res.clearCookie('connect.sid');
      res.send('Logout successful');
    }
  });
});

// Start the server
const port = 8000;
app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

Explanation:

When a user initiates a login request through the /login route, a fresh session is established, and a unique userId is generated randomly. This userId is then stored within the session. Access to the /authenticatedRoute route is safeguarded by the checkLoggedIn middleware, which permits only authenticated users (those possessing a valid userId within their session) to proceed.

Whenever the user decides to log out using the /logout route, their session is terminated, and the associated session ID cookie is cleared. Before running the server with the provided code, ensure you have installed the necessary dependencies. You can then start the server by running node app.js.

Token-Based Authentication:

How it works:

  1. Users submit their credentials(username and password) which are encoded into a string using the base64 algorithm.

  2. The encoded string is set in the Authorization header and is sent along with each HTTP Request.

To set up token-based authentication, run the command below in your terminal:

npm install express body-parser jsonwebtoken

By running the aforementioned command, the necessary modules and dependencies will be installed, enabling the implementation of token-based authentication. Below is a code snippet to assist you in this process:

Create an app.js file inside the project directory and paste the code below:

app.js:


const express = require('express');
const bodyParser = require('body-parser');
const jwt = require('jsonwebtoken');

const app = express();
const secretKey = 'your-secret-key'; 

const users = [
  { username: 'user1', password: 'hashedPassword1' }, 
  { username: 'user2', password: 'hashedPassword2' },
];

function generateToken(username) {
  return jwt.sign({ username }, secretKey, { expiresIn: '1h' }); 
}

function authenticateToken(req, res, next) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];

  if (!token) {
    return res.sendStatus(401);
  }

  jwt.verify(token, secretKey, (err, decoded) => {
    if (err) {
      return res.sendStatus(403);
    }

    req.user = decoded;
    next();
  });
}

app.use(bodyParser.json());

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

  if (!user) {
    return res.sendStatus(401);
  }

  const token = generateToken(username);
  res.json({ token });
});

app.get('/protected', authenticateToken, (req, res) => {
  res.json({ message: 'Protected Resource Accessed', user: req.user });
});

const port = 3000;
app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

Explanation:

The generateToken function generates a JWT with a 1-hour expiration time based on the provided username. In addition, the authenticateToken middleware uses jwt.verify to validate the token found in the Authorization header.

Upon successful validation, the decoded username is added to the request object (req.user), allowing the middleware to proceed with the request. The /login route remains responsible for user authentication and returns a JWT upon successful login. The /protected route remains protected by the authenticateToken middleware, granting access to the protected resource only when a valid JWT is provided.

Passwordless Authentication:

Passwordless authentication involves sending a magic link to the user's email address instead of traditional credentials.

How it works:

  1. The user provides their email address during the registration process. After that, a unique magic link generated by the server is generated. The magic link is associated with the user’s account.

  2. The magic link is sent to the user's email address. The email usually includes a call-to-action button or URL that the user can click.

  3. When the user clicks the magic link, they are redirected to the application or website.

4. If the magic link is valid and not expired, the user is considered authenticated and granted access to the website or resource.

Note:

      • Magic links are one-time-use tokens, which means they are valid for a single authentication session. This means that once the user clicks the magic link and successfully authenticates, the link becomes invalid, thereby enhancing security.

To setup passwordless authentication, run the command below in your terminal:

npm install express body-parser nodemailer

By running the aforementioned command, the necessary modules and dependencies will be installed, enabling the implementation of passwordless authentication. Below is a code snippet to assist you in this process:

Create an app.js file inside the project directory and paste the code below:

app.js:


const express = require('express');
const bodyParser = require('body-parser');
const nodemailer = require('nodemailer');

const app = express();
const PORT = 3000;

// In a real application, use a proper database for user accounts.
const users = [
  { id: 1, email: 'user1@example.com' },
  { id: 2, email: 'user2@example.com' },
];

// Generate a magic link for the user
function generateToken() {
  return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
}

// Send the magic link to the user's email
async function sendMagicLink(email, token) {
  try {

    const transporter = nodemailer.createTransport({
      host: 'smtp.example.com',
      port: 587,
      secure: false,
      auth: {
        user: 'your-email@example.com',
        pass: 'your-email-password',
      },
    });

    const message = {
      from: 'your-email@example.com',
      to: email,
      subject: 'Magic Link for Authentication',
      text: `Click the following link to log in: http://localhost:${PORT}/auth/${token}`,
    };

    await transporter.sendMail(message);
  } catch (error) {
    console.error('Error sending email:', error);
  }
}

app.use(bodyParser.json());

// Initialization of passwordless authentication
app.post('/login', (req, res) => {
  const { email } = req.body;
  const user = users.find((u) => u.email === email);

  if (!user) {
    return res.status(404).json({ error: 'User not found.' });
  }

  const token = generateToken();

  sendMagicLink(email, token);
  res.json({ message: 'Magic link sent to your email. Check your inbox.' });
});

// Authentication route using the magic link
app.get('/auth/:token', (req, res) => {
  const { token } = req.params;
  res.json({ message: 'Authentication successful. You are now logged in.' });
});

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Explanation:

The code utilizes Express and Nodemailer for passwordless authentication. It stores sample user data in the user's array. The generateToken function creates a random token for users, while sendMagicLink simulates sending the magic link via Nodemailer (in a real app, configure a proper email transporter).

The /login route initiates passwordless authentication, generating a token based on the provided email and saving it for verification. The /auth/:token route simulates authentication using the magic link.

Importance of Authentication

  1. Security: Authentication ensures that only authorized users can access sensitive data, preventing unauthorized external access.

  2. Privacy: It helps maintain data privacy and protects against external intrusion.

  3. User experience: Authentication enhances user experience by providing personalized services, such as displaying account history and personal information.

Conclusion

In conclusion, authentication is crucial in verifying user identities, ensuring data security, and providing a better user experience. Different techniques can be employed based on the desired level of security and user convenience.

Thanks for reading :)