Authentication used to stress me out.
Not because it's conceptually hard, but because every tutorial I found either oversimplified it to the point of being useless, or made it so complicated I needed a PhD to understand what was happening.
After building several projects and breaking authentication in creative ways, I finally figured out a setup that actually works and doesn't make me want to throw my laptop out the window.
Here's how I do it, explained like I'm talking to a friend over coffee, not writing a textbook.
What We're Working With
Node.js & Express - The backend
MongoDB & Mongoose - Where user data lives
bcryptjs - Makes passwords unreadable (important!)
jsonwebtoken - Creates those tokens everyone talks about
cookie-parser - Handles cookies properly
dotenv - Keeps secrets actually secret
Nothing fancy. Nothing you can't install in 30 seconds.
Step 1: Letting People Sign Up
When someone registers, we need to save their info. But we can't just store their password as-is. That's like leaving your house key under the doormat with a sign that says "KEY HERE."
const bcrypt = require('bcryptjs');
const User = require('../models/User');
app.post('/api/register', async (req, res) => {
const { username, email, password } = req.body;
// Turn the password into scrambled nonsense
const hashedPassword = await bcrypt.hash(password, 10);
const user = new User({ username, email, password: hashedPassword });
try {
await user.save();
res.status(201).json({ message: 'User created successfully' });
} catch (err) {
res.status(400).json({ error: 'User already exists or invalid input' });
}
});
What's happening: We take their password and scramble it (that's the hashing part). Even if someone breaks into your database, they just see gibberish instead of actual passwords.
The hacker after breaking into your database:
Step 2: Logging In and Getting Tokens
This is where it gets interesting. When someone logs in, we give them TWO tokens:
Access token - Short-lived (15 minutes). Like a temporary pass to get into places.
Refresh token - Lasts longer (5 days). Used to get new access tokens without logging in again.
Why two? Security. If someone steals your access token, it expires quickly. The refresh token stays safe in a cookie.
const jwt = require('jsonwebtoken');
const generateAccessToken = (payload) => {
return jwt.sign(payload, process.env.JWT_SECRET, {expiresIn: '15m'})
}
app.post('/api/login', async (req, res) => {
const { email, password } = req.body;
// Find the user
const user = await User.findOne({ email });
if (!user) return res.status(401).json({ error: 'Invalid credentials' });
// Check if password matches
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) return res.status(401).json({ error: 'Invalid credentials' });
// Package up user info
const payload = {
id: user._id,
username: user.username,
email: user.email
}
// Create both tokens
const refreshToken = jwt.sign(
payload,
process.env.JWT_REFRESH_SECRET,
{ expiresIn: '5d' }
);
const accessToken = generateAccessToken(payload);
// Send refresh token as a secure cookie
res.cookie("refreshToken", refreshToken, {
secure: true,
httpOnly: true,
maxAge: 1000 * 60 * 60 * 24 * 7 // 7 days
})
res.status(200).json({ accessToken });
});
What's happening: We check if the user exists, verify their password, then create two tokens. The refresh token goes in a special cookie that JavaScript can't touch (that's the httpOnly
part). The access token goes in the response.
Step 3: Getting a New Access Token
After 15 minutes, your access token expires. Instead of making users log in again, we use the refresh token to get a new one.
First, set up cookie parsing in your main file:
const cookieParser = require("cookie-parser");
app.use(cookieParser());
Then create the refresh endpoint:
app.post('/token/refresh', (req, res) => {
// Get the refresh token from the cookie
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) return res.status(401).json({ error: 'No token' });
// Verify it's legit
const verifiedToken = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
if (!verifiedToken) return res.status(403).json({ error: 'Invalid token' });
// Create a new access token
const {id, username, email} = verifiedToken;
const accessToken = generateAccessToken({id, username, email});
res.status(200).json({ accessToken });
});
What's happening: We grab the refresh token from the cookie, check if it's valid, and if it is, we create a fresh access token. Simple.
Step 4: Protecting Routes
Now we need to make sure only logged-in users can access certain routes. That's what middleware is for.
const authMiddleware = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader) return res.status(401).json({ error: 'No token provided' });
// Token comes as "Bearer actualtoken", so we split it
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded; // Now we know who's making the request
next();
} catch (err) {
res.status(403).json({ error: 'Invalid or expired token' });
}
};
// Use it on any route you want to protect
app.get('/api/profile', authMiddleware, async (req, res) => {
const user = await User.findById(req.user.id).select('-password');
res.json(user);
});
What's happening: The middleware checks if there's a valid token. If yes, attach the user info to the request and move on. If no, reject them. Think of it as a bouncer checking IDs.
Why This Setup Works
It's stateless. No need to store sessions on the server. Everything lives in the tokens.
It scales. Works whether you have 10 users or 10,000.
It's secure enough. Passwords are hashed, tokens expire, and the refresh token is protected.
It's not overwhelming. You can understand what each piece does without a computer science degree.
What I'd Add Next
Right now, this handles basic authentication. But for bigger projects, you might want:
Role-based access - Admins can do more than regular users
Token blacklisting - Invalidate tokens when users log out
Rate limiting - Stop people from spamming your login endpoint
But for most projects? This setup is solid.
The Real Talk
Authentication doesn't have to be scary or complicated. Yes, there are edge cases. Yes, there are more secure ways to do things. But this approach has worked for my projects, and it's simple enough that I can set it up in an hour without pulling my hair out.
If you're just starting with backend auth or you've been putting it off because it seems overwhelming, start here. Get it working, understand what each part does, then optimize later.
Perfect is the enemy of shipped.
Got questions? Different approach? Let me know in the comments. Always happy to learn better ways to do this.
Originally published on My Blog
Top comments (0)