Secure Authentication with JWT: Implementing Token-Based Login in a MERN Stack Application
When developing a full stack application, whether it’s a web or mobile app, one of the most critical aspects to consider is authentication. Proper authentication mechanisms are essential for ensuring that user data and backend resources are secure.
Understanding Authentication
Authentication is the process of verifying the identity of a user or system. In the context of a full stack application, this means ensuring that only authorized users can access certain resources or perform specific actions. Effective authentication helps protect sensitive data and maintain the integrity of the application.
Although there are many form of Authentication in this blog we are gonna disuse JWT (JSON web token) Authentication .
What is JSON web token
JSON Web Token (JWT) is a popular authentication method that generates a token using a specified secret key. This token encodes essential information, such as a user’s ID, and is digitally signed to ensure its integrity. Once created, the JWT is stored on the client side, typically in cookies or local storage.
When a request is made to the backend, the token is included in the HTTP headers. This allows the server to verify the token’s validity using the secret key and authenticate the user’s request. By facilitating secure data exchanges and verifying user identities, JWT helps enhance the security of web applications.
Implementing JWT Authentication in a MERN App
JSON Web Token (JWT) is a popular authentication method that generates a token using a specified secret key. This token encodes essential information, such as a user’s ID, and is digitally signed to ensure its integrity. Once created, the JWT is stored on the client side, typically in cookies or local storage.
When a request is made to the backend, the token is included in the HTTP headers. This allows the server to verify the token’s validity using the secret key and authenticate the user’s request. By facilitating secure data exchanges and verifying user identities, JWT helps enhance the security of web applications.
To implement JWT authentication in your MERN (MongoDB, Express, React, Node.js) app, follow these steps:
Set Up Backend and Frontend: Ensure you have both your backend and frontend components created and connected.
Create an
.env
File: To securely manage your secret key and other environment variables, create an.env
file in your project.
Backend Implementation
- Install Necessary Packages:
npm install express jsonwebtoken dotenv bcryptjs
2.Create and Configure the .env
File: In your backend root directory, create an .env
file and add your secret key.
JWT_SECRET=your_secret_key
3. Set Up Rider Model: Define a user schema using Mongoose.
import mongoose from "mongoose";
const RiderSchema = new mongoose.Schema(
{
name: String,
email: String,
phone: Number,
password: String,
all_orders: [Number],
delivered_order: [Number],
pending_order: {
type: Number,
default: 0
},
rating: Number,
assigned_order: [Number]
},
{ timestamps: true }
);
const Rider = mongoose.model("Rider", RiderSchema);
export default Rider;
4.Create Authentication Middleware: Verify the JWT in incoming requests.
import jwt from "jsonwebtoken"
export const Authentication = async (token) => {
token=token.replace(/"/g, '');
console.log(token_key,'tokem key')
return new Promise((resolve, reject) => {
token = token.replace(/"/g, '');
jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
if (err) {
console.log(err);
reject('error');
} else {
resolve(decoded.conversation_id);
}
});
});
};
5. Create a Controller for Rider Authentication
export const RiderInformation = async (req, res) => {
try {
const authorizationHeader = req.headers['authorization'];
// console.log(authorizationHeader);
if (!authorizationHeader) {
return res.status(401).send('Not authorized');
}
let user_id = '';
try {
let tokenheaders=true;
await authorizeToken(authorizationHeader,tokenheaders)
.then(decodedId => {
user_id = decodedId;
// console.log(userId, 'user_id');
})
.catch(error => {
console.log("Token verification error:", error);
});
} catch (error) {
console.log("Token verification error:", error);
return res.status(500).json({ error: "Token verification failed" });
}
//console.log(user_id,'user_id')
try {
const rider = await Rider.findById(user_id);
if (!rider) {
console.log("error rider not found");
return res.status(404).json({ error: "Rider not found" });
}
const client = createClient();
await client.connect();
const cachedOrders = await client.get(rider.name);
if (cachedOrders) {
const orders = JSON.parse(cachedOrders);
console.log(orders);
return res.status(200).json({ rider, orders });
}
const orders = await Orders.find({ rider: rider._id }).populate("customer_id");
await client.set(rider.name, JSON.stringify(orders));
console.log(rider,orders);
return res.status(200).json({ rider, orders });
} catch (error) {
console.error(error);
return res.status(500).json({ error: "Internal server error" });
}
} catch (error) {
console.error(error);
return res.status(500).json({ error: "Internal server error" });
}
};
5. Create Routes for Rider Authentication :
import express from "express"
import {AuthenticateUser } from "../controllers/users.js";
router.post('/',AuthenticateUser)
export default router;
6.Integrate Routes in the Server:
import express from "express"
import bodyParser from "body-parser"
import cors from "cors"
import dotenv from "dotenv"
import helmet from "helmet"
import morgan from "morgan"
import mongoose from "mongoose"
import bcrypt from "bcrypt"
import Rider from "./model/rider.js"
import SalesRoutes from "./routes/sales.js"
import RIderRoutes from "./routes/riders.js"
dotenv.config();
const app=express()
app.use(bodyParser.json());
const server=http.createServer(app)
app.use(express.json())
app.use(bodyParser.urlencoded({
extended: true
}));
app.use(helmet())
app.use(helmet.crossOriginResourcePolicy({policy:"cross-origin"}))
app.use(morgan("common"))
app.use(cors())
app.get("/",(req,res)=>{
res.json("hiii")
})
app.use("/riders",RIderRoutes)
const PORT = process.env.PORT || 9000;
mongoose.set("strictQuery", false);
mongoose
.connect(process.env.MONGO_URL)
.then(() => {
console.log('MongoDB connected');
server.listen(PORT, () => console.log(`Server Port: ${PORT}`));
})
.catch((err) => console.error('MongoDB connection error:', err));
export { io };
Frontend Implementation
- npm install axios
npm install axios
Set Up Authentication Requests:
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
import { BASE_URL } from '@env';
export const RiderInformation = createAsyncThunk(
'post/RiderInformation',
async ({ rejectWithValue, token }) => {
try {
const response = await axios.post(`{BASE_URL}riders/information`, {}, {
headers: {
Authorization: `Bearer ${token}`
}
});
return response.data;
} catch (error) {
if (!error.response) {
throw error;
}
return rejectWithValue(error.response.data);
}
}
);
export const APISlice = createSlice({
name: 'API',
initialState: { data: [], error: null, status: 'idle', verifiedStatus: null }, // Fixed typo in "verfiedStatus" to "verifiedStatus"
reducers: {},
extraReducers: (builder) => {
builder
.addCase(RiderInformation.pending, (state) => {
state.status = 'loading';
})
.addCase(RiderInformation.fulfilled, (state, action) => {
state.status = 'succeeded';
state.data = action.payload;
state.error = null;
})
.addCase(RiderInformation.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload;
})
}
});
export default APISlice.reducer;
3. Integrate Authentication in React Components:
import React from 'react';
import { View, Text, Image, TextInput, TouchableOpacity, Alert } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { styles } from '../../styles/styles';
import myImage from "../../assets/Amin Resturant-logos_white.png";
import * as Yup from "yup";
import { useFormik } from "formik";
import { LoginRider } from '../../redux/slice/API';
import { useDispatch } from 'react-redux';
import * as SecureStore from 'expo-secure-store';
import { useEffect } from 'react';
import { BASE_URL } from '@env';
function Login({ navigation }) {
const dispatch = useDispatch();
const formik = useFormik({
initialValues: {
email: "",
password: ""
},
validationSchema: Yup.object({
email: Yup.string().email("Invalid email address").required("Email is required"),
password: Yup.string()
.required("Password is required")
.min(8, "Must be 8 characters or more")
.matches(/[a-z]+/, "One lowercase character")
.matches(/[A-Z]+/, "One uppercase character")
.matches(/[@$!%*#?&]+/, "One special character")
.matches(/\d+/, "One number")
}),
onSubmit: async (values) => {
try {
// alert("hiiii")
const action = await dispatch(LoginRider(values));
console.log(action,'action')
if (action.payload.error) {
Alert.alert("Login Failed", action.payload.error);
} else if (action.payload.token) {
try {
await SecureStore.setItemAsync(
'authToken',
action.payload.token
);
// console.log("Token stored successfully.");
navigation.navigate('Home');
} catch (error) {
console.log("Error:", error);
Alert.alert("Error", "Failed to store token.");
}
}
} catch (error) {
console.log("Error:", error);
console.log(`${BASE_URL}riders/login`);
Alert.alert("Login Failed", "Invalid email or password. Please try again.");
}
}
});
return (
<View style={styles.container}>
<LinearGradient
colors={['#213246', '#7fa142']}
style={styles.colorPart}
/>
<View style={styles.whitePart}>
<View style={styles.logoContainer}>
<Image style={styles.logo} source={myImage} />
</View>
<View style={styles.formContainer}>
<Text style={styles.title}>Unleash Your Ride</Text>
<TextInput
style={styles.input}
onChangeText={formik.handleChange('email')}
onBlur={formik.handleBlur('email')}
value={formik.values.email}
placeholder="Email"
/>
{formik.touched.email && formik.errors.email ? (
<Text style={styles.error}>{formik.errors.email}</Text>
) : null}
<TextInput
style={styles.input}
onChangeText={formik.handleChange('password')}
onBlur={formik.handleBlur('password')}
value={formik.values.password}
placeholder="Password"
secureTextEntry={true}
/>
{formik.touched.password && formik.errors.password ? (
<Text style={styles.error}>{formik.errors.password}</Text>
) : null}
<TouchableOpacity style={styles.button} onPress={formik.handleSubmit}>
<Text style={styles.buttonText}>Login</Text>
</TouchableOpacity>
</View>
</View>
</View>
);
}
export default Login;
import React, { useEffect, useState, useContext } from 'react';
import { View, Text, ScrollView, Alert } from 'react-native';
import { useDispatch } from 'react-redux';
import * as SecureStore from 'expo-secure-store';
import { RiderInformation } from '../../redux/slice/API';
import { UserContext } from '../../contextState/contextState';
import NewOrderModal from '../../components/NewOrderModal';
import OrdersDetails from '../../components/OrdersDetails';
import TabButton from '../../components/TabButton';
import styles from '../../styles/styles';
function Home({ navigation }) {
const dispatch = useDispatch();
const userContext = useContext(UserContext);
const { SetNewOrderModal, SetNewOrderData, SetRiderOrderData, activeTab, setActiveTab, RiderOrderData, NewOrderModal_ } = userContext;
const [CurrentOrder, SetCurrentOrders] = useState([]);
useEffect(() => {
const getTokenAndFetchOrders = async () => {
try {
const token = await SecureStore.getItemAsync('authToken');
const action = await dispatch(RiderInformation({ token }));
if (action.payload) {
SetRiderOrderData(action.payload);
}
} catch (error) {
console.error('Error retrieving token:', error);
}
};
getTokenAndFetchOrders();
}, [dispatch]);
useEffect(() => {
if (RiderOrderData && RiderOrderData.orders && RiderOrderData.orders.length > 0) {
if (activeTab === "Assigned") {
SetCurrentOrders(RiderOrderData.orders.filter(order => order.status === "Pending" || order.status === "Assigned"));
} else if (activeTab === "On Route") {
SetCurrentOrders(RiderOrderData.orders.filter(order => order.status === "On Route"));
} else if (activeTab === "Delivered") {
SetCurrentOrders(RiderOrderData.orders.filter(order => order.status === "Delivered"));
}
}
}, [activeTab, RiderOrderData]);
return (
<View>
{NewOrderModal_ && (
<NewOrderModal
Customer_name={userContext.NewOrderData[0].customer_name}
address={userContext.NewOrderData[0].address}
order_number={userContext.NewOrderData[0].order_number}
/>
)}
<View style={styles.HomePageMain}>
<Text style={styles.HomePageText}>Order List</Text>
<TabButton />
<ScrollView>
{CurrentOrder && CurrentOrder.length > 0 && CurrentOrder.map((order, index) => (
<OrdersDetails
key={index}
status={order.status}
Order_Number={order.order_id}
CustomersName={order.customer_id.name}
CustomersAddress={order.customer_id.address}
/>
))}
</ScrollView>
</View>
</View>
);
}
export default Home;
By following these steps, you can implement JWT authentication in your MERN app, providing a secure way to handle user authentication and protect sensitive routes.