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.
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.
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.
SOCIAL SHARE CARD GENERATOR