Authentication is one of the most important parts of any modern web application.
While building my MERN Stack E-Commerce application, I wanted users to be able to securely create accounts, log in with their email and password, and also authenticate using Google.
For this project, I implemented:
JWT-based authentication
- Google OAuth authentication
- Bcrypt password hashing
- Protected routes
- Role-based access control
- Secure authentication flows between React and Node.js/Express**
In this article, I'll explain the concepts behind these technologies and how I used them together in my e-commerce application.
1. What is JWT?
JWT stands for JSON Web Token.
JWT is a compact way of securely transferring information between two parties.
In a web application, JWTs are commonly used to implement stateless authentication.
The basic flow looks like this:
User
↓
Login
↓
Server verifies credentials
↓
Server generates JWT
↓
JWT sent to client
↓
Client sends JWT with protected requests
↓
Server verifies JWT
↓
Access granted
What does a JWT look like?
A JWT usually looks something like:
xxxxx.yyyyy.zzzzz
It consists of three parts:
Header.Payload.Signature
Header
The header contains information about the token, such as the signing algorithm.
{
"alg": "HS256",
"typ": "JWT"
}
Payload
The payload contains claims about the user.
For example:
{
"userId": "64f123...",
"role": "admin"
}
Signature
The signature allows the server to verify that the token hasn't been modified.
Conceptually:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
The important thing to remember is that a JWT is signed, not encrypted by default.
Therefore, sensitive information such as passwords should never be placed inside the JWT payload.
2. How I Used JWT Authentication
After a user successfully logs in, my backend generates a JWT.
For example, using Node.js and the jsonwebtoken package:
import jwt from "jsonwebtoken";
const token = jwt.sign(
{
userId: user._id,
role: user.role
},
process.env.JWT_SECRET,
{
expiresIn: "1d"
}
);
The secret is stored in an environment variable rather than directly inside the source code:
JWT_SECRET=your_secret_key
The client can then use the token when making authenticated requests.
For example:
Authorization: Bearer
On the server, middleware can verify the token before allowing access to protected routes.
const token = req.headers.authorization?.split(" ")[1];
if (!token) {
return res.status(401).json({
message: "Authentication required"
});
}
const decoded = jwt.verify(token, process.env.JWT_SECRET);
After verification, the application knows which user is making the request.
3. What is OAuth?
OAuth stands for Open Authorization.
It is an authorization framework that allows an application to obtain limited access to resources or identity information without requiring the user to give the application their password.
You may have seen buttons such as:
Continue with Google
Instead of creating another password, the user can authenticate through Google.
This is where OAuth-based authentication becomes useful.
4. OAuth vs Traditional Authentication
With traditional email/password authentication:
User
↓
Email + Password
↓
Our Application
↓
Verify Password
↓
Login
With Google OAuth:
User
↓
Click "Continue with Google"
↓
Google
↓
User grants permission
↓
Google redirects back to our application
↓
Application receives authenticated user information
↓
User is logged in
The important advantage is that my application doesn't need to ask the user for their Google password.
5. Implementing Google OAuth
For my MERN application, I used Passport.js with Google's OAuth strategy.
The basic idea is to configure a Google strategy:
import passport from "passport";
import { Strategy as GoogleStrategy } from "passport-google-oauth20";
passport.use(
new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: "/auth/google/callback"
},
async (accessToken, refreshToken, profile, done) => {
// Find or create user
// ...
done(null, user);
}
)
);
The credentials are stored in environment variables:
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
When a user chooses Google login, Google handles the authentication process and redirects the user back to the application.
My backend can then find an existing user or create a new account.
6. What is Hashing?
Before implementing password authentication, there is another important concept to understand:
Hashing.
Hashing converts data into a fixed-length string using a mathematical function.
For example:
MyPassword123
↓
Hashing
↓
$2b$10$...
Unlike encryption, hashing is designed to be one-way.
That means we shouldn't be able to simply take a password hash and turn it back into the original password.
This is extremely important for password storage.
7. Why Shouldn't We Store Passwords Directly?
Imagine a database containing:
email: hamza@example.com
password: MyPassword123
If an attacker gains access to the database, they can immediately see the user's password.
This is why applications should never store plaintext passwords.
Instead, we store something like:
*email: hamza@example.com
password:
$2b$10$K8J7...
*
The database contains the hash rather than the original password.
8. How Bcrypt Helps Secure Passwords
For my application, I used Bcrypt to hash passwords.
Bcrypt is specifically designed for password hashing and includes a configurable cost factor that makes password cracking more computationally expensive.
First, install it:
npm install bcrypt
Then import it:
import bcrypt from "bcrypt";
9. Hashing a Password with Bcrypt
When a user registers, I don't store their password directly.
Instead:
const hashedPassword = await bcrypt.hash(password, 10);
Here:
10
is the salt-rounds/cost parameter.
The resulting hash can then be stored in MongoDB.
For example:
const user = await User.create({
name,
email,
password: hashedPassword
});
The database stores the hash instead of:
MyPassword123
10. What is a Salt?
A salt is random data added during password hashing.
Bcrypt automatically generates and incorporates a salt into the resulting hash.
For example:
Password
+
Random Salt
↓
Bcrypt
↓
Password Hash
This helps prevent attackers from efficiently using precomputed hash tables against many users with the same password.
Another useful property is that two users with the same password can still have different bcrypt hashes because different salts are generated.
11. How Login Password Verification Works
One common mistake beginners make is trying to decrypt the stored password hash.
That's not how password hashing works.
Instead, when the user logs in, we use:
bcrypt.compare(password, user.password);
For example:
const isPasswordCorrect = await bcrypt.compare(
password,
user.password
);
if (!isPasswordCorrect) {
return res.status(401).json({
message: "Invalid credentials"
});
}
Bcrypt takes the entered password, applies the appropriate hashing process using the information stored in the hash, and checks whether it matches.
If it matches:
Password correct
↓
Generate JWT
↓
User authenticated
If it doesn't:
Password incorrect
↓
Reject login
12. Putting JWT + Bcrypt Together
This is where the authentication system starts coming together.
Registration
When a user registers:
User enters email + password
↓
Bcrypt hashes password
↓
Hash stored in MongoDB
The plaintext password is not stored.
Login
When the user logs in:
User enters email + password
↓
Find user in MongoDB
↓
bcrypt.compare()
↓
Password valid?
↓ ↓
Yes No
↓ ↓
Generate JWT Reject login
↓
Return authentication result
Accessing Protected Resources
Once authenticated:
Client
↓
JWT
↓
Protected API endpoint
↓
JWT verification middleware
↓
Valid token?
↓
Controller
This allows me to protect endpoints such as:
/api/orders
/api/profile
/api/admin
13. Protecting Routes with Middleware
Express middleware can be used to verify JWTs before allowing access to protected endpoints.
For example:
const protect = (req, res, next) => {
const token = req.headers.authorization?.split(" ")[1];
if (!token) {
return res.status(401).json({
message: "Not authenticated"
});
}
try {
const decoded = jwt.verify(
token,
process.env.JWT_SECRET
);
req.user = decoded;
next();
} catch (error) {
return res.status(401).json({
message: "Invalid or expired token"
});
}
};
Now I can protect routes:
router.get("/orders", protect, getOrders);
Only authenticated users can access this endpoint.
14. Adding Role-Based Access Control
Authentication answers:
Who are you?
Authorization answers:
What are you allowed to do?
For an e-commerce application, this distinction is important.
For example:
Customer
├── View products
├── Add products to cart
└── Place orders
Admin
├── Add products
├── Update products
├── Delete products
└── Manage orders
I can include the user's role in the JWT:
const token = jwt.sign(
{
userId: user._id,
role: user.role
},
process.env.JWT_SECRET,
{
expiresIn: "1d"
}
);
Then middleware can check the role before allowing access to admin functionality.
For example:
if (req.user.role !== "admin") {
return res.status(403).json({
message: "Access denied"
});
}
This gives the application both:
Authentication
Authorization
15. The Complete Authentication Architecture
The authentication system in my MERN e-commerce application can be visualized like this:
┌──────────────┐
│ React │
│ Frontend │
└──────┬───────┘
│
┌─────────────┴─────────────┐
│ │
Email/Password Google OAuth
│ │
↓ ↓
Express API Google
│ │
↓ ↓
Bcrypt Verify OAuth Callback
│ │
└─────────────┬─────────────┘
↓
User Authenticated
│
↓
JWT Token
│
↓
Protected API Routes
│
↓
MongoDB
16. Security Lessons I Learned
Building authentication for a real application taught me that authentication isn't just about creating a login page.
There are several important security practices to keep in mind.
Never store plaintext passwords
Always use a password hashing algorithm such as Bcrypt or another modern password-hashing solution.
Never put passwords inside JWTs
JWT payloads are not encrypted by default.
Keep secrets in environment variables
For example:
JWT_SECRET=...
GOOGLE_CLIENT_SECRET=...
Don't commit these values to GitHub.
Validate user input
Don't blindly trust data coming from the frontend.
Validate things such as:
Email format
Password requirements
Required fields
User IDs
Request data
Use HTTPS in production
Authentication credentials and tokens should be transmitted over encrypted connections.
Give tokens appropriate expiration times
A token that remains valid indefinitely increases the impact if it is stolen.
Protect sensitive endpoints
Routes such as admin dashboards and order management should require appropriate authentication and authorization.
17. What I Built
By combining these technologies, I was able to create a complete authentication system for my MERN e-commerce application.
The application supports:
User registration
Email/password login
Bcrypt password hashing
Password verification
JWT-based authentication
Protected API routes
Google OAuth login
User roles
Admin-only functionality
MongoDB user storage
The biggest thing I learned was that authentication is not a single feature.
It's a combination of multiple concepts working together:
Bcrypt
↓
Secure password storage
OAuth
↓
Third-party authentication
JWT
↓
Authenticated API requests
Middleware
↓
Protected routes
Roles
↓
Authorization
Conclusion
Implementing authentication was one of the more valuable parts of building my MERN e-commerce application because it forced me to understand what happens behind a simple Login button.
Before working on this project, authentication could seem like:
Login → Done
But behind that button are several important systems:
Password Hashing
↓
Credential Verification
↓
OAuth
↓
JWT
↓
Middleware
↓
Authorization
↓
Protected Resources
Understanding these concepts has made it much easier for me to design authentication systems in other full-stack applications.
If you're building a MERN application yourself, I highly recommend implementing authentication as a project rather than only learning the concepts theoretically. Building the complete flow makes the relationship between Bcrypt, OAuth, JWT, Express middleware, MongoDB, and React much clearer.
Top comments (1)