DEV Community

Cover image for Advanced Secure Node.js Auth with JWT
Salah Eddine Lalami for IDURAR | Where Ai Build Software

Posted on • Updated on

Advanced Secure Node.js Auth with JWT

In This tutorial we will learn How to Secure React.js App Auth with JWT httpOnly Cookies , Redux and Node.js

In modern web development, implementing secure authentication is of paramount importance to protect sensitive user information and prevent unauthorized access. This article will guide you through the process of securing React.js authentication using Redux for state management and Node.js as the backend server.

Setting up the backend with Node.js and Express:

  1. Start by setting up a Node.js project using a package manager like npm or yarn.
  2. Install required dependencies such as Express, bcrypt for password hashing, and jsonwebtoken for generating and verifying JWTs (JSON Web Tokens).
  3. Create routes for user registration, login, and logout.
  4. Implement middleware for verifying JWTs on protected routes to ensure only authenticated users can access them.
  5. Store user information securely in a database, ensuring that passwords are properly hashed and salted.

const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');

const mongoose = require('mongoose');

const Admin = mongoose.model('Admin');

require('dotenv').config({ path: '.variables.env' });

exports.login = async (req, res) => {
  try {
    const { email, password } = req.body;

    // validate
    if (!email || !password)
      return res.status(400).json({
        success: false,
        result: null,
        message: 'Not all fields have been entered.',
      });

    const admin = await Admin.findOne({ email: email, removed: false });

    if (!admin)
      return res.status(400).json({
        success: false,
        result: null,
        message: 'No account with this email has been registered.',
      });

    const isMatch = await bcrypt.compare(password, admin.password);
    if (!isMatch)
      return res.status(400).json({
        success: false,
        result: null,
        message: 'Invalid credentials.',
      });

    const token = jwt.sign(
      {
        id: admin._id,
      },
      process.env.JWT_SECRET,
      { expiresIn: req.body.remember ? 365 * 24 + 'h' : '24h' }
    );

    const result = await Admin.findOneAndUpdate(
      { _id: admin._id },
      { isLoggedIn: true },
      {
        new: true,
      }
    ).exec();

    res
      .status(200)
      .cookie('token', token, {
        maxAge: req.body.remember ? 365 * 24 * 60 * 60 * 1000 : null, // Cookie expires after 30 days
        sameSite: 'Lax',
        httpOnly: true,
        secure: process.env.NODE_ENV === 'production' ? true : false,
        domain: req.hostname,
        Path: '/',
      })
      .json({
        success: true,
        result: {
          token,
          admin: {
            id: result._id,
            name: result.name,
            isLoggedIn: result.isLoggedIn,
          },
        },
        message: 'Successfully login admin',
      });
  } catch (err) {
    res.status(500).json({ success: false, result: null, message: err.message, error: err });
  }
};

exports.isValidAdminToken = async (req, res, next) => {
  try {
    const token = req.cookies.token;

    if (!token)
      return res.status(401).json({
        success: false,
        result: null,
        message: 'No authentication token, authorization denied.',
        jwtExpired: true,
      });

    const verified = jwt.verify(token, process.env.JWT_SECRET);

    if (!verified)
      return res.status(401).json({
        success: false,
        result: null,
        message: 'Token verification failed, authorization denied.',
        jwtExpired: true,
      });

    const admin = await Admin.findOne({ _id: verified.id, removed: false });
    if (!admin)
      return res.status(401).json({
        success: false,
        result: null,
        message: "Admin doens't Exist, authorization denied.",
        jwtExpired: true,
      });

    else {
      req.admin = admin;
      next();
    }
  } catch (err) {
    res.status(503).json({
      success: false,
      result: null,
      message: err.message,
      error: err,
    });
  }
};

Enter fullscreen mode Exit fullscreen mode

find all code source in real react.js node.js project :

GitHub Repo : https://github.com/idurar/idurar-erp-crm

Open Source ERP / CRM

Implementing user registration and login on the frontend:

  1. Set up the React.js project using create-react-app or any other preferred method.
  2. Install necessary dependencies such as axios for making API requests, react-router-dom for handling client-side routing, and redux-thunk for asynchronous action creators.
  3. Create a registration form where users can provide their details such as username and password.
  4. Upon form submission, dispatch an action to send the user data to the backend API for registration.
  5. Implement a login form with fields for username and password.
  6. Dispatch an action to authenticate the user credentials against the backend API.
  7. If the authentication is successful, store the JWT received from the server in the browser's in cookies onlyHttp for future requests.

import { API_BASE_URL } from '@/config/serverApiConfig';

import axios from 'axios';
import errorHandler from '@/request/errorHandler';
import successHandler from '@/request/successHandler';

export const login = async ({ loginData }) => {
  try {
    const response = await fetch(API_BASE_URL + `login?timestamp=${new Date().getTime()}`, {
      method: 'POST', // *GET, POST, PUT, DELETE, etc.
      mode: 'cors', // no-cors, *cors, same-origin
      cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cache
      credentials: 'include',
      headers: {
        'Content-Type': 'application/json',
      },
      redirect: 'follow', // manual, *follow, error
      referrerPolicy: 'no-referrer', // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
      body: JSON.stringify(loginData), // body data type must match "Content-Type" header
    });

    const { status } = response;
    const data = await response.json();

    successHandler(
      { data, status },
      {
        notifyOnSuccess: false,
        notifyOnFailed: true,
      }
    );
    return data;
  } catch (error) {
    return errorHandler(error);
  }
};
export const logout = async () => {
  axios.defaults.withCredentials = true;
  try {
    window.localStorage.clear();
    await axios.post(API_BASE_URL + `logout?timestamp=${new Date().getTime()}`);
  } catch (error) {
    return errorHandler(error);
  }
};
Enter fullscreen mode Exit fullscreen mode

Why use Redux for managing authentication state?

Redux is a predictable state container that helps manage the global state of an application in a consistent and organized manner. By centralizing the authentication state in Redux, we can easily synchronize the login/logout status across different components and make secure API requests.

import * as actionTypes from './types';
import * as authService from '@/auth';

import history from '@/utils/history';

export const login =
  ({ loginData }) =>
  async (dispatch) => {
    dispatch({
      type: actionTypes.LOADING_REQUEST,
      payload: { loading: true },
    });
    const data = await authService.login({ loginData });

    if (data.success === true) {
      window.localStorage.setItem('isLoggedIn', true);
      window.localStorage.setItem('auth', JSON.stringify(data.result.admin));
      dispatch({
        type: actionTypes.LOGIN_SUCCESS,
        payload: data.result.admin,
      });
      history.push('/');
    } else {
      dispatch({
        type: actionTypes.FAILED_REQUEST,
        payload: data,
      });
    }
  };

export const logout = () => async (dispatch) => {
  authService.logout();
  dispatch({
    type: actionTypes.LOGOUT_SUCCESS,
  });
  history.push('/login');
};


Enter fullscreen mode Exit fullscreen mode

Securing API requests with JWT:

  1. Create an interceptor in the frontend to attach the JWT token to every API request.
  2. Upon receiving a response from the server, check for authorization errors or expired tokens.
  3. If the token has expired, prompt the user to log in again.
  4. If an error occurs due to unauthorized access, redirect the user to the login page.

find all code source in real react.js node.js project :

GitHub Repo : https://github.com/idurar/idurar-erp-crm

Open Source ERP / CRM

By following these steps, you can ensure a secure authentication flow in your React.js application using Redux for state management and Node.js as the backend server. Remember to always implement best practices for secure password storage, token handling, and API request verification to protect user information and maintain data integrity.

Top comments (1)

Collapse
 
arslanahmed777 profile image
Arslan Ahmed Shaad

what aboutt refresh tokken?