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:
- Start by setting up a Node.js project using a package manager like npm or yarn.
- Install required dependencies such as Express, bcrypt for password hashing, and jsonwebtoken for generating and verifying JWTs (JSON Web Tokens).
- Create routes for user registration, login, and logout.
- Implement middleware for verifying JWTs on protected routes to ensure only authenticated users can access them.
- 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,
});
}
};
find all code source in real react.js node.js project :
GitHub Repo : https://github.com/idurar/idurar-erp-crm
Implementing user registration and login on the frontend:
- Set up the React.js project using create-react-app or any other preferred method.
- 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.
- Create a registration form where users can provide their details such as username and password.
- Upon form submission, dispatch an action to send the user data to the backend API for registration.
- Implement a login form with fields for username and password.
- Dispatch an action to authenticate the user credentials against the backend API.
- 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);
}
};
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');
};
Securing API requests with JWT:
- Create an interceptor in the frontend to attach the JWT token to every API request.
- Upon receiving a response from the server, check for authorization errors or expired tokens.
- If the token has expired, prompt the user to log in again.
- 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
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)
what aboutt refresh tokken?