In modern web development, ensuring the security of user data and providing a smooth, seamless experience for users is critical. One of the cornerstones of a secure web application is authentication. Authentication is the process of verifying the identity of a user before granting access to resources, and it’s fundamental to almost all web applications.
In this blog post, we’ll walk through how to set up a secure login system using the MERN stack—MongoDB, Express.js, React, and Node.js. We’ll cover the implementation of both front-end and back-end authentication and integrate security practices such as JWT (JSON Web Tokens) for token-based authentication, password hashing, and session management.
Before diving into the specifics of setting up authentication, let’s briefly discuss the MERN stack:
These four technologies work seamlessly together, and authentication is an essential feature in most MERN applications.
First, let’s begin by setting up the backend, where we will handle authentication logic.
In the backend folder of your MERN project, run the following commands to install necessary dependencies:
bash
code
npm init -y
npm install express mongoose bcryptjs jsonwebtoken dotenv cors
Here’s what these dependencies do:
.env
file.Create a basic Express server in server.js
:
javascript
code
const express =
require(
‘express’);
const mongoose =
require(
‘mongoose’);
const cors =
require(
‘cors’);
require(
‘dotenv’).
config();
const app =
express();
app.
use(
cors());
app.
use(express.
json());
// For parsing application/json
// Connect to MongoDB
mongoose.
connect(process.
env.
MONGO_URI, {
useNewUrlParser:
true,
useUnifiedTopology:
true,
}).
then(
() => {
console.
log(
‘MongoDB connected’);
}).
catch(
(err) => {
console.
log(err);
});
const
PORT = process.
env.
PORT ||
5000;
app.
listen(
PORT,
() => {
console.
log(
`Server running on port ${PORT}`);
});
Let’s create a user model to define how user data will be structured in the database. In the models
folder, create User.js
:
javascript
code
const mongoose =
require(
‘mongoose’);
const bcrypt =
require(
‘bcryptjs’);
const userSchema =
new mongoose.
Schema({
username: {
type:
String,
required:
true,
unique:
true },
email: {
type:
String,
required:
true,
unique:
true },
password: {
type:
String,
required:
true }
});
// Hash password before saving user
userSchema.
pre(
‘save’,
async
function(
next) {
if (!
this.
isModified(
‘password’))
return
next();
const salt =
await bcrypt.
genSalt(
10);
this.
password =
await bcrypt.
hash(
this.
password, salt);
next();
});
// Compare password with hashed password
userSchema.
methods.
comparePassword =
function(
password) {
return bcrypt.
compare(password,
this.
password);
};
module.
exports = mongoose.
model(
‘User’, userSchema);
In the routes
folder, create an auth.js
file to handle the login and registration functionality:
javascript
code
const express =
require(
‘express’);
const
User =
require(
‘../models/User’);
const jwt =
require(
‘jsonwebtoken’);
const router = express.
Router();
// Register user
router.
post(
‘/register’,
async (req, res) => {
const { username, email, password } = req.
body;
try {
const user =
new
User({ username, email, password });
await user.
save();
res.
status(
201).
json({
message:
‘User registered successfully’ });
}
catch (error) {
res.
status(
400).
json({
message:
‘Error registering user’ });
}
});
// Login user
router.
post(
‘/login’,
async (req, res) => {
const { email, password } = req.
body;
try {
const user =
await
User.
findOne({ email });
if (!user) {
return res.
status(
400).
json({
message:
‘Invalid email or password’ });
}
const isMatch =
await user.
comparePassword(password);
if (!isMatch) {
return res.
status(
400).
json({
message:
‘Invalid email or password’ });
}
const token = jwt.
sign({
id: user.
_id }, process.
env.
JWT_SECRET, {
expiresIn:
‘1h’ });
res.
json({ token });
}
catch (error) {
res.
status(
500).
json({
message:
‘Server error’ });
}
});
module.
exports = router;
In this code:
bcryptjs
and compared during login.In server.js
, import and use the routes:
javascript
code
const authRoutes =
require(
‘./routes/auth’);
app.
use(
‘/api/auth’, authRoutes);
Now, let’s focus on the frontend where users will interact with the login and registration forms.
In the React frontend folder, install the following dependencies:
bash
code
npm install axios react-router-dom
Create two React components, Login.js
and Register.js
.
Login.js:
javascript
code
import { useState }
from
‘react’;
import axios
from
‘axios’;
import { useHistory }
from
‘react-router-dom’;
const
Login = () => {
const [email, setEmail] =
useState(
”);
const [password, setPassword] =
useState(
”);
const history =
useHistory();
const
handleLogin =
async (
e) => {
e.
preventDefault();
try {
const response =
await axios.
post(
‘http://localhost:5000/api/auth/login’, { email, password });
localStorage.
setItem(
‘token’, response.
data.
token);
history.
push(
‘/’);
}
catch (error) {
console.
error(
‘Invalid credentials’);
}
};
return (
<form onSubmit={handleLogin}>
<input type=”email” placeholder=”Email” value={email} onChange={(e) => setEmail(e.target.value)} required />
<input type=”password” placeholder=”Password” value={password} onChange={(e) => setPassword(e.target.value)} required />
<button type=”submit”>Login</button>
</form>
);
};
export
default
Login;
Register.js:
javascript
code
import { useState }
from
‘react’;
import axios
from
‘axios’;
import { useHistory }
from
‘react-router-dom’;
const
Register = () => {
const [username, setUsername] =
useState(
”);
const [email, setEmail] =
useState(
”);
const [password, setPassword] =
useState(
”);
const history =
useHistory();
const
handleRegister =
async (
e) => {
e.
preventDefault();
try {
await axios.
post(
‘http://localhost:5000/api/auth/register’, { username, email, password });
history.
push(
‘/login’);
}
catch (error) {
console.
error(
‘Registration failed’);
}
};
return (
<form onSubmit={handleRegister}>
<input type=”text” placeholder=”Username” value={username} onChange={(e) => setUsername(e.target.value)} required />
<input type=”email” placeholder=”Email” value={email} onChange={(e) => setEmail(e.target.value)} required />
<input type=”password” placeholder=”Password” value={password} onChange={(e) => setPassword(e.target.value)} required />
<button type=”submit”>Register</button>
</form>
);
};
export
default
Register;
In this blog post, we’ve covered the basic setup of a secure login system using MERN stack:
bcryptjs
to ensure sensitive data is protected.Security is an ongoing process, and there are many other techniques you can use to enhance your application, including:
By following best practices and staying up to date with security trends, you can build a secure and user-friendly authentication system for your MERN stack application.
localStorage
.Let me know if you need further assistance!
Comments are closed